Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Faster Cache #121

Merged
merged 11 commits into from
Sep 17, 2020
Merged

Faster Cache #121

merged 11 commits into from
Sep 17, 2020

Conversation

JimBobSquarePants
Copy link
Member

@JimBobSquarePants JimBobSquarePants commented Sep 11, 2020

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

Introduces a time aware least recently used cache to store results of cache lookups. This removes most of the IO required to check whether an image is in the cache for subsequent requests for the same URL.

I'm extremely concerned about the way our AsyncKeyLock works now though. Cache lookups are now so fast we're getting misses as the next request for the same URL is attempting to read the cache. This leads to multiple writes which are safe but inefficient.

@codecov
Copy link

codecov bot commented Sep 11, 2020

Codecov Report

Merging #121 into master will increase coverage by 0.26%.
The diff coverage is 93.63%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #121      +/-   ##
==========================================
+ Coverage   87.98%   88.24%   +0.26%     
==========================================
  Files          44       42       -2     
  Lines        1190     1191       +1     
  Branches      164      140      -24     
==========================================
+ Hits         1047     1051       +4     
- Misses         97      100       +3     
+ Partials       46       40       -6     
Flag Coverage Δ
#unittests 88.24% <93.63%> (+0.26%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
.../ImageSharp.Web/Middleware/ImageSharpMiddleware.cs 87.40% <92.90%> (+8.13%) ⬆️
.../ImageSharp.Web/Caching/PhysicalFileSystemCache.cs 96.77% <100.00%> (+2.48%) ⬆️
...arp.Web/Caching/PhysicalFileSystemCacheResolver.cs 100.00% <100.00%> (ø)
src/ImageSharp.Web/ImageCacheMetadata.cs 83.82% <100.00%> (+0.24%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update aa1b192...668b1fc. Read the comment docs.

@JimBobSquarePants
Copy link
Member Author

You can actually see the negative effect in the Doorman coverage here as all our small number of requests are entering the read lock before the write lock is established.

https://codecov.io/gh/SixLabors/ImageSharp.Web/compare/aa1b1928c675cf02e86c3a6284b9d6b22d1880e5...620f818383811c4a49075d7317e1fddd1bda3089/src/src/ImageSharp.Web/Caching/Doorman.cs

It seems to me we need a single lock which is upgradeable. Any and all advice is most welcome.

@deanmarcussen
Copy link
Collaborator

Have you considered trying a lock free (ish because of the Lazy) implementation?

Something like
private readonly ConcurrentDictionary<string, Lazy<Task>> Workers = new ConcurrentDictionary<string, Lazy<Task>>(StringComparer.OrdinalIgnoreCase);

@JimBobSquarePants
Copy link
Member Author

@deanmarcussen I don't quite follow I'm afraid.

@deanmarcussen
Copy link
Collaborator

@deanmarcussen I don't quite follow I'm afraid.

Sorry not well explained - late on a Friday.

Easier to show than explain. Here's a working branch for ideas, not stress tested, or perf tested.

The idea is to maintain a thread safe dictionary of lazy tasks, rather than use a lock.

Ignore if you don't like

https://github.com/ThisNetWorks/ImageSharp.Web/tree/lock-free

@JimBobSquarePants
Copy link
Member Author

JimBobSquarePants commented Sep 12, 2020

@deanmarcussen More me than you mate, I'm not firing on all cylinders.

Thanks for having a look! I'd love to go lock free actually!

Might be best to base any changes on this branch though as I've changed the cache lookup method to speed it up. Though if I’m reading your code right your changes would negate the need for an lru cache.

@deanmarcussen
Copy link
Collaborator

Might be best to base any changes on this branch though as I've changed the cache lookup method to speed it up. Though if I’m reading your code right your changes would negate the need for an lru cache.

@JimBobSquarePants Trying to get time to look at this properly today.

From pulling this branch it looks like the LRU cache is using similar concepts to what I was proposing for the AsyncLock, but to achieve a different result.

So to clarify, is the LRU is to provide cached access to source metadata and source resolver - not the actual stream but just the resolver?

I think you would still want threadsafe locklike behaviour read around the cache, and write safe when not in the cache?

I'll try and update based of this branch later today

@JimBobSquarePants
Copy link
Member Author

Thanks @deanmarcussen

So to clarify, is the LRU is to provide cached access to source metadata and source resolver - not the actual stream but just the resolver?

They cache both resolvers and the metadata returned. Reading values from the cache is actually fairly IO intensive and by caching the result in an expiring LRU cache I can cut that all out.

I think you would still want threadsafe locklike behaviour read around the cache, and write safe when not in the cache?

Yeah, I reviewed and still think we need that. The current read/write lock is allowing for too many queued writes though.

Basically if I have 10 requests sent at the same time the first 7 end up processing the file and the last 3 (plus any future requests) use the cached result. This is in extreme (artificial) circumstances of course so I might be worrying about nothing but I still don't like it.

@deanmarcussen
Copy link
Collaborator

deanmarcussen commented Sep 15, 2020

New branch @JimBobSquarePants https://github.com/ThisNetWorks/ImageSharp.Web/tree/dm/lock-free-cache

What's the best way to test this? Draft PR or will you run it?

I see options for improving it - the Lazy is useful on the Writer, to prevent multiple writes, but may not be necesary on the Reader.

But worth seeing if it has anything to gain over your existing AsyncKeyLock first before looking at further optimization.

Basically if I have 10 requests sent at the same time the first 7 end up processing the file and the last 3

I'm not sure where this race is, and if what I've done achieves anything about it
Because there's still a race/gap between a ReadWorker, and creating a WriteWorker, which might be what you're seeing?

But with this, we should see only one write occuring (at least I hope so.)

They could potentially merge into one set of Workers, which produce a func for reading (then cached), and perform the write if not read?

Still getting my head around the LRU cache ;)

@JimBobSquarePants
Copy link
Member Author

Thanks @deanmarcussen I'll have a look tonight.

This test is useful for tracking reads and writes. I just debug it and add some break points. Comment out the AzureTestImage theory data if you want to just test the physical provider.

https://github.com/ThisNetWorks/ImageSharp.Web/blob/0efc559c63f2a9f8811f689482cfbc44eb40797b/tests/ImageSharp.Web.Tests/Processing/PhysicalFileSystemCacheServerTests.cs#L108

@sebastienros
Copy link
Contributor

If you need me to run a benchmark let me know which branch

@JimBobSquarePants
Copy link
Member Author

@deanmarcussen Just pulled down your branch and all tests are passing. In addition I also saw the correct number of writes whic was awesome.

Cranked up the numbers and everything still looks good. Curious to see how it profiles.

@deanmarcussen
Copy link
Collaborator

Whew! And cool.

I need to tweak something tomorrow I think. The write func should probably check the read dictionary for operations and await it as well.

Let's see if we can bench it after :)

@deanmarcussen
Copy link
Collaborator

@JimBobSquarePants Pushed the tweak I wanted to.

The test you pointed out has been quietly running this morning, with some tweaks to process 10+ images of a larger size (10MB to a width of 200px), and either a new image (cache busted) or the same image from the cache (every odd number task).

I can't fault it.

In terms of performance this is not a great way to bench it, but I applied the same test run to this branch here four or five times and the test run averages around 43s.

On my branch it drops to around 15s per test run!

MacOS i7 Quad Core 24GB

@JimBobSquarePants
Copy link
Member Author

@deanmarcussen Can you open a PR targeting this branch then?

I'll merge it then @sebastienros will be able to profile this branch and tell us what the numbers are.

@JimBobSquarePants
Copy link
Member Author

@sebastienros This branch js/faster-cache is ready to benchmark at your leisure 🙂

@sebastienros
Copy link
Contributor

🎉

It's faster than static files, at least on a cached file. Went from 2K to 60K, with super low latency too.
There are some error, around 0.1% of the requests, but I don't know where they come from yet. Not more or less than before.

@JimBobSquarePants
Copy link
Member Author

It's faster than static files, at least on a cached file. Went from 2K to 60K, with super low latency too.

Holy Moly! @deanmarcussen I owe you any beverage of your choice!

@sebastienros 0.1% errors is fine by me just now. We can diagnose at another time.

@deanmarcussen
Copy link
Collaborator

It's faster than static files, at least on a cached file. Went from 2K to 60K, with super low latency too.

Holy Moly! @deanmarcussen I owe you any beverage of your choice!

Mind blown - don't know what I was expecting, but nothing like that. Awesome :)

@JimBobSquarePants JimBobSquarePants marked this pull request as ready for review September 17, 2020 15:52
@JimBobSquarePants JimBobSquarePants merged commit f9d781a into master Sep 17, 2020
@JimBobSquarePants JimBobSquarePants deleted the js/faster-cache branch September 17, 2020 15:53
@JimBobSquarePants JimBobSquarePants changed the title WIP Faster Cache Faster Cache Sep 17, 2020
@sebastienros
Copy link
Contributor

Let us know when this is on nuget so we can test it in OC.

@JimBobSquarePants
Copy link
Member Author

It's on MyGet just now. I'll push to NuGet in the next day or two once I've double checked a few other things.

https://www.myget.org/feed/sixlabors/package/nuget/SixLabors.ImageSharp.Web/1.0.0-rc.3.16

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants