Changes the relationship between OMEZarrLoader and the cache: rather than caching whatever arbitrary subsets of the volume are requested, the loader caches the actual array chunks loaded by the underlying zarr library. This has some significant benefits:
All data loaded from the server is cached, not just the data placed in the volume. For example, the chunks in our test volumes commonly cross four z-slices. Currently in z-slice mode, we cache only the slice that is displayed, and have to re-request data when the user changes slices. With this change, we get those three extra slices in the cache for free.
The cache may provide the same data for multiple requests with different regions. Even if all the data required for a request has been previously loaded, if that request's region doesn't exactly match a previously cached request's region the current behavior is to reload the requested data and re-cache it with a different region. With this change, the same data can be applied to multiple distinct but intersecting regions, and the loader may even source data from a combination of cache and remote requests.
I did this by writing a new Store, which is zarr.js's tiny abstraction around anything that can read and write data to a string path (the user's local storage, a server, etc.). ChunkCachingStore wraps any other store and adds cache queries and inserts. This approach is great in that it allows us to call regular zarr load methods without having to worry about the cache at all, but it also has some important implications:
Because Stores are so low-level, data is now cached before decompression and before possible conversion of the buffer to Uint8Array. The performance overhead of doing these extra steps on cache hits seems to be minimal, though not entirely negligible. At least we save the user some memory though.
I had to make some changes to how VolumeCache works. Most notably, because it's impractical and useless to convert from chunk coordinates to pixel subregions all the time, the cache is now indexed by chunks. The cache also now works with raw ArrayBuffers (because entries haven't necessarily been type-converted) and does not sanity check buffer length against region extent (because entries may be compressed and don't have a consistent size).
With accesses to a single shared cache now integrated into all our interactions with the zarr library, it becomes impractical to hand off each channel load to its own web worker. (It's certainly not impossible though, if we want to keep it.) All the testing I did indicates the performance difference between the old and new loaders is basically a toss-up, but this is still a bummer. (It seemed like the old loader was delayed by a long compile step for all its workers? If we put workers back, it might be worth the effort to do it with a sort of thread pool approach utilizing a fixed number of persistent workers.)
One more thing: the loader now does its various metadata fetches on construction, rather than with every call to a loading method. This ought to save some unnecessary requests, but it did require replacing the constructor with an async static method.
Changes the relationship between
OMEZarrLoader
and the cache: rather than caching whatever arbitrary subsets of the volume are requested, the loader caches the actual array chunks loaded by the underlying zarr library. This has some significant benefits:I did this by writing a new
Store
, which is zarr.js's tiny abstraction around anything that can read and write data to astring
path (the user's local storage, a server, etc.).ChunkCachingStore
wraps any other store and adds cache queries and inserts. This approach is great in that it allows us to call regular zarr load methods without having to worry about the cache at all, but it also has some important implications:Store
s are so low-level, data is now cached before decompression and before possible conversion of the buffer toUint8Array
. The performance overhead of doing these extra steps on cache hits seems to be minimal, though not entirely negligible. At least we save the user some memory though.VolumeCache
works. Most notably, because it's impractical and useless to convert from chunk coordinates to pixel subregions all the time, the cache is now indexed by chunks. The cache also now works with rawArrayBuffer
s (because entries haven't necessarily been type-converted) and does not sanity check buffer length against region extent (because entries may be compressed and don't have a consistent size).One more thing: the loader now does its various metadata fetches on construction, rather than with every call to a loading method. This ought to save some unnecessary requests, but it did require replacing the constructor with an async static method.