allen-cell-animated / volume-viewer

https://allen-cell-animated.github.io/volume-viewer/
Other
90 stars 7 forks source link

Feature/loader worker #177

Closed frasercl closed 6 months ago

frasercl commented 6 months ago

Adds the ability to offload most volume loaders to a separate persistent worker, to avoid blocking updates and rendering on the "main thread."

This PR is a child of #176 and includes all its commits. Review that one first! I'll keep this one as a draft until that one merges.

How it works:

Currently, the easiest and most generic way to create a loader is to call createVolumeLoader, which accepts a path and an options object and returns a loader of the appropriate type. The procedure to create a loader on a separate worker is quite similar, but includes a couple extra steps:

  1. Create a new LoadWorker. This accepts arguments for initializing a cache and queue. Internally, LoadWorker spins up a new webworker and tells it to get a cache and queue going.
  2. Before using the LoadWorker, await LoadWorker.onOpen(), which returns a promise that resolves when the above initialization step completes and the worker reports that it's ready.
  3. Call LoadWorker.createLoader. This method has almost identical arguments as createVolumeLoader (it just doesn't accept a cache and queue, since loaders on the worker will use the worker's shared cache and queue instances) and returns a WorkerLoader which acts as a handle to the loader on the worker and can go wherever a normal IVolumeLoader can.
  4. More loaders can be created via this method, but currently only one loader can run on the worker at a time, so any previous WorkerLoader will be invalidated and begin throwing errors when any of its methods are called. (There's no technical reason we can't run multiple loaders on the same worker, in fact we'd get some benefits by automatically sharing cache and queue, we're just not yet likely enough to use that functionality in practice to justify the effort of implementing it.)

Worker implementation keyfiles

Changes to existing loaders

I had to make some changes to existing volume loaders to support working across the boundary between the main thread and the worker. Most significantly, most loaders are now implemented around a new abstract class called ThreadableVolumeLoader.

The most important methods of IVolumeLoader (the shared interface which all volume loaders implement) want to work directly with Volumes: createVolume builds and returns a Volume, and loadVolumeData accepts a Volume as an argument and modifies it directly. But transferring full Volumes to/from a worker would be impractical for a bunch of reasons. ThreadableVolumeLoader is an abstract class which introduces equivalent methods - createImageInfo and loadRawChannelData, respectively - that work with the ImageInfo and LoadSpec objects that describe a Volume. LoadWorker calls these lower-level methods and transfers the resulting changes to ImageInfo and/or LoadSpec back to the main thread, where direct changes can be made to the relevant Volume. ThreadableVolumeLoader also provides default implementations of createVolume and loadVolumeData around these new abstract methods, allowing a loader on the main thread to still use the old API without duplicated code. This has the added benefit of reducing a bit of boilerplate between loaders, since it turns out that the manipulations they were doing to Volume were all basically the same.

Additionally, JsonImageInfoLoader loaded images by creating an off-screen tag and waiting for it to load. But since DOM nodes can't be created on a worker, I rewrote this section of the code to use the fetch API.

ShrimpCryptid commented 6 months ago

Overall looks really good, I like how clean the change to the ThreadableLoader was! I'm going to wait until all the comments are resolved before approving. 👌