From ed81992800c80b9d2ec7d355418a327955a4bc9c Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 13 Dec 2023 11:23:19 -0800 Subject: [PATCH 01/35] make `IVolumeLoader` API easier to work with across threads --- src/loaders/IVolumeLoader.ts | 44 ++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/loaders/IVolumeLoader.ts b/src/loaders/IVolumeLoader.ts index 48b3552c..fe6b1ee5 100644 --- a/src/loaders/IVolumeLoader.ts +++ b/src/loaders/IVolumeLoader.ts @@ -1,6 +1,7 @@ import { Box3, Vector3 } from "three"; -import Volume from "../Volume"; +import Volume, { ImageInfo } from "../Volume"; +import { buildDefaultMetadata } from "./VolumeLoaderUtils"; export class LoadSpec { time = 0; @@ -34,15 +35,25 @@ export class VolumeDims { */ export type PerChannelCallback = (volume: Volume, channelIndex: number) => void; +export type RawChannelDataCallback = (ch: number, data: Uint8Array, atlased: boolean, w?: number, h?: number) => void; + /** * Loads volume data from a source specified by a `LoadSpec`. * * Loaders may keep state for reuse between volume creation and volume loading, and should be kept alive until volume * loading is complete. (See `createVolume`) */ -export interface IVolumeLoader { +export abstract class IVolumeLoader { /** Use VolumeDims to further refine a `LoadSpec` for use in `createVolume` */ - loadDims(loadSpec: LoadSpec): Promise; + abstract loadDims(loadSpec: LoadSpec): Promise; + + abstract createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]>; + + abstract loadRawChannelData( + imageInfo: ImageInfo, + loadSpec: LoadSpec, + onData: RawChannelDataCallback + ): Promise; /** * Create an empty `Volume` from a `LoadSpec`, which must be passed to `loadVolumeData` to begin loading. @@ -52,7 +63,13 @@ export interface IVolumeLoader { * information about that source. Once this method has been called, every subsequent call to it or * `loadVolumeData` should reference the same source. */ - createVolume(loadSpec: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise; + async createVolume(loadSpec: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { + const [imageInfo, adjustedSpec] = await this.createImageInfo(loadSpec); + const vol = new Volume(imageInfo, adjustedSpec, this); + vol.channelLoadCallback = onChannelLoaded; + vol.imageMetadata = buildDefaultMetadata(imageInfo); + return vol; + } /** * Begin loading a volume's data, as specified in its `LoadSpec`. @@ -63,5 +80,22 @@ export interface IVolumeLoader { // TODO this is not cancellable in the sense that any async requests initiated here are not stored // in a way that they can be interrupted. // TODO explicitly passing a `LoadSpec` is now rarely useful. Remove? - loadVolumeData(volume: Volume, loadSpec?: LoadSpec, onChannelLoaded?: PerChannelCallback): void; + async loadVolumeData(volume: Volume, loadSpec?: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { + const onChannelData: RawChannelDataCallback = (channelIndex, data, atlased, w, h) => { + if (atlased) { + volume.setChannelDataFromAtlas(channelIndex, data, w!, h!); + } else { + volume.setChannelDataFromVolume(channelIndex, data); + } + onChannelLoaded?.(volume, channelIndex); + }; + + const spec = loadSpec || volume.loadSpec; + const adjustedImageInfo = await this.loadRawChannelData(volume.imageInfo, spec, onChannelData); + + if (adjustedImageInfo) { + volume.imageInfo = adjustedImageInfo; + volume.updateDimensions(); + } + } } From 26c7eeebc045473ed33f60edb7f6d298114d3005 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 13 Dec 2023 14:19:50 -0800 Subject: [PATCH 02/35] adapt loaders to use transferrable API --- src/loaders/IVolumeLoader.ts | 13 +++++---- src/loaders/JsonImageInfoLoader.ts | 46 ++++++++++++++++-------------- src/loaders/OmeZarrLoader.ts | 45 +++++++++++++++-------------- src/loaders/OpenCellLoader.ts | 25 ++++++++-------- src/loaders/TiffLoader.ts | 28 +++++++++--------- 5 files changed, 82 insertions(+), 75 deletions(-) diff --git a/src/loaders/IVolumeLoader.ts b/src/loaders/IVolumeLoader.ts index fe6b1ee5..865b8808 100644 --- a/src/loaders/IVolumeLoader.ts +++ b/src/loaders/IVolumeLoader.ts @@ -35,7 +35,7 @@ export class VolumeDims { */ export type PerChannelCallback = (volume: Volume, channelIndex: number) => void; -export type RawChannelDataCallback = (ch: number, data: Uint8Array, atlased: boolean, w?: number, h?: number) => void; +export type RawChannelDataCallback = (ch: number, data: Uint8Array, atlasDims?: [number, number]) => void; /** * Loads volume data from a source specified by a `LoadSpec`. @@ -53,7 +53,7 @@ export abstract class IVolumeLoader { imageInfo: ImageInfo, loadSpec: LoadSpec, onData: RawChannelDataCallback - ): Promise; + ): Promise<[ImageInfo | undefined, LoadSpec | undefined]>; /** * Create an empty `Volume` from a `LoadSpec`, which must be passed to `loadVolumeData` to begin loading. @@ -81,9 +81,9 @@ export abstract class IVolumeLoader { // in a way that they can be interrupted. // TODO explicitly passing a `LoadSpec` is now rarely useful. Remove? async loadVolumeData(volume: Volume, loadSpec?: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { - const onChannelData: RawChannelDataCallback = (channelIndex, data, atlased, w, h) => { - if (atlased) { - volume.setChannelDataFromAtlas(channelIndex, data, w!, h!); + const onChannelData: RawChannelDataCallback = (channelIndex, data, atlasDims) => { + if (atlasDims) { + volume.setChannelDataFromAtlas(channelIndex, data, atlasDims[0], atlasDims[1]); } else { volume.setChannelDataFromVolume(channelIndex, data); } @@ -91,11 +91,12 @@ export abstract class IVolumeLoader { }; const spec = loadSpec || volume.loadSpec; - const adjustedImageInfo = await this.loadRawChannelData(volume.imageInfo, spec, onChannelData); + const [adjustedImageInfo, adjustedLoadSpec] = await this.loadRawChannelData(volume.imageInfo, spec, onChannelData); if (adjustedImageInfo) { volume.imageInfo = adjustedImageInfo; volume.updateDimensions(); } + volume.loadSpec = adjustedLoadSpec || spec; } } diff --git a/src/loaders/JsonImageInfoLoader.ts b/src/loaders/JsonImageInfoLoader.ts index b0b96d36..d28bed2f 100644 --- a/src/loaders/JsonImageInfoLoader.ts +++ b/src/loaders/JsonImageInfoLoader.ts @@ -1,7 +1,6 @@ import { Box3, Vector2, Vector3 } from "three"; -import { IVolumeLoader, LoadSpec, PerChannelCallback, VolumeDims } from "./IVolumeLoader"; -import { buildDefaultMetadata } from "./VolumeLoaderUtils"; +import { IVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; import Volume, { ImageInfo } from "../Volume"; import VolumeCache from "../VolumeCache"; @@ -93,13 +92,15 @@ const convertImageInfo = (json: JsonImageInfo): ImageInfo => ({ userData: json.userData, }); -class JsonImageInfoLoader implements IVolumeLoader { +class JsonImageInfoLoader extends IVolumeLoader { urls: string[]; jsonInfo: (JsonImageInfo | undefined)[]; cache?: VolumeCache; constructor(urls: string | string[], cache?: VolumeCache) { + super(); + if (Array.isArray(urls)) { this.urls = urls; } else { @@ -136,27 +137,25 @@ class JsonImageInfoLoader implements IVolumeLoader { return [d]; } - async createVolume(loadSpec: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { + async createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]> { const jsonInfo = await this.getJsonImageInfo(loadSpec.time); - const imageInfo = convertImageInfo(jsonInfo); - - const vol = new Volume(imageInfo, loadSpec, this); - vol.channelLoadCallback = onChannelLoaded; - vol.imageMetadata = buildDefaultMetadata(imageInfo); - return vol; + return [convertImageInfo(jsonInfo), loadSpec]; } - async loadVolumeData(vol: Volume, explicitLoadSpec?: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { + async loadRawChannelData( + imageInfo: ImageInfo, + loadSpec: LoadSpec, + onData: RawChannelDataCallback + ): Promise<[undefined, LoadSpec | undefined]> { // if you need to adjust image paths prior to download, // now is the time to do it. // Try to figure out the urlPrefix from the LoadSpec. // For this format we assume the image data is in the same directory as the json file. - const loadSpec = explicitLoadSpec || vol.loadSpec; const jsonInfo = await this.getJsonImageInfo(loadSpec.time); let images = jsonInfo?.images; if (!images) { - return; + return [undefined, undefined]; } const requestedChannels = loadSpec.channels; @@ -169,7 +168,12 @@ class JsonImageInfoLoader implements IVolumeLoader { const urlPrefix = this.urls[loadSpec.time].replace(/[^/]*$/, ""); images = images.map((element) => ({ ...element, name: urlPrefix + element.name })); - vol.loadSpec = { + const w = imageInfo.atlasTileDims.x * imageInfo.volumeSize.x; + const h = imageInfo.atlasTileDims.y * imageInfo.volumeSize.y; + const wrappedOnData = (ch: number, data: Uint8Array) => onData(ch, data, [w, h]); + JsonImageInfoLoader.loadVolumeAtlasData(images, wrappedOnData, this.cache); + + const adjustedLoadSpec = { ...loadSpec, // `subregion` and `multiscaleLevel` are unused by this loader subregion: new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)), @@ -177,7 +181,7 @@ class JsonImageInfoLoader implements IVolumeLoader { // include all channels in any loaded images channels: images.flatMap(({ channels }) => channels), }; - JsonImageInfoLoader.loadVolumeAtlasData(vol, images, onChannelLoaded, this.cache); + return [undefined, adjustedLoadSpec]; } /** @@ -200,9 +204,8 @@ class JsonImageInfoLoader implements IVolumeLoader { * }], mycallback); */ static loadVolumeAtlasData( - volume: Volume, imageArray: PackedChannelsImage[], - onChannelLoaded?: PerChannelCallback, + onData: RawChannelDataCallback, cache?: VolumeCache ): PackedChannelsImageRequests { const numImages = imageArray.length; @@ -221,8 +224,7 @@ class JsonImageInfoLoader implements IVolumeLoader { const chindex = batch[j]; const cacheResult = cache?.get(`${url}/${chindex}`); if (cacheResult) { - volume.setChannelDataFromVolume(chindex, new Uint8Array(cacheResult)); - onChannelLoaded?.(volume, chindex); + onData(chindex, new Uint8Array(cacheResult)); } else { cacheHit = false; // we can stop checking because we know we are going to have to fetch the whole batch @@ -283,9 +285,9 @@ class JsonImageInfoLoader implements IVolumeLoader { for (let ch = 0; ch < Math.min(batch.length, 4); ++ch) { const chindex = batch[ch]; - volume.setChannelDataFromAtlas(chindex, channelsBits[ch], w, h); - cache?.insert(`${url}/${chindex}`, volume.channels[chindex].volumeData.buffer); - onChannelLoaded?.(volume, chindex); + cache?.insert(`${url}/${chindex}`, channelsBits[ch]); + // NOTE: the atlas dimensions passed in here are currently unused + onData(chindex, channelsBits[ch], [w, h]); } }; img.crossOrigin = "Anonymous"; diff --git a/src/loaders/OmeZarrLoader.ts b/src/loaders/OmeZarrLoader.ts index 1bf119a2..56585513 100644 --- a/src/loaders/OmeZarrLoader.ts +++ b/src/loaders/OmeZarrLoader.ts @@ -10,7 +10,7 @@ import { FetchStore } from "zarrita"; import Volume, { ImageInfo } from "../Volume"; import VolumeCache from "../VolumeCache"; import SubscribableRequestQueue from "../utils/SubscribableRequestQueue"; -import { IVolumeLoader, LoadSpec, PerChannelCallback, VolumeDims } from "./IVolumeLoader"; +import { IVolumeLoader, LoadSpec, PerChannelCallback, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; import { buildDefaultMetadata, composeSubregion, @@ -221,7 +221,7 @@ function convertChannel(channelData: zarr.TypedArray): Uint type NumericZarrArray = zarr.Array>; -class OMEZarrLoader implements IVolumeLoader { +class OMEZarrLoader extends IVolumeLoader { /** Hold one optional subscriber ID per timestep, each defined iff a batch of prefetches is waiting for that frame */ private prefetchSubscribers: (SubscriberId | undefined)[]; /** The ID of the subscriber responsible for "actual loads" (non-prefetch requests) */ @@ -240,6 +240,7 @@ class OMEZarrLoader implements IVolumeLoader { // TODO: should we be able to share `RequestQueue`s between loaders? private requestQueue: SubscribableRequestQueue ) { + super(); const ti = this.axesTCZYX[0]; const times = ti < 0 ? 1 : this.scaleLevels[0].shape[this.axesTCZYX[0]]; this.prefetchSubscribers = new Array(times).fill(undefined); @@ -375,7 +376,7 @@ class OMEZarrLoader implements IVolumeLoader { return Promise.resolve(result); } - async createVolume(loadSpec: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { + createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]> { const [t, c, z, y, x] = this.axesTCZYX; const hasT = t > -1; const hasC = c > -1; @@ -442,10 +443,7 @@ class OMEZarrLoader implements IVolumeLoader { subregion: new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)), }; - const vol = new Volume(imgdata, fullExtentLoadSpec, this); - vol.channelLoadCallback = onChannelLoaded; - vol.imageMetadata = buildDefaultMetadata(imgdata); - return vol; + return Promise.resolve([imgdata, fullExtentLoadSpec]); } private async prefetchChunk(basePath: string, coords: TCZYX, subscriber: SubscriberId): Promise { @@ -525,7 +523,11 @@ class OMEZarrLoader implements IVolumeLoader { } } - async loadVolumeData(vol: Volume, explicitLoadSpec?: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { + loadRawChannelData( + imageInfo: ImageInfo, + loadSpec: LoadSpec, + onData: RawChannelDataCallback + ): Promise<[ImageInfo, LoadSpec]> { // First, cancel any pending requests for this volume if (this.loadSubscriber !== undefined) { this.requestQueue.removeSubscriber(this.loadSubscriber, CHUNK_REQUEST_CANCEL_REASON); @@ -533,12 +535,11 @@ class OMEZarrLoader implements IVolumeLoader { const subscriber = this.requestQueue.addSubscriber(); this.loadSubscriber = subscriber; - vol.loadSpec = explicitLoadSpec ?? vol.loadSpec; const maxExtent = this.maxExtent ?? new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)); const [z, y, x] = this.axesTCZYX.slice(2); - const subregion = composeSubregion(vol.loadSpec.subregion, maxExtent); + const subregion = composeSubregion(loadSpec.subregion, maxExtent); - const levelIdx = pickLevelToLoad({ ...vol.loadSpec, subregion }, this.getLevelShapesZYX()); + const levelIdx = pickLevelToLoad({ ...loadSpec, subregion }, this.getLevelShapesZYX()); const level = this.scaleLevels[levelIdx]; const levelShape = level.shape; @@ -554,18 +555,17 @@ class OMEZarrLoader implements IVolumeLoader { new Vector3(levelShape[x], levelShape[y], z === -1 ? 1 : levelShape[z]) ); const volSizePx = volExtentPx.getSize(new Vector3()); - vol.imageInfo = { - ...vol.imageInfo, + const updatedImageInfo: ImageInfo = { + ...imageInfo, atlasTileDims, volumeSize: volSizePx, subregionSize: regionSizePx, subregionOffset: regionPx.min, multiscaleLevel: levelIdx, }; - vol.updateDimensions(); - const { numChannels } = vol.imageInfo; - const channelIndexes = vol.loadSpec.channels ?? Array.from({ length: numChannels }, (_, i) => i); + const { numChannels } = updatedImageInfo; + const channelIndexes = loadSpec.channels ?? Array.from({ length: numChannels }, (_, i) => i); // Prefetch housekeeping: we want to save keys involved in this load to prefetch later const keys: string[] = []; @@ -578,17 +578,18 @@ class OMEZarrLoader implements IVolumeLoader { const channelPromises = channelIndexes.map(async (ch) => { // Build slice spec const { min, max } = regionPx; - const unorderedSpec = [vol.loadSpec.time, ch, slice(min.z, max.z), slice(min.y, max.y), slice(min.x, max.x)]; + const unorderedSpec = [loadSpec.time, ch, slice(min.z, max.z), slice(min.y, max.y), slice(min.x, max.x)]; const sliceSpec = this.orderByDimension(unorderedSpec as TCZYX); try { + console.log(level, sliceSpec, subscriber); const result = await zarrGet(level, sliceSpec, { opts: { subscriber, reportKey } }); const u8 = convertChannel(result.data); - vol.setChannelDataFromVolume(ch, u8); - onChannelLoaded?.(vol, ch); + onData(ch, u8); } catch (e) { // TODO: verify that cancelling requests in progress doesn't leak memory if (e !== CHUNK_REQUEST_CANCEL_REASON) { + console.log(e); throw e; } } @@ -597,8 +598,10 @@ class OMEZarrLoader implements IVolumeLoader { console.log(keys); setTimeout(() => this.beginPrefetch(keys, level), 0); - await Promise.all(channelPromises); - this.requestQueue.removeSubscriber(subscriber); + Promise.all(channelPromises).then(() => + this.requestQueue.removeSubscriber(subscriber, CHUNK_REQUEST_CANCEL_REASON) + ); + return Promise.resolve([updatedImageInfo, loadSpec]); } } diff --git a/src/loaders/OpenCellLoader.ts b/src/loaders/OpenCellLoader.ts index 6f974acd..f9b49522 100644 --- a/src/loaders/OpenCellLoader.ts +++ b/src/loaders/OpenCellLoader.ts @@ -1,12 +1,10 @@ import { Vector2, Vector3 } from "three"; -import { IVolumeLoader, LoadSpec, PerChannelCallback, VolumeDims } from "./IVolumeLoader"; -import { buildDefaultMetadata } from "./VolumeLoaderUtils"; +import { IVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; import { ImageInfo } from "../Volume"; -import Volume from "../Volume"; import { JsonImageInfoLoader } from "./JsonImageInfoLoader"; -class OpenCellLoader implements IVolumeLoader { +class OpenCellLoader extends IVolumeLoader { async loadDims(_: LoadSpec): Promise { const d = new VolumeDims(); d.shape = [1, 2, 27, 600, 600]; @@ -16,7 +14,7 @@ class OpenCellLoader implements IVolumeLoader { return [d]; } - async createVolume(_loadSpec: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { + async createImageInfo(_loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]> { const numChannels = 2; // we know these are standardized to 600x600, two channels, one channel per jpg. @@ -49,15 +47,15 @@ class OpenCellLoader implements IVolumeLoader { }, }; - // got some data, now let's construct the volume. // This loader uses no fields from `LoadSpec`. Initialize volume with defaults. - const vol = new Volume(imgdata, new LoadSpec(), this); - vol.channelLoadCallback = onChannelLoaded; - vol.imageMetadata = buildDefaultMetadata(imgdata); - return vol; + return [imgdata, new LoadSpec()]; } - loadVolumeData(vol: Volume, _loadSpec?: LoadSpec, onChannelLoaded?: PerChannelCallback): void { + loadRawChannelData( + imageInfo: ImageInfo, + _loadSpec: LoadSpec, + onData: RawChannelDataCallback + ): Promise<[undefined, undefined]> { // HQTILE or LQTILE // make a json metadata dict for the two channels: const urls = [ @@ -71,7 +69,10 @@ class OpenCellLoader implements IVolumeLoader { }, ]; - JsonImageInfoLoader.loadVolumeAtlasData(vol, urls, onChannelLoaded); + const w = imageInfo.atlasTileDims.x * imageInfo.volumeSize.x; + const h = imageInfo.atlasTileDims.y * imageInfo.volumeSize.y; + JsonImageInfoLoader.loadVolumeAtlasData(urls, (ch, data) => onData(ch, data, [w, h])); + return Promise.resolve([undefined, undefined]); } } diff --git a/src/loaders/TiffLoader.ts b/src/loaders/TiffLoader.ts index a7d099e3..5e158540 100644 --- a/src/loaders/TiffLoader.ts +++ b/src/loaders/TiffLoader.ts @@ -1,10 +1,9 @@ import { fromUrl } from "geotiff"; import { Vector3 } from "three"; -import { IVolumeLoader, LoadSpec, PerChannelCallback, VolumeDims } from "./IVolumeLoader"; -import { buildDefaultMetadata, computePackedAtlasDims } from "./VolumeLoaderUtils"; +import { IVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; +import { computePackedAtlasDims } from "./VolumeLoaderUtils"; import { ImageInfo } from "../Volume"; -import Volume from "../Volume"; function prepareXML(xml: string): string { // trim trailing unicode zeros? @@ -62,11 +61,12 @@ function getOMEDims(imageEl: Element): OMEDims { const getBytesPerSample = (type: string): number => (type === "uint8" ? 1 : type === "uint16" ? 2 : 4); -class TiffLoader implements IVolumeLoader { +class TiffLoader extends IVolumeLoader { url: string; dims?: OMEDims; constructor(url: string) { + super(); this.url = url; } @@ -98,7 +98,7 @@ class TiffLoader implements IVolumeLoader { return [d]; } - async createVolume(_loadSpec: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { + async createImageInfo(_loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]> { const dims = await this.loadOmeDims(); // compare with sizex, sizey //const width = image.getWidth(); @@ -142,16 +142,15 @@ class TiffLoader implements IVolumeLoader { }; // This loader uses no fields from `LoadSpec`. Initialize volume with defaults. - const vol = new Volume(imgdata, new LoadSpec(), this); - vol.channelLoadCallback = onChannelLoaded; - vol.imageMetadata = buildDefaultMetadata(imgdata); - - return vol; + return [imgdata, new LoadSpec()]; } - async loadVolumeData(vol: Volume, _loadSpec?: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { + async loadRawChannelData( + imageInfo: ImageInfo, + _loadSpec: LoadSpec, + onData: RawChannelDataCallback + ): Promise<[undefined, undefined]> { const dims = await this.loadOmeDims(); - const imageInfo = vol.imageInfo; // do each channel on a worker? for (let channel = 0; channel < imageInfo.numChannels; ++channel) { @@ -171,9 +170,8 @@ class TiffLoader implements IVolumeLoader { worker.onmessage = (e) => { const u8 = e.data.data; const channel = e.data.channel; - vol.setChannelDataFromVolume(channel, u8); + onData(channel, u8); // make up a unique name? or have caller pass this in? - onChannelLoaded?.(vol, channel); worker.terminate(); }; worker.onerror = (e) => { @@ -181,6 +179,8 @@ class TiffLoader implements IVolumeLoader { }; worker.postMessage(params); } + + return [undefined, undefined]; } } From 21d4f0c733218bf21797b374a28d431f055fc816 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 09:53:39 -0800 Subject: [PATCH 03/35] first crack at loader worker handle --- src/workers/LoadWorkerHandle.ts | 166 ++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 src/workers/LoadWorkerHandle.ts diff --git a/src/workers/LoadWorkerHandle.ts b/src/workers/LoadWorkerHandle.ts new file mode 100644 index 00000000..29f9cf13 --- /dev/null +++ b/src/workers/LoadWorkerHandle.ts @@ -0,0 +1,166 @@ +import { ImageInfo } from "../Volume"; +import { CreateLoaderOptions } from "../loaders"; +import { IVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "../loaders/IVolumeLoader"; +import { + WorkerMsgType, + WorkerRequest, + WorkerRequestPayload, + WorkerResponse, + WorkerResponsePayload, + ChannelLoadEvent, +} from "./types"; + +type StoredPromise = { + resolve: (value: WorkerResponsePayload) => void; + reject: (reason?: unknown) => void; +}; + +/** + * A handle that holds the worker and lets us interact with it through async calls and events rather than messages. + * This is separate from `LoadWorker` so that `sendMessage` and `onChannelData` can be shared with `WorkerLoader`s + * without leaking the API outside this file. + */ +class InternalLoadWorkerHandle { + private worker: Worker; + private pendingRequests: (StoredPromise | undefined)[] = []; + + public onChannelData: ((e: ChannelLoadEvent) => void) | undefined = undefined; + + constructor() { + this.worker = new Worker(new URL("./VolumeLoadWorker", import.meta.url)); + this.worker.onmessage = this.receiveMessage.bind(this); + this.worker.onerror = this.receiveError.bind(this); + } + + /** Given a handle for settling a promise when a response is received from the worker, store it and return its ID */ + private registerMessagePromise(prom: StoredPromise): number { + for (const [i, pendingPromise] of this.pendingRequests.entries()) { + if (pendingPromise === undefined) { + this.pendingRequests[i] = prom; + return i; + } + } + + return this.pendingRequests.push(prom) - 1; + } + + sendMessage(type: T, payload: WorkerRequestPayload): Promise> { + let msgId = -1; + const promise = new Promise>((resolve, reject) => { + msgId = this.registerMessagePromise({ resolve, reject } as StoredPromise); + }); + + const msg: WorkerRequest = { msgId, type, payload, isEvent: false }; + this.worker.postMessage(msg); + + return promise; + } + + private receiveMessage({ data }: MessageEvent | ChannelLoadEvent>): void { + if (data.isEvent) { + this.onChannelData?.(data); + } else { + const prom = this.pendingRequests[data.msgId]; + if (prom === undefined) { + throw new Error(`Received response for unknown message ID ${data.msgId}`); + } + prom.resolve(data.payload); + this.pendingRequests[data.msgId] = undefined; + } + } + + private receiveError(e: ErrorEvent): void { + // TODO propagate errors through promises + // if (!e.error) + console.log(e); + } +} + +class LoadWorker { + private workerHandle: InternalLoadWorkerHandle; + private openPromise: Promise; + + private activeLoader: WorkerLoader | undefined = undefined; + private activeLoaderId = -1; + + constructor(maxCacheSize?: number) { + this.workerHandle = new InternalLoadWorkerHandle(); + this.openPromise = this.workerHandle.sendMessage(WorkerMsgType.INIT, { maxCacheSize }); + } + + onOpen(): Promise { + return this.openPromise; + } + + async createLoader(path: string | string[], options?: CreateLoaderOptions): Promise { + const success = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_LOADER, { path, options }); + if (!success) { + throw new Error("Failed to create loader"); + } + + this.activeLoader?.close(); + this.activeLoaderId += 1; + this.activeLoader = new WorkerLoader(this.activeLoaderId, this.workerHandle); + return this.activeLoader; + } +} + +class WorkerLoader extends IVolumeLoader { + private isActive = true; + + private currentLoadId = -1; + private currentLoadCallback: RawChannelDataCallback | undefined = undefined; + + constructor(private loaderId: number, private workerHandle: InternalLoadWorkerHandle) { + super(); + workerHandle.onChannelData = this.onChannelData.bind(this); + } + + private checkIsActive(): void { + if (!this.isActive) { + throw new Error("Tried to use a closed loader"); + } + } + + close(): void { + this.isActive = false; + } + + loadDims(loadSpec: LoadSpec): Promise { + this.checkIsActive(); + return this.workerHandle.sendMessage(WorkerMsgType.LOAD_DIMS, loadSpec); + } + + createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]> { + this.checkIsActive(); + return this.workerHandle.sendMessage(WorkerMsgType.CREATE_VOLUME, loadSpec); + } + + loadRawChannelData( + imageInfo: ImageInfo, + loadSpec: LoadSpec, + onData: RawChannelDataCallback + ): Promise<[ImageInfo | undefined, LoadSpec | undefined]> { + this.checkIsActive(); + + this.currentLoadCallback = onData; + this.currentLoadId += 1; + return this.workerHandle.sendMessage(WorkerMsgType.LOAD_VOLUME_DATA, { + imageInfo, + loadSpec, + loaderId: this.loaderId, + loadId: this.currentLoadId, + }); + } + + onChannelData(e: ChannelLoadEvent): void { + if (e.loaderId !== this.loaderId || e.loadId !== this.currentLoadId) { + return; + } + + this.currentLoadCallback?.(e.channelIndex, e.data, e.atlasDims); + } +} + +export default LoadWorker; +export type { WorkerLoader }; From f83fe9b5c1ebbd7b1f8b2aae4bbe07508ba26ec4 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 09:57:37 -0800 Subject: [PATCH 04/35] start on loader worker --- src/workers/VolumeLoadWorker.ts | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/workers/VolumeLoadWorker.ts diff --git a/src/workers/VolumeLoadWorker.ts b/src/workers/VolumeLoadWorker.ts new file mode 100644 index 00000000..5373921c --- /dev/null +++ b/src/workers/VolumeLoadWorker.ts @@ -0,0 +1,64 @@ +import VolumeCache from "../VolumeCache"; +import { createVolumeLoader } from "../loaders"; +import { IVolumeLoader } from "../loaders/IVolumeLoader"; +import { WorkerMsgType, WorkerRequest, WorkerRequestPayload, WorkerResponsePayload } from "./types"; + +let cache: VolumeCache | undefined = undefined; +let loader: IVolumeLoader | undefined = undefined; + +type MessageHandlersType = { + [T in WorkerMsgType]: (payload: WorkerRequestPayload) => Promise>; +}; + +const messageHandlers: MessageHandlersType = { + [WorkerMsgType.INIT]: ({ maxCacheSize }) => { + cache = new VolumeCache(maxCacheSize); + return Promise.resolve(); + }, + + [WorkerMsgType.CREATE_LOADER]: async ({ path, options }) => { + loader = await createVolumeLoader(path, { ...options, cache }); + return loader !== undefined; + }, + + [WorkerMsgType.CREATE_VOLUME]: async (loadSpec) => { + if (loader === undefined) { + throw new Error("No loader created"); + } + const volume = await loader.createVolume(loadSpec); + return [volume.imageInfo, volume.loadSpec]; + }, + + [WorkerMsgType.LOAD_DIMS]: async (loadSpec) => { + if (loader === undefined) { + throw new Error("No loader created"); + } + return await loader.loadDims(loadSpec); + }, + + [WorkerMsgType.LOAD_VOLUME_DATA]: async ({ imageInfo, loadSpec, loaderId, loadId }) => { + if (loader === undefined) { + throw new Error("No loader created"); + } + if (loaderId !== 0) { + throw new Error("Only one loader is supported"); + } + const [updatedImageInfo, updatedLoadSpec] = await loader.loadRawChannelData( + imageInfo, + loadSpec, + (data, channelIndex, atlasDims) => { + self.postMessage({ + isEvent: true, + loaderId, + loadId, + channelIndex, + data, + atlasDims, + }); + } + ); + return [updatedImageInfo, updatedLoadSpec]; + }, +}; + +self.onmessage = ({ data }: MessageEvent>) => {}; From 8a96f12c5eab3b6f6a6fb0c3abd4166b1ad62261 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 09:59:21 -0800 Subject: [PATCH 05/35] loader worker message types --- src/workers/types.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/workers/types.ts diff --git a/src/workers/types.ts b/src/workers/types.ts new file mode 100644 index 00000000..092f7d1c --- /dev/null +++ b/src/workers/types.ts @@ -0,0 +1,56 @@ +import { ImageInfo } from "../Volume"; +import { CreateLoaderOptions } from "../loaders"; +import { LoadSpec, VolumeDims } from "../loaders/IVolumeLoader"; + +export enum WorkerMsgType { + INIT, + CREATE_LOADER, + CREATE_VOLUME, + LOAD_DIMS, + LOAD_VOLUME_DATA, +} + +type WorkerMsgBase = { + isEvent: false; + msgId: number; + type: T; + payload: P; +}; + +export type WorkerRequestPayload = { + [WorkerMsgType.INIT]: { + maxCacheSize?: number; + }; + [WorkerMsgType.CREATE_LOADER]: { + path: string | string[]; + options?: CreateLoaderOptions; + }; + [WorkerMsgType.CREATE_VOLUME]: LoadSpec; + [WorkerMsgType.LOAD_DIMS]: LoadSpec; + [WorkerMsgType.LOAD_VOLUME_DATA]: { + imageInfo: ImageInfo; + loadSpec: LoadSpec; + loaderId: number; + loadId: number; + }; +}[T]; + +export type WorkerResponsePayload = { + [WorkerMsgType.INIT]: void; + [WorkerMsgType.CREATE_LOADER]: boolean; + [WorkerMsgType.CREATE_VOLUME]: [ImageInfo, LoadSpec]; + [WorkerMsgType.LOAD_DIMS]: VolumeDims[]; + [WorkerMsgType.LOAD_VOLUME_DATA]: [ImageInfo | undefined, LoadSpec | undefined]; +}[T]; + +export type WorkerRequest = WorkerMsgBase>; +export type WorkerResponse = WorkerMsgBase>; + +export type ChannelLoadEvent = { + isEvent: true; + loaderId: number; + loadId: number; + channelIndex: number; + data: Uint8Array; + atlasDims?: [number, number]; +}; From 0d28a9ef4b3ffb4b4250da5a4c401b69df62fbe2 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 11:58:21 -0800 Subject: [PATCH 06/35] fix object prototypes not making it through worker messages --- src/workers/LoadWorkerHandle.ts | 39 +++++++++++++-------- src/workers/VolumeLoadWorker.ts | 60 +++++++++++++++++++-------------- src/workers/util.ts | 26 ++++++++++++++ 3 files changed, 86 insertions(+), 39 deletions(-) create mode 100644 src/workers/util.ts diff --git a/src/workers/LoadWorkerHandle.ts b/src/workers/LoadWorkerHandle.ts index 29f9cf13..ba243cfa 100644 --- a/src/workers/LoadWorkerHandle.ts +++ b/src/workers/LoadWorkerHandle.ts @@ -9,9 +9,11 @@ import { WorkerResponsePayload, ChannelLoadEvent, } from "./types"; +import { rebuildImageInfo, rebuildLoadSpec } from "./util"; -type StoredPromise = { - resolve: (value: WorkerResponsePayload) => void; +type StoredPromise = { + type: T; + resolve: (value: WorkerResponsePayload) => void; reject: (reason?: unknown) => void; }; @@ -20,9 +22,9 @@ type StoredPromise = { * This is separate from `LoadWorker` so that `sendMessage` and `onChannelData` can be shared with `WorkerLoader`s * without leaking the API outside this file. */ -class InternalLoadWorkerHandle { +class SharedLoadWorkerHandle { private worker: Worker; - private pendingRequests: (StoredPromise | undefined)[] = []; + private pendingRequests: (StoredPromise | undefined)[] = []; public onChannelData: ((e: ChannelLoadEvent) => void) | undefined = undefined; @@ -33,7 +35,7 @@ class InternalLoadWorkerHandle { } /** Given a handle for settling a promise when a response is received from the worker, store it and return its ID */ - private registerMessagePromise(prom: StoredPromise): number { + private registerMessagePromise(prom: StoredPromise): number { for (const [i, pendingPromise] of this.pendingRequests.entries()) { if (pendingPromise === undefined) { this.pendingRequests[i] = prom; @@ -47,7 +49,7 @@ class InternalLoadWorkerHandle { sendMessage(type: T, payload: WorkerRequestPayload): Promise> { let msgId = -1; const promise = new Promise>((resolve, reject) => { - msgId = this.registerMessagePromise({ resolve, reject } as StoredPromise); + msgId = this.registerMessagePromise({ type, resolve, reject } as StoredPromise); }); const msg: WorkerRequest = { msgId, type, payload, isEvent: false }; @@ -56,14 +58,19 @@ class InternalLoadWorkerHandle { return promise; } - private receiveMessage({ data }: MessageEvent | ChannelLoadEvent>): void { + private receiveMessage({ data }: MessageEvent | ChannelLoadEvent>): void { if (data.isEvent) { this.onChannelData?.(data); } else { const prom = this.pendingRequests[data.msgId]; + if (prom === undefined) { throw new Error(`Received response for unknown message ID ${data.msgId}`); } + if (prom.type !== data.type) { + throw new Error(`Received response of type ${data.type} for message of type ${prom.type}`); + } + prom.resolve(data.payload); this.pendingRequests[data.msgId] = undefined; } @@ -77,14 +84,14 @@ class InternalLoadWorkerHandle { } class LoadWorker { - private workerHandle: InternalLoadWorkerHandle; + private workerHandle: SharedLoadWorkerHandle; private openPromise: Promise; private activeLoader: WorkerLoader | undefined = undefined; private activeLoaderId = -1; constructor(maxCacheSize?: number) { - this.workerHandle = new InternalLoadWorkerHandle(); + this.workerHandle = new SharedLoadWorkerHandle(); this.openPromise = this.workerHandle.sendMessage(WorkerMsgType.INIT, { maxCacheSize }); } @@ -111,7 +118,7 @@ class WorkerLoader extends IVolumeLoader { private currentLoadId = -1; private currentLoadCallback: RawChannelDataCallback | undefined = undefined; - constructor(private loaderId: number, private workerHandle: InternalLoadWorkerHandle) { + constructor(private loaderId: number, private workerHandle: SharedLoadWorkerHandle) { super(); workerHandle.onChannelData = this.onChannelData.bind(this); } @@ -131,12 +138,13 @@ class WorkerLoader extends IVolumeLoader { return this.workerHandle.sendMessage(WorkerMsgType.LOAD_DIMS, loadSpec); } - createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]> { + async createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]> { this.checkIsActive(); - return this.workerHandle.sendMessage(WorkerMsgType.CREATE_VOLUME, loadSpec); + const [imageInfo, adjustedLoadSpec] = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_VOLUME, loadSpec); + return [rebuildImageInfo(imageInfo), rebuildLoadSpec(adjustedLoadSpec)]; } - loadRawChannelData( + async loadRawChannelData( imageInfo: ImageInfo, loadSpec: LoadSpec, onData: RawChannelDataCallback @@ -145,12 +153,15 @@ class WorkerLoader extends IVolumeLoader { this.currentLoadCallback = onData; this.currentLoadId += 1; - return this.workerHandle.sendMessage(WorkerMsgType.LOAD_VOLUME_DATA, { + + const [newImageInfo, newLoadSpec] = await this.workerHandle.sendMessage(WorkerMsgType.LOAD_VOLUME_DATA, { imageInfo, loadSpec, loaderId: this.loaderId, loadId: this.currentLoadId, }); + + return [newImageInfo && rebuildImageInfo(newImageInfo), newLoadSpec && rebuildLoadSpec(newLoadSpec)]; } onChannelData(e: ChannelLoadEvent): void { diff --git a/src/workers/VolumeLoadWorker.ts b/src/workers/VolumeLoadWorker.ts index 5373921c..a5ea60ed 100644 --- a/src/workers/VolumeLoadWorker.ts +++ b/src/workers/VolumeLoadWorker.ts @@ -2,17 +2,20 @@ import VolumeCache from "../VolumeCache"; import { createVolumeLoader } from "../loaders"; import { IVolumeLoader } from "../loaders/IVolumeLoader"; import { WorkerMsgType, WorkerRequest, WorkerRequestPayload, WorkerResponsePayload } from "./types"; +import { rebuildImageInfo, rebuildLoadSpec } from "./util"; let cache: VolumeCache | undefined = undefined; let loader: IVolumeLoader | undefined = undefined; +let initialized = false; -type MessageHandlersType = { - [T in WorkerMsgType]: (payload: WorkerRequestPayload) => Promise>; -}; +type MessageHandler = (payload: WorkerRequestPayload) => Promise>; -const messageHandlers: MessageHandlersType = { +const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { [WorkerMsgType.INIT]: ({ maxCacheSize }) => { - cache = new VolumeCache(maxCacheSize); + if (!initialized) { + cache = new VolumeCache(maxCacheSize); + initialized = true; + } return Promise.resolve(); }, @@ -25,40 +28,47 @@ const messageHandlers: MessageHandlersType = { if (loader === undefined) { throw new Error("No loader created"); } - const volume = await loader.createVolume(loadSpec); - return [volume.imageInfo, volume.loadSpec]; + + return await loader.createImageInfo(rebuildLoadSpec(loadSpec)); }, [WorkerMsgType.LOAD_DIMS]: async (loadSpec) => { if (loader === undefined) { throw new Error("No loader created"); } - return await loader.loadDims(loadSpec); + return await loader.loadDims(rebuildLoadSpec(loadSpec)); }, [WorkerMsgType.LOAD_VOLUME_DATA]: async ({ imageInfo, loadSpec, loaderId, loadId }) => { if (loader === undefined) { throw new Error("No loader created"); } - if (loaderId !== 0) { - throw new Error("Only one loader is supported"); - } - const [updatedImageInfo, updatedLoadSpec] = await loader.loadRawChannelData( - imageInfo, - loadSpec, - (data, channelIndex, atlasDims) => { - self.postMessage({ - isEvent: true, - loaderId, - loadId, - channelIndex, - data, - atlasDims, - }); + + return await loader.loadRawChannelData( + rebuildImageInfo(imageInfo), + rebuildLoadSpec(loadSpec), + (channelIndex, data, atlasDims) => { + self.postMessage({ isEvent: true, loaderId, loadId, channelIndex, data, atlasDims }, [data.buffer]); } ); - return [updatedImageInfo, updatedLoadSpec]; }, }; -self.onmessage = ({ data }: MessageEvent>) => {}; +self.onmessage = async ({ data }: MessageEvent>) => { + const { msgId, type, payload } = data; + const handler = messageHandlers[type]; + console.log("Worker received message of type " + type); + + // try { + const response = await handler(payload); + self.postMessage({ isEvent: false, msgId, type, payload: response }); + // } catch (e) { + // // self.postMessage({ + // // isEvent: false, + // // msgId, + // // type, + // // payload: e.message, + // // }); + // console.log(e); + // } +}; diff --git a/src/workers/util.ts b/src/workers/util.ts new file mode 100644 index 00000000..051e527e --- /dev/null +++ b/src/workers/util.ts @@ -0,0 +1,26 @@ +import { Box3, Vector2, Vector3 } from "three"; +import { LoadSpec } from "../loaders/IVolumeLoader"; +import { ImageInfo } from "../Volume"; + +export function rebuildLoadSpec(spec: LoadSpec): LoadSpec { + return { + ...spec, + subregion: new Box3(new Vector3().copy(spec.subregion.min), new Vector3().copy(spec.subregion.max)), + }; +} + +export function rebuildImageInfo(imageInfo: ImageInfo): ImageInfo { + return { + ...imageInfo, + originalSize: new Vector3().copy(imageInfo.originalSize), + atlasTileDims: new Vector2().copy(imageInfo.atlasTileDims), + volumeSize: new Vector3().copy(imageInfo.volumeSize), + subregionSize: new Vector3().copy(imageInfo.subregionSize), + subregionOffset: new Vector3().copy(imageInfo.subregionOffset), + physicalPixelSize: new Vector3().copy(imageInfo.physicalPixelSize), + transform: { + translation: new Vector3().copy(imageInfo.transform.translation), + rotation: new Vector3().copy(imageInfo.transform.rotation), + }, + }; +} From ceb254dc583bb1394e83135462f5c88c8f1e7fbb Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 13:17:26 -0800 Subject: [PATCH 07/35] better error passing from worker; don't run `TiffLoader` on worker --- src/loaders/index.ts | 26 ++++++++++++++----------- src/workers/LoadWorkerHandle.ts | 32 ++++++++++++++++++------------- src/workers/VolumeLoadWorker.ts | 34 +++++++++++++++++---------------- src/workers/types.ts | 19 ++++++++++++------ 4 files changed, 65 insertions(+), 46 deletions(-) diff --git a/src/loaders/index.ts b/src/loaders/index.ts index b24217a4..bced0a42 100644 --- a/src/loaders/index.ts +++ b/src/loaders/index.ts @@ -18,26 +18,30 @@ export type CreateLoaderOptions = { concurrencyLimit?: number; }; +export function pathToFileType(path: string): VolumeFileFormat { + if (path.endsWith(".json")) { + return VolumeFileFormat.JSON; + } else if (path.endsWith(".tif") || path.endsWith(".tiff")) { + return VolumeFileFormat.TIFF; + } else { + return VolumeFileFormat.ZARR; + } +} + export async function createVolumeLoader( path: string | string[], options?: CreateLoaderOptions ): Promise { const pathString = typeof path === "object" ? path[0] : path; + const fileType = options?.fileType || pathToFileType(pathString); - switch (options?.fileType) { + switch (fileType) { case VolumeFileFormat.ZARR: - return await OMEZarrLoader.createLoader(pathString, options.scene, options.cache, options.concurrencyLimit); + const { scene, cache, concurrencyLimit } = options || {}; + return await OMEZarrLoader.createLoader(pathString, scene, cache, concurrencyLimit); case VolumeFileFormat.JSON: - return new JsonImageInfoLoader(path, options.cache); + return new JsonImageInfoLoader(path, options?.cache); case VolumeFileFormat.TIFF: return new TiffLoader(pathString); - default: - if (pathString.endsWith(".json")) { - return new JsonImageInfoLoader(path, options?.cache); - } else if (pathString.endsWith(".tif") || pathString.endsWith(".tiff")) { - return new TiffLoader(pathString); - } else { - return await OMEZarrLoader.createLoader(pathString, options?.scene, options?.cache, options?.concurrencyLimit); - } } } diff --git a/src/workers/LoadWorkerHandle.ts b/src/workers/LoadWorkerHandle.ts index ba243cfa..dbebdadb 100644 --- a/src/workers/LoadWorkerHandle.ts +++ b/src/workers/LoadWorkerHandle.ts @@ -1,6 +1,7 @@ import { ImageInfo } from "../Volume"; -import { CreateLoaderOptions } from "../loaders"; +import { CreateLoaderOptions, VolumeFileFormat, pathToFileType } from "../loaders"; import { IVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "../loaders/IVolumeLoader"; +import { TiffLoader } from "../loaders/TiffLoader"; import { WorkerMsgType, WorkerRequest, @@ -8,6 +9,7 @@ import { WorkerResponse, WorkerResponsePayload, ChannelLoadEvent, + WorkerResponseKind, } from "./types"; import { rebuildImageInfo, rebuildLoadSpec } from "./util"; @@ -31,7 +33,6 @@ class SharedLoadWorkerHandle { constructor() { this.worker = new Worker(new URL("./VolumeLoadWorker", import.meta.url)); this.worker.onmessage = this.receiveMessage.bind(this); - this.worker.onerror = this.receiveError.bind(this); } /** Given a handle for settling a promise when a response is received from the worker, store it and return its ID */ @@ -52,14 +53,14 @@ class SharedLoadWorkerHandle { msgId = this.registerMessagePromise({ type, resolve, reject } as StoredPromise); }); - const msg: WorkerRequest = { msgId, type, payload, isEvent: false }; + const msg: WorkerRequest = { msgId, type, payload }; this.worker.postMessage(msg); return promise; } - private receiveMessage({ data }: MessageEvent | ChannelLoadEvent>): void { - if (data.isEvent) { + private receiveMessage({ data }: MessageEvent>): void { + if (data.responseKind === WorkerResponseKind.EVENT) { this.onChannelData?.(data); } else { const prom = this.pendingRequests[data.msgId]; @@ -71,16 +72,14 @@ class SharedLoadWorkerHandle { throw new Error(`Received response of type ${data.type} for message of type ${prom.type}`); } - prom.resolve(data.payload); + if (data.responseKind === WorkerResponseKind.ERROR) { + prom.reject(data.payload); + } else { + prom.resolve(data.payload); + } this.pendingRequests[data.msgId] = undefined; } } - - private receiveError(e: ErrorEvent): void { - // TODO propagate errors through promises - // if (!e.error) - console.log(e); - } } class LoadWorker { @@ -99,7 +98,14 @@ class LoadWorker { return this.openPromise; } - async createLoader(path: string | string[], options?: CreateLoaderOptions): Promise { + async createLoader(path: string | string[], options?: CreateLoaderOptions): Promise { + // Special case: TIFF loader doesn't work on a worker, has its own workers anyways, and doesn't use cache or queue. + const pathString = typeof path === "object" ? path[0] : path; + const fileType = options?.fileType || pathToFileType(pathString); + if (fileType === VolumeFileFormat.TIFF) { + return new TiffLoader(pathString); + } + const success = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_LOADER, { path, options }); if (!success) { throw new Error("Failed to create loader"); diff --git a/src/workers/VolumeLoadWorker.ts b/src/workers/VolumeLoadWorker.ts index a5ea60ed..0c7c44ff 100644 --- a/src/workers/VolumeLoadWorker.ts +++ b/src/workers/VolumeLoadWorker.ts @@ -1,7 +1,7 @@ import VolumeCache from "../VolumeCache"; import { createVolumeLoader } from "../loaders"; import { IVolumeLoader } from "../loaders/IVolumeLoader"; -import { WorkerMsgType, WorkerRequest, WorkerRequestPayload, WorkerResponsePayload } from "./types"; +import { WorkerMsgType, WorkerRequest, WorkerRequestPayload, WorkerResponseKind, WorkerResponsePayload } from "./types"; import { rebuildImageInfo, rebuildLoadSpec } from "./util"; let cache: VolumeCache | undefined = undefined; @@ -48,7 +48,17 @@ const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { rebuildImageInfo(imageInfo), rebuildLoadSpec(loadSpec), (channelIndex, data, atlasDims) => { - self.postMessage({ isEvent: true, loaderId, loadId, channelIndex, data, atlasDims }, [data.buffer]); + self.postMessage( + { + responseKind: WorkerResponseKind.EVENT, + loaderId, + loadId, + channelIndex, + data, + atlasDims, + }, + [data.buffer] + ); } ); }, @@ -56,19 +66,11 @@ const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { self.onmessage = async ({ data }: MessageEvent>) => { const { msgId, type, payload } = data; - const handler = messageHandlers[type]; - console.log("Worker received message of type " + type); - // try { - const response = await handler(payload); - self.postMessage({ isEvent: false, msgId, type, payload: response }); - // } catch (e) { - // // self.postMessage({ - // // isEvent: false, - // // msgId, - // // type, - // // payload: e.message, - // // }); - // console.log(e); - // } + try { + const response = await messageHandlers[type](payload); + self.postMessage({ responseKind: WorkerResponseKind.SUCCESS, msgId, type, payload: response }); + } catch (e) { + self.postMessage({ responseKind: WorkerResponseKind.ERROR, msgId, type, payload: (e as Error).message }); + } }; diff --git a/src/workers/types.ts b/src/workers/types.ts index 092f7d1c..fcfde640 100644 --- a/src/workers/types.ts +++ b/src/workers/types.ts @@ -2,7 +2,7 @@ import { ImageInfo } from "../Volume"; import { CreateLoaderOptions } from "../loaders"; import { LoadSpec, VolumeDims } from "../loaders/IVolumeLoader"; -export enum WorkerMsgType { +export const enum WorkerMsgType { INIT, CREATE_LOADER, CREATE_VOLUME, @@ -10,8 +10,13 @@ export enum WorkerMsgType { LOAD_VOLUME_DATA, } +export const enum WorkerResponseKind { + SUCCESS, + ERROR, + EVENT, +} + type WorkerMsgBase = { - isEvent: false; msgId: number; type: T; payload: P; @@ -43,14 +48,16 @@ export type WorkerResponsePayload = { [WorkerMsgType.LOAD_VOLUME_DATA]: [ImageInfo | undefined, LoadSpec | undefined]; }[T]; -export type WorkerRequest = WorkerMsgBase>; -export type WorkerResponse = WorkerMsgBase>; - export type ChannelLoadEvent = { - isEvent: true; loaderId: number; loadId: number; channelIndex: number; data: Uint8Array; atlasDims?: [number, number]; }; + +export type WorkerRequest = WorkerMsgBase>; +export type WorkerResponse = + | ({ responseKind: WorkerResponseKind.SUCCESS } & WorkerMsgBase>) + | ({ responseKind: WorkerResponseKind.ERROR } & WorkerMsgBase) + | ({ responseKind: WorkerResponseKind.EVENT } & ChannelLoadEvent); From 16af166268cce3fbcd51111a6028f06dc56d7fd9 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 13:22:39 -0800 Subject: [PATCH 08/35] add `close` method to `LoadWorker` --- src/workers/LoadWorkerHandle.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/workers/LoadWorkerHandle.ts b/src/workers/LoadWorkerHandle.ts index dbebdadb..b30ce35f 100644 --- a/src/workers/LoadWorkerHandle.ts +++ b/src/workers/LoadWorkerHandle.ts @@ -27,6 +27,7 @@ type StoredPromise = { class SharedLoadWorkerHandle { private worker: Worker; private pendingRequests: (StoredPromise | undefined)[] = []; + private workerOpen = true; public onChannelData: ((e: ChannelLoadEvent) => void) | undefined = undefined; @@ -47,6 +48,15 @@ class SharedLoadWorkerHandle { return this.pendingRequests.push(prom) - 1; } + get isOpen(): boolean { + return this.workerOpen; + } + + close(): void { + this.worker.terminate(); + this.workerOpen = false; + } + sendMessage(type: T, payload: WorkerRequestPayload): Promise> { let msgId = -1; const promise = new Promise>((resolve, reject) => { @@ -95,9 +105,17 @@ class LoadWorker { } onOpen(): Promise { + if (!this.workerHandle.isOpen) { + return Promise.reject("Worker is closed"); + } return this.openPromise; } + close(): void { + this.workerHandle.close(); + this.activeLoader?.close(); + } + async createLoader(path: string | string[], options?: CreateLoaderOptions): Promise { // Special case: TIFF loader doesn't work on a worker, has its own workers anyways, and doesn't use cache or queue. const pathString = typeof path === "object" ? path[0] : path; From 6c83c1c006ff4d163418e4e5e37de993d45c7471 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 13:25:07 -0800 Subject: [PATCH 09/35] futzing with names in `WorkerLoader` --- src/workers/LoadWorkerHandle.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/workers/LoadWorkerHandle.ts b/src/workers/LoadWorkerHandle.ts index b30ce35f..738fd182 100644 --- a/src/workers/LoadWorkerHandle.ts +++ b/src/workers/LoadWorkerHandle.ts @@ -137,8 +137,7 @@ class LoadWorker { } class WorkerLoader extends IVolumeLoader { - private isActive = true; - + private isOpen = true; private currentLoadId = -1; private currentLoadCallback: RawChannelDataCallback | undefined = undefined; @@ -147,23 +146,23 @@ class WorkerLoader extends IVolumeLoader { workerHandle.onChannelData = this.onChannelData.bind(this); } - private checkIsActive(): void { - if (!this.isActive) { + private checkIsOpen(): void { + if (!this.isOpen || !this.workerHandle.isOpen) { throw new Error("Tried to use a closed loader"); } } close(): void { - this.isActive = false; + this.isOpen = false; } loadDims(loadSpec: LoadSpec): Promise { - this.checkIsActive(); + this.checkIsOpen(); return this.workerHandle.sendMessage(WorkerMsgType.LOAD_DIMS, loadSpec); } async createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]> { - this.checkIsActive(); + this.checkIsOpen(); const [imageInfo, adjustedLoadSpec] = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_VOLUME, loadSpec); return [rebuildImageInfo(imageInfo), rebuildLoadSpec(adjustedLoadSpec)]; } @@ -173,7 +172,7 @@ class WorkerLoader extends IVolumeLoader { loadSpec: LoadSpec, onData: RawChannelDataCallback ): Promise<[ImageInfo | undefined, LoadSpec | undefined]> { - this.checkIsActive(); + this.checkIsOpen(); this.currentLoadCallback = onData; this.currentLoadId += 1; From c47dd7b3ac0a81c43f86490f4e6aac31be93dadf Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 13:36:26 -0800 Subject: [PATCH 10/35] futzing with type checks and lint --- src/loaders/JsonImageInfoLoader.ts | 2 +- src/loaders/index.ts | 3 +-- src/workers/VolumeLoadWorker.ts | 35 ++++++++++++++++++------------ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/loaders/JsonImageInfoLoader.ts b/src/loaders/JsonImageInfoLoader.ts index d28bed2f..df6a1a16 100644 --- a/src/loaders/JsonImageInfoLoader.ts +++ b/src/loaders/JsonImageInfoLoader.ts @@ -1,7 +1,7 @@ import { Box3, Vector2, Vector3 } from "three"; import { IVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; -import Volume, { ImageInfo } from "../Volume"; +import { ImageInfo } from "../Volume"; import VolumeCache from "../VolumeCache"; interface PackedChannelsImage { diff --git a/src/loaders/index.ts b/src/loaders/index.ts index bced0a42..77e89a0d 100644 --- a/src/loaders/index.ts +++ b/src/loaders/index.ts @@ -37,8 +37,7 @@ export async function createVolumeLoader( switch (fileType) { case VolumeFileFormat.ZARR: - const { scene, cache, concurrencyLimit } = options || {}; - return await OMEZarrLoader.createLoader(pathString, scene, cache, concurrencyLimit); + return await OMEZarrLoader.createLoader(pathString, options?.scene, options?.cache, options?.concurrencyLimit); case VolumeFileFormat.JSON: return new JsonImageInfoLoader(path, options?.cache); case VolumeFileFormat.TIFF: diff --git a/src/workers/VolumeLoadWorker.ts b/src/workers/VolumeLoadWorker.ts index 0c7c44ff..bf849544 100644 --- a/src/workers/VolumeLoadWorker.ts +++ b/src/workers/VolumeLoadWorker.ts @@ -1,7 +1,14 @@ import VolumeCache from "../VolumeCache"; import { createVolumeLoader } from "../loaders"; import { IVolumeLoader } from "../loaders/IVolumeLoader"; -import { WorkerMsgType, WorkerRequest, WorkerRequestPayload, WorkerResponseKind, WorkerResponsePayload } from "./types"; +import { + WorkerMsgType, + WorkerRequest, + WorkerRequestPayload, + WorkerResponse, + WorkerResponseKind, + WorkerResponsePayload, +} from "./types"; import { rebuildImageInfo, rebuildLoadSpec } from "./util"; let cache: VolumeCache | undefined = undefined; @@ -48,17 +55,15 @@ const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { rebuildImageInfo(imageInfo), rebuildLoadSpec(loadSpec), (channelIndex, data, atlasDims) => { - self.postMessage( - { - responseKind: WorkerResponseKind.EVENT, - loaderId, - loadId, - channelIndex, - data, - atlasDims, - }, - [data.buffer] - ); + const message: WorkerResponse = { + responseKind: WorkerResponseKind.EVENT, + loaderId, + loadId, + channelIndex, + data, + atlasDims, + }; + self.postMessage(message, [data.buffer]); } ); }, @@ -66,11 +71,13 @@ const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { self.onmessage = async ({ data }: MessageEvent>) => { const { msgId, type, payload } = data; + let message: WorkerResponse; try { const response = await messageHandlers[type](payload); - self.postMessage({ responseKind: WorkerResponseKind.SUCCESS, msgId, type, payload: response }); + message = { responseKind: WorkerResponseKind.SUCCESS, msgId, type, payload: response }; } catch (e) { - self.postMessage({ responseKind: WorkerResponseKind.ERROR, msgId, type, payload: (e as Error).message }); + message = { responseKind: WorkerResponseKind.ERROR, msgId, type, payload: (e as Error).message }; } + self.postMessage(message); }; From d620ac4c9138cd1f3de290b2b7cbf70b92fe876d Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 14:52:45 -0800 Subject: [PATCH 11/35] mess with how we check for arrays in a couple spots --- public/index.ts | 8 ++++++-- src/workers/LoadWorkerHandle.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/public/index.ts b/public/index.ts index 95c2ed93..fab42831 100644 --- a/public/index.ts +++ b/public/index.ts @@ -23,6 +23,7 @@ import { import { OpenCellLoader } from "../src/loaders/OpenCellLoader"; import { State, TestDataSpec } from "./types"; import { getDefaultImageInfo } from "../src/Volume"; +import LoadWorker from "../src/workers/LoadWorkerHandle"; const CACHE_MAX_SIZE = 1_000_000_000; const PLAYBACK_INTERVAL = 80; @@ -83,7 +84,8 @@ const TEST_DATA: Record = { let view3D: View3d; -const volumeCache = new VolumeCache(CACHE_MAX_SIZE); +// const volumeCache = new VolumeCache(CACHE_MAX_SIZE); +const loadWorker = new LoadWorker(CACHE_MAX_SIZE); const myState: State = { file: "", @@ -1005,6 +1007,8 @@ function createTestVolume() { } async function createLoader(data: TestDataSpec): Promise { + await loadWorker.onOpen(); + console.log(new Vector2(0, 1)); if (data.type === "opencell") { return new OpenCellLoader(); } @@ -1018,7 +1022,7 @@ async function createLoader(data: TestDataSpec): Promise { } } - return await createVolumeLoader(path, { cache: volumeCache }); + return await loadWorker.createLoader(path); } async function loadVolume(loadSpec: LoadSpec, loader: IVolumeLoader): Promise { diff --git a/src/workers/LoadWorkerHandle.ts b/src/workers/LoadWorkerHandle.ts index 738fd182..2aafc18b 100644 --- a/src/workers/LoadWorkerHandle.ts +++ b/src/workers/LoadWorkerHandle.ts @@ -118,7 +118,7 @@ class LoadWorker { async createLoader(path: string | string[], options?: CreateLoaderOptions): Promise { // Special case: TIFF loader doesn't work on a worker, has its own workers anyways, and doesn't use cache or queue. - const pathString = typeof path === "object" ? path[0] : path; + const pathString = Array.isArray(path) ? path[0] : path; const fileType = options?.fileType || pathToFileType(pathString); if (fileType === VolumeFileFormat.TIFF) { return new TiffLoader(pathString); From 2f629618d11615b173ca2e49f2c1dab51f342cc4 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 14:53:00 -0800 Subject: [PATCH 12/35] commited the wrong file --- src/loaders/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loaders/index.ts b/src/loaders/index.ts index 77e89a0d..5d25cecc 100644 --- a/src/loaders/index.ts +++ b/src/loaders/index.ts @@ -32,7 +32,7 @@ export async function createVolumeLoader( path: string | string[], options?: CreateLoaderOptions ): Promise { - const pathString = typeof path === "object" ? path[0] : path; + const pathString = Array.isArray(path) ? path[0] : path; const fileType = options?.fileType || pathToFileType(pathString); switch (fileType) { From d0a14f02fb8408c2ba060e07587e71975aa8d373 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 15:56:05 -0800 Subject: [PATCH 13/35] remove unused imports --- public/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/index.ts b/public/index.ts index fab42831..53e314ce 100644 --- a/public/index.ts +++ b/public/index.ts @@ -9,7 +9,6 @@ import { JsonImageInfoLoader, View3d, Volume, - VolumeCache, VolumeMaker, Light, AREA_LIGHT, @@ -17,7 +16,6 @@ import { RENDERMODE_RAYMARCH, SKY_LIGHT, VolumeFileFormat, - createVolumeLoader, } from "../src"; // special loader really just for this demo app but lives with the other loaders import { OpenCellLoader } from "../src/loaders/OpenCellLoader"; From c276f5012f8ee24345828ba586a4612a23dbc9d4 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 15:56:54 -0800 Subject: [PATCH 14/35] change JSON loader to not rely on DOM objects --- src/loaders/JsonImageInfoLoader.ts | 114 +++++++++++------------------ src/workers/VolumeLoadWorker.ts | 8 +- 2 files changed, 49 insertions(+), 73 deletions(-) diff --git a/src/loaders/JsonImageInfoLoader.ts b/src/loaders/JsonImageInfoLoader.ts index df6a1a16..fb29243a 100644 --- a/src/loaders/JsonImageInfoLoader.ts +++ b/src/loaders/JsonImageInfoLoader.ts @@ -8,7 +8,6 @@ interface PackedChannelsImage { name: string; channels: number[]; } -type PackedChannelsImageRequests = Record; /* eslint-disable @typescript-eslint/naming-convention */ type JsonImageInfo = { @@ -189,9 +188,6 @@ class JsonImageInfoLoader extends IVolumeLoader { * @param {Volume} volume * @param {Array.<{name:string, channels:Array.}>} imageArray * @param {PerChannelCallback} onChannelLoaded Per-channel callback. Called when each channel's atlased volume data is loaded - * @returns {Object.} a map(imageurl : Image object) that should be used to cancel the download requests, - * for example if you need to destroy the image before all data has arrived. - * as requests arrive, the callback will be called per image, not per channel * @example loadVolumeAtlasData([{ * "name": "AICS-10_5_5.ome.tif_atlas_0.png", * "channels": [0, 1, 2] @@ -203,26 +199,19 @@ class JsonImageInfoLoader extends IVolumeLoader { * "channels": [6, 7, 8] * }], mycallback); */ - static loadVolumeAtlasData( + static async loadVolumeAtlasData( imageArray: PackedChannelsImage[], onData: RawChannelDataCallback, cache?: VolumeCache - ): PackedChannelsImageRequests { - const numImages = imageArray.length; - - const requests = {}; - //console.log("BEGIN DOWNLOAD DATA"); - for (let i = 0; i < numImages; ++i) { - const url = imageArray[i].name; - const batch = imageArray[i].channels; - + ): Promise { + const loadPromises = imageArray.map(async (image) => { // Because the data is fetched such that one fetch returns a whole batch, // if any in batch is cached then they all should be. So if any in batch is NOT cached, // then we will have to do a batch request. This logic works both ways because it's all or nothing. let cacheHit = true; - for (let j = 0; j < Math.min(batch.length, 4); ++j) { - const chindex = batch[j]; - const cacheResult = cache?.get(`${url}/${chindex}`); + for (let j = 0; j < Math.min(image.channels.length, 4); ++j) { + const chindex = image.channels[j]; + const cacheResult = cache?.get(`${image.name}/${chindex}`); if (cacheResult) { onData(chindex, new Uint8Array(cacheResult)); } else { @@ -235,67 +224,50 @@ class JsonImageInfoLoader extends IVolumeLoader { // if all channels were in cache then we can move on to the next // image (batch) without requesting if (cacheHit) { - continue; + return; } - // using Image is just a trick to download the bits as a png. - // the Image will never be used again. - const img: HTMLImageElement = new Image(); - - img.onerror = () => { - console.log("ERROR LOADING " + url); - }; - - img.onload = (event) => { - //console.log("GOT ch " + me.src); - // extract pixels by drawing to canvas - const canvas = document.createElement("canvas"); - // nice thing about this is i could downsample here - const w = Math.floor((event?.target as HTMLImageElement).naturalWidth); - const h = Math.floor((event?.target as HTMLImageElement).naturalHeight); - canvas.setAttribute("width", "" + w); - canvas.setAttribute("height", "" + h); - const ctx = canvas.getContext("2d"); - if (!ctx) { - console.log("Error creating canvas 2d context for " + url); - return; - } - ctx.globalCompositeOperation = "copy"; - ctx.globalAlpha = 1.0; - ctx.drawImage(event?.target as CanvasImageSource, 0, 0, w, h); - // getImageData returns rgba. - // optimize: collapse rgba to single channel arrays - const iData = ctx.getImageData(0, 0, w, h); - - const channelsBits: Uint8Array[] = []; - - // allocate channels in batch - for (let ch = 0; ch < Math.min(batch.length, 4); ++ch) { - channelsBits.push(new Uint8Array(w * h)); - } + const response = await fetch(image.name, { mode: "cors" }); + const blob = await response.blob(); + const bitmap = await createImageBitmap(blob); - // extract the data - for (let j = 0; j < Math.min(batch.length, 4); ++j) { - for (let px = 0; px < w * h; px++) { - channelsBits[j][px] = iData.data[px * 4 + j]; - } - } + const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); + const ctx = canvas.getContext("2d"); + if (!ctx) { + console.log("Error creating canvas 2d context for " + image.name); + return; + } + ctx.globalCompositeOperation = "copy"; + ctx.globalAlpha = 1.0; + ctx.drawImage(bitmap, 0, 0); + const iData = ctx.getImageData(0, 0, bitmap.width, bitmap.height); - // done with img, iData, and canvas now. + const channelsBits: Uint8Array[] = []; + const length = bitmap.width * bitmap.height; - for (let ch = 0; ch < Math.min(batch.length, 4); ++ch) { - const chindex = batch[ch]; - cache?.insert(`${url}/${chindex}`, channelsBits[ch]); - // NOTE: the atlas dimensions passed in here are currently unused - onData(chindex, channelsBits[ch], [w, h]); + // allocate channels in batch + for (let ch = 0; ch < Math.min(image.channels.length, 4); ++ch) { + channelsBits.push(new Uint8Array(length)); + } + + // extract the data + for (let j = 0; j < Math.min(image.channels.length, 4); ++j) { + for (let px = 0; px < length; px++) { + channelsBits[j][px] = iData.data[px * 4 + j]; } - }; - img.crossOrigin = "Anonymous"; - img.src = url; - requests[url] = img; - } + } + + // done with img, iData, and canvas now. + + for (let ch = 0; ch < Math.min(image.channels.length, 4); ++ch) { + const chindex = image.channels[ch]; + cache?.insert(`${image.name}/${chindex}`, channelsBits[ch]); + // NOTE: the atlas dimensions passed in here are currently unused by `JSONImageInfoLoader` + onData(chindex, channelsBits[ch], [bitmap.width, bitmap.height]); + } + }); - return requests; + await Promise.all(loadPromises); } } diff --git a/src/workers/VolumeLoadWorker.ts b/src/workers/VolumeLoadWorker.ts index bf849544..c705e20b 100644 --- a/src/workers/VolumeLoadWorker.ts +++ b/src/workers/VolumeLoadWorker.ts @@ -1,5 +1,5 @@ import VolumeCache from "../VolumeCache"; -import { createVolumeLoader } from "../loaders"; +import { VolumeFileFormat, createVolumeLoader, pathToFileType } from "../loaders"; import { IVolumeLoader } from "../loaders/IVolumeLoader"; import { WorkerMsgType, @@ -14,6 +14,7 @@ import { rebuildImageInfo, rebuildLoadSpec } from "./util"; let cache: VolumeCache | undefined = undefined; let loader: IVolumeLoader | undefined = undefined; let initialized = false; +let copyOnLoad = false; type MessageHandler = (payload: WorkerRequestPayload) => Promise>; @@ -27,6 +28,9 @@ const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { }, [WorkerMsgType.CREATE_LOADER]: async ({ path, options }) => { + const pathString = Array.isArray(path) ? path[0] : path; + const fileType = options?.fileType || pathToFileType(pathString); + copyOnLoad = fileType === VolumeFileFormat.JSON; loader = await createVolumeLoader(path, { ...options, cache }); return loader !== undefined; }, @@ -63,7 +67,7 @@ const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { data, atlasDims, }; - self.postMessage(message, [data.buffer]); + self.postMessage(message, copyOnLoad ? [] : [data.buffer]); } ); }, From a4df0dfc4f3afa3f3daec7726f8e58c65431f469 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 16:06:17 -0800 Subject: [PATCH 15/35] zarr prefetch tweaks --- src/loaders/OmeZarrLoader.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/loaders/OmeZarrLoader.ts b/src/loaders/OmeZarrLoader.ts index 56585513..cca9f6f4 100644 --- a/src/loaders/OmeZarrLoader.ts +++ b/src/loaders/OmeZarrLoader.ts @@ -7,12 +7,11 @@ import { AbsolutePath, AsyncReadable, Readable } from "@zarrita/storage"; // Getting it from the top-level package means we don't get its type. This is also a bug, but it's more acceptable. import { FetchStore } from "zarrita"; -import Volume, { ImageInfo } from "../Volume"; +import { ImageInfo } from "../Volume"; import VolumeCache from "../VolumeCache"; import SubscribableRequestQueue from "../utils/SubscribableRequestQueue"; -import { IVolumeLoader, LoadSpec, PerChannelCallback, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; +import { IVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; import { - buildDefaultMetadata, composeSubregion, computePackedAtlasDims, convertSubregionToPixels, @@ -496,6 +495,9 @@ class OMEZarrLoader extends IVolumeLoader { const activeTimestep = chunkCoords[0][0]; const tmin = Math.max(0, activeTimestep - PREFETCH_TIME_MARGIN); const tmax = Math.min(chunkDims[0], activeTimestep + PREFETCH_TIME_MARGIN); + const matchesTCZYX = ([t1, c1, z1, y1, x1]: TCZYX, [t2, c2, z2, y2, x2]: TCZYX) => { + return t1 === t2 && c1 === c2 && z1 === z2 && y1 === y2 && x1 === x2; + }; for (let t = tmin; t < tmax; t++) { const subscriber = this.requestQueue.addSubscriber(); this.prefetchSubscribers[t] = subscriber; @@ -508,7 +510,9 @@ class OMEZarrLoader extends IVolumeLoader { for (let z = 0; z < chunkDims[2]; z++) { for (let y = 0; y < chunkDims[3]; y++) { for (let x = 0; x < chunkDims[4]; x++) { - this.prefetchChunk(scaleLevel.path, [t, c, z, y, x], subscriber); + if (!chunkCoords.some((coord) => matchesTCZYX(coord, [t, c, z, y, x]))) { + this.prefetchChunk(scaleLevel.path, [t, c, z, y, x], subscriber); + } } } } @@ -582,7 +586,6 @@ class OMEZarrLoader extends IVolumeLoader { const sliceSpec = this.orderByDimension(unorderedSpec as TCZYX); try { - console.log(level, sliceSpec, subscriber); const result = await zarrGet(level, sliceSpec, { opts: { subscriber, reportKey } }); const u8 = convertChannel(result.data); onData(ch, u8); @@ -595,12 +598,10 @@ class OMEZarrLoader extends IVolumeLoader { } }); - console.log(keys); - setTimeout(() => this.beginPrefetch(keys, level), 0); - - Promise.all(channelPromises).then(() => - this.requestQueue.removeSubscriber(subscriber, CHUNK_REQUEST_CANCEL_REASON) - ); + Promise.all(channelPromises).then(() => { + this.requestQueue.removeSubscriber(subscriber, CHUNK_REQUEST_CANCEL_REASON); + setTimeout(() => this.beginPrefetch(keys, level), 1000); + }); return Promise.resolve([updatedImageInfo, loadSpec]); } } From 773d1a6a425e78f774bd9a78356a2543d3bf9641 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 16:09:40 -0800 Subject: [PATCH 16/35] remove prefetch console log --- src/loaders/OmeZarrLoader.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/loaders/OmeZarrLoader.ts b/src/loaders/OmeZarrLoader.ts index cca9f6f4..944e8600 100644 --- a/src/loaders/OmeZarrLoader.ts +++ b/src/loaders/OmeZarrLoader.ts @@ -121,9 +121,6 @@ class WrappedStore = Readable> implements A if (!this.cache || ZARR_EXTS.some((s) => key.endsWith(s))) { return this.baseStore.get(key, opts?.options); } - if (opts?.isPrefetch) { - console.log("prefetch: ", key); - } if (opts?.reportKey) { opts.reportKey(key, opts.subscriber); } From dde77396e0d072ccdd50fb4bc775f8cdbb743044 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 16:13:45 -0800 Subject: [PATCH 17/35] convince the type checker that I know what I'm doing --- src/loaders/JsonImageInfoLoader.ts | 2 +- src/workers/VolumeLoadWorker.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/loaders/JsonImageInfoLoader.ts b/src/loaders/JsonImageInfoLoader.ts index fb29243a..9b63aac6 100644 --- a/src/loaders/JsonImageInfoLoader.ts +++ b/src/loaders/JsonImageInfoLoader.ts @@ -232,7 +232,7 @@ class JsonImageInfoLoader extends IVolumeLoader { const bitmap = await createImageBitmap(blob); const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); - const ctx = canvas.getContext("2d"); + const ctx = canvas.getContext("2d") as OffscreenCanvasRenderingContext2D | null; if (!ctx) { console.log("Error creating canvas 2d context for " + image.name); return; diff --git a/src/workers/VolumeLoadWorker.ts b/src/workers/VolumeLoadWorker.ts index c705e20b..c355f963 100644 --- a/src/workers/VolumeLoadWorker.ts +++ b/src/workers/VolumeLoadWorker.ts @@ -67,7 +67,7 @@ const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { data, atlasDims, }; - self.postMessage(message, copyOnLoad ? [] : [data.buffer]); + (self as unknown as Worker).postMessage(message, copyOnLoad ? [] : [data.buffer]); } ); }, From 68eb73e8d82654b3c7909d57104a8aed6f2968e7 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 16:26:12 -0800 Subject: [PATCH 18/35] restore `IVolumeLoader` to its original form --- src/loaders/IVolumeLoader.ts | 43 ++++++++++++++++++------------ src/loaders/JsonImageInfoLoader.ts | 4 +-- src/loaders/OmeZarrLoader.ts | 4 +-- src/loaders/OpenCellLoader.ts | 4 +-- src/loaders/TiffLoader.ts | 4 +-- src/loaders/index.ts | 4 +-- src/workers/LoadWorkerHandle.ts | 4 +-- src/workers/VolumeLoadWorker.ts | 4 +-- 8 files changed, 40 insertions(+), 31 deletions(-) diff --git a/src/loaders/IVolumeLoader.ts b/src/loaders/IVolumeLoader.ts index 865b8808..4b03a57c 100644 --- a/src/loaders/IVolumeLoader.ts +++ b/src/loaders/IVolumeLoader.ts @@ -43,17 +43,9 @@ export type RawChannelDataCallback = (ch: number, data: Uint8Array, atlasDims?: * Loaders may keep state for reuse between volume creation and volume loading, and should be kept alive until volume * loading is complete. (See `createVolume`) */ -export abstract class IVolumeLoader { +export interface IVolumeLoader { /** Use VolumeDims to further refine a `LoadSpec` for use in `createVolume` */ - abstract loadDims(loadSpec: LoadSpec): Promise; - - abstract createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]>; - - abstract loadRawChannelData( - imageInfo: ImageInfo, - loadSpec: LoadSpec, - onData: RawChannelDataCallback - ): Promise<[ImageInfo | undefined, LoadSpec | undefined]>; + loadDims(loadSpec: LoadSpec): Promise; /** * Create an empty `Volume` from a `LoadSpec`, which must be passed to `loadVolumeData` to begin loading. @@ -63,13 +55,7 @@ export abstract class IVolumeLoader { * information about that source. Once this method has been called, every subsequent call to it or * `loadVolumeData` should reference the same source. */ - async createVolume(loadSpec: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { - const [imageInfo, adjustedSpec] = await this.createImageInfo(loadSpec); - const vol = new Volume(imageInfo, adjustedSpec, this); - vol.channelLoadCallback = onChannelLoaded; - vol.imageMetadata = buildDefaultMetadata(imageInfo); - return vol; - } + createVolume(loadSpec: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise; /** * Begin loading a volume's data, as specified in its `LoadSpec`. @@ -80,6 +66,29 @@ export abstract class IVolumeLoader { // TODO this is not cancellable in the sense that any async requests initiated here are not stored // in a way that they can be interrupted. // TODO explicitly passing a `LoadSpec` is now rarely useful. Remove? + loadVolumeData(volume: Volume, loadSpec?: LoadSpec, onChannelLoaded?: PerChannelCallback): void; +} + +/** Abstract class which allows loaders to accept and return types that are easier to transfer to/from a worker. */ +export abstract class ThreadableVolumeLoader implements IVolumeLoader { + abstract loadDims(loadSpec: LoadSpec): Promise; + + abstract createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]>; + + abstract loadRawChannelData( + imageInfo: ImageInfo, + loadSpec: LoadSpec, + onData: RawChannelDataCallback + ): Promise<[ImageInfo | undefined, LoadSpec | undefined]>; + + async createVolume(loadSpec: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { + const [imageInfo, adjustedSpec] = await this.createImageInfo(loadSpec); + const vol = new Volume(imageInfo, adjustedSpec, this); + vol.channelLoadCallback = onChannelLoaded; + vol.imageMetadata = buildDefaultMetadata(imageInfo); + return vol; + } + async loadVolumeData(volume: Volume, loadSpec?: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { const onChannelData: RawChannelDataCallback = (channelIndex, data, atlasDims) => { if (atlasDims) { diff --git a/src/loaders/JsonImageInfoLoader.ts b/src/loaders/JsonImageInfoLoader.ts index 9b63aac6..1edb2fa7 100644 --- a/src/loaders/JsonImageInfoLoader.ts +++ b/src/loaders/JsonImageInfoLoader.ts @@ -1,6 +1,6 @@ import { Box3, Vector2, Vector3 } from "three"; -import { IVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; +import { ThreadableVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; import { ImageInfo } from "../Volume"; import VolumeCache from "../VolumeCache"; @@ -91,7 +91,7 @@ const convertImageInfo = (json: JsonImageInfo): ImageInfo => ({ userData: json.userData, }); -class JsonImageInfoLoader extends IVolumeLoader { +class JsonImageInfoLoader extends ThreadableVolumeLoader { urls: string[]; jsonInfo: (JsonImageInfo | undefined)[]; diff --git a/src/loaders/OmeZarrLoader.ts b/src/loaders/OmeZarrLoader.ts index 944e8600..cc96685b 100644 --- a/src/loaders/OmeZarrLoader.ts +++ b/src/loaders/OmeZarrLoader.ts @@ -10,7 +10,7 @@ import { FetchStore } from "zarrita"; import { ImageInfo } from "../Volume"; import VolumeCache from "../VolumeCache"; import SubscribableRequestQueue from "../utils/SubscribableRequestQueue"; -import { IVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; +import { ThreadableVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; import { composeSubregion, computePackedAtlasDims, @@ -217,7 +217,7 @@ function convertChannel(channelData: zarr.TypedArray): Uint type NumericZarrArray = zarr.Array>; -class OMEZarrLoader extends IVolumeLoader { +class OMEZarrLoader extends ThreadableVolumeLoader { /** Hold one optional subscriber ID per timestep, each defined iff a batch of prefetches is waiting for that frame */ private prefetchSubscribers: (SubscriberId | undefined)[]; /** The ID of the subscriber responsible for "actual loads" (non-prefetch requests) */ diff --git a/src/loaders/OpenCellLoader.ts b/src/loaders/OpenCellLoader.ts index f9b49522..a9f587eb 100644 --- a/src/loaders/OpenCellLoader.ts +++ b/src/loaders/OpenCellLoader.ts @@ -1,10 +1,10 @@ import { Vector2, Vector3 } from "three"; -import { IVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; +import { ThreadableVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; import { ImageInfo } from "../Volume"; import { JsonImageInfoLoader } from "./JsonImageInfoLoader"; -class OpenCellLoader extends IVolumeLoader { +class OpenCellLoader extends ThreadableVolumeLoader { async loadDims(_: LoadSpec): Promise { const d = new VolumeDims(); d.shape = [1, 2, 27, 600, 600]; diff --git a/src/loaders/TiffLoader.ts b/src/loaders/TiffLoader.ts index 5e158540..89f36c7b 100644 --- a/src/loaders/TiffLoader.ts +++ b/src/loaders/TiffLoader.ts @@ -1,7 +1,7 @@ import { fromUrl } from "geotiff"; import { Vector3 } from "three"; -import { IVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; +import { ThreadableVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; import { computePackedAtlasDims } from "./VolumeLoaderUtils"; import { ImageInfo } from "../Volume"; @@ -61,7 +61,7 @@ function getOMEDims(imageEl: Element): OMEDims { const getBytesPerSample = (type: string): number => (type === "uint8" ? 1 : type === "uint16" ? 2 : 4); -class TiffLoader extends IVolumeLoader { +class TiffLoader extends ThreadableVolumeLoader { url: string; dims?: OMEDims; diff --git a/src/loaders/index.ts b/src/loaders/index.ts index 5d25cecc..33e24ea3 100644 --- a/src/loaders/index.ts +++ b/src/loaders/index.ts @@ -1,4 +1,4 @@ -import { IVolumeLoader } from "./IVolumeLoader"; +import { ThreadableVolumeLoader } from "./IVolumeLoader"; import { OMEZarrLoader } from "./OmeZarrLoader"; import { JsonImageInfoLoader } from "./JsonImageInfoLoader"; @@ -31,7 +31,7 @@ export function pathToFileType(path: string): VolumeFileFormat { export async function createVolumeLoader( path: string | string[], options?: CreateLoaderOptions -): Promise { +): Promise { const pathString = Array.isArray(path) ? path[0] : path; const fileType = options?.fileType || pathToFileType(pathString); diff --git a/src/workers/LoadWorkerHandle.ts b/src/workers/LoadWorkerHandle.ts index 2aafc18b..83859ea4 100644 --- a/src/workers/LoadWorkerHandle.ts +++ b/src/workers/LoadWorkerHandle.ts @@ -1,6 +1,6 @@ import { ImageInfo } from "../Volume"; import { CreateLoaderOptions, VolumeFileFormat, pathToFileType } from "../loaders"; -import { IVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "../loaders/IVolumeLoader"; +import { ThreadableVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "../loaders/IVolumeLoader"; import { TiffLoader } from "../loaders/TiffLoader"; import { WorkerMsgType, @@ -136,7 +136,7 @@ class LoadWorker { } } -class WorkerLoader extends IVolumeLoader { +class WorkerLoader extends ThreadableVolumeLoader { private isOpen = true; private currentLoadId = -1; private currentLoadCallback: RawChannelDataCallback | undefined = undefined; diff --git a/src/workers/VolumeLoadWorker.ts b/src/workers/VolumeLoadWorker.ts index c355f963..220a973d 100644 --- a/src/workers/VolumeLoadWorker.ts +++ b/src/workers/VolumeLoadWorker.ts @@ -1,6 +1,6 @@ import VolumeCache from "../VolumeCache"; import { VolumeFileFormat, createVolumeLoader, pathToFileType } from "../loaders"; -import { IVolumeLoader } from "../loaders/IVolumeLoader"; +import { ThreadableVolumeLoader } from "../loaders/IVolumeLoader"; import { WorkerMsgType, WorkerRequest, @@ -12,7 +12,7 @@ import { import { rebuildImageInfo, rebuildLoadSpec } from "./util"; let cache: VolumeCache | undefined = undefined; -let loader: IVolumeLoader | undefined = undefined; +let loader: ThreadableVolumeLoader | undefined = undefined; let initialized = false; let copyOnLoad = false; From 8cafddfe929b142e7134b001865e3eb13fcfe464 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Fri, 15 Dec 2023 16:59:41 -0800 Subject: [PATCH 19/35] remove a redundant `else` --- src/loaders/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/loaders/index.ts b/src/loaders/index.ts index 33e24ea3..6da3475f 100644 --- a/src/loaders/index.ts +++ b/src/loaders/index.ts @@ -23,9 +23,8 @@ export function pathToFileType(path: string): VolumeFileFormat { return VolumeFileFormat.JSON; } else if (path.endsWith(".tif") || path.endsWith(".tiff")) { return VolumeFileFormat.TIFF; - } else { - return VolumeFileFormat.ZARR; } + return VolumeFileFormat.ZARR; } export async function createVolumeLoader( From 3a3402e793acf911ec362cd72fcd0f1c47b88ac4 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Sat, 13 Jan 2024 00:56:16 -0800 Subject: [PATCH 20/35] allow queue/prefetch options to be passed to loader worker --- public/index.ts | 8 ++++---- src/workers/LoadWorkerHandle.ts | 13 ++++++++++--- src/workers/VolumeLoadWorker.ts | 10 ++++++++-- src/workers/types.ts | 2 ++ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/public/index.ts b/public/index.ts index 295866e5..d4ee8b1b 100644 --- a/public/index.ts +++ b/public/index.ts @@ -86,8 +86,7 @@ const TEST_DATA: Record = { let view3D: View3d; -// const volumeCache = new VolumeCache(CACHE_MAX_SIZE); -const loadWorker = new LoadWorker(CACHE_MAX_SIZE); +const loadWorker = new LoadWorker(CACHE_MAX_SIZE, CONCURRENCY_LIMIT, PREFETCH_CONCURRENCY_LIMIT); const myState: State = { file: "", @@ -1010,7 +1009,6 @@ function createTestVolume() { async function createLoader(data: TestDataSpec): Promise { await loadWorker.onOpen(); - console.log(new Vector2(0, 1)); if (data.type === "opencell") { return new OpenCellLoader(); } @@ -1024,7 +1022,9 @@ async function createLoader(data: TestDataSpec): Promise { } } - return await loadWorker.createLoader(path); + return await loadWorker.createLoader(path, { + fetchOptions: { maxPrefetchDistance: PREFETCH_DISTANCE, maxPrefetchChunks: MAX_PREFETCH_CHUNKS }, + }); } async function loadVolume(loadSpec: LoadSpec, loader: IVolumeLoader): Promise { diff --git a/src/workers/LoadWorkerHandle.ts b/src/workers/LoadWorkerHandle.ts index 83859ea4..06cddfee 100644 --- a/src/workers/LoadWorkerHandle.ts +++ b/src/workers/LoadWorkerHandle.ts @@ -99,9 +99,13 @@ class LoadWorker { private activeLoader: WorkerLoader | undefined = undefined; private activeLoaderId = -1; - constructor(maxCacheSize?: number) { + constructor(maxCacheSize?: number, maxActiveRequests?: number, maxLowPriorityRequests?: number) { this.workerHandle = new SharedLoadWorkerHandle(); - this.openPromise = this.workerHandle.sendMessage(WorkerMsgType.INIT, { maxCacheSize }); + this.openPromise = this.workerHandle.sendMessage(WorkerMsgType.INIT, { + maxCacheSize, + maxActiveRequests, + maxLowPriorityRequests, + }); } onOpen(): Promise { @@ -116,7 +120,10 @@ class LoadWorker { this.activeLoader?.close(); } - async createLoader(path: string | string[], options?: CreateLoaderOptions): Promise { + async createLoader( + path: string | string[], + options?: Omit + ): Promise { // Special case: TIFF loader doesn't work on a worker, has its own workers anyways, and doesn't use cache or queue. const pathString = Array.isArray(path) ? path[0] : path; const fileType = options?.fileType || pathToFileType(pathString); diff --git a/src/workers/VolumeLoadWorker.ts b/src/workers/VolumeLoadWorker.ts index 220a973d..ec48973b 100644 --- a/src/workers/VolumeLoadWorker.ts +++ b/src/workers/VolumeLoadWorker.ts @@ -1,6 +1,8 @@ import VolumeCache from "../VolumeCache"; import { VolumeFileFormat, createVolumeLoader, pathToFileType } from "../loaders"; import { ThreadableVolumeLoader } from "../loaders/IVolumeLoader"; +import RequestQueue from "../utils/RequestQueue"; +import SubscribableRequestQueue from "../utils/SubscribableRequestQueue"; import { WorkerMsgType, WorkerRequest, @@ -12,6 +14,8 @@ import { import { rebuildImageInfo, rebuildLoadSpec } from "./util"; let cache: VolumeCache | undefined = undefined; +let queue: RequestQueue | undefined = undefined; +let subscribableQueue: SubscribableRequestQueue | undefined = undefined; let loader: ThreadableVolumeLoader | undefined = undefined; let initialized = false; let copyOnLoad = false; @@ -19,9 +23,11 @@ let copyOnLoad = false; type MessageHandler = (payload: WorkerRequestPayload) => Promise>; const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { - [WorkerMsgType.INIT]: ({ maxCacheSize }) => { + [WorkerMsgType.INIT]: ({ maxCacheSize, maxActiveRequests, maxLowPriorityRequests }) => { if (!initialized) { cache = new VolumeCache(maxCacheSize); + queue = new RequestQueue(maxActiveRequests, maxLowPriorityRequests); + subscribableQueue = new SubscribableRequestQueue(queue); initialized = true; } return Promise.resolve(); @@ -31,7 +37,7 @@ const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { const pathString = Array.isArray(path) ? path[0] : path; const fileType = options?.fileType || pathToFileType(pathString); copyOnLoad = fileType === VolumeFileFormat.JSON; - loader = await createVolumeLoader(path, { ...options, cache }); + loader = await createVolumeLoader(path, { ...options, cache, queue: subscribableQueue }); return loader !== undefined; }, diff --git a/src/workers/types.ts b/src/workers/types.ts index fcfde640..be4b2bc0 100644 --- a/src/workers/types.ts +++ b/src/workers/types.ts @@ -25,6 +25,8 @@ type WorkerMsgBase = { export type WorkerRequestPayload = { [WorkerMsgType.INIT]: { maxCacheSize?: number; + maxActiveRequests?: number; + maxLowPriorityRequests?: number; }; [WorkerMsgType.CREATE_LOADER]: { path: string | string[]; From b8dda009d581af17db78f90ae7435dc1d6f99a49 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 17 Jan 2024 10:09:20 -0800 Subject: [PATCH 21/35] document `ThreadableVolumeLoader` better --- src/loaders/IVolumeLoader.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/loaders/IVolumeLoader.ts b/src/loaders/IVolumeLoader.ts index 6d07271c..77024f9a 100644 --- a/src/loaders/IVolumeLoader.ts +++ b/src/loaders/IVolumeLoader.ts @@ -35,7 +35,7 @@ export class VolumeDims { */ export type PerChannelCallback = (volume: Volume, channelIndex: number) => void; -export type RawChannelDataCallback = (ch: number, data: Uint8Array, atlasDims?: [number, number]) => void; +export type RawChannelDataCallback = (channelIndex: number, data: Uint8Array, atlasDims?: [number, number]) => void; /** * Loads volume data from a source specified by a `LoadSpec`. @@ -50,10 +50,6 @@ export interface IVolumeLoader { /** * Create an empty `Volume` from a `LoadSpec`, which must be passed to `loadVolumeData` to begin loading. * Optionally pass a callback to respond whenever new channel data is loaded into the volume. - * - * Loaders are allowed to assume that they will only be called on a single data source, in order to cache - * information about that source. Once this method has been called, every subsequent call to it or - * `loadVolumeData` should reference the same source. */ createVolume(loadSpec: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise; @@ -71,10 +67,26 @@ export interface IVolumeLoader { /** Abstract class which allows loaders to accept and return types that are easier to transfer to/from a worker. */ export abstract class ThreadableVolumeLoader implements IVolumeLoader { + /** Unchanged from `IVolumeLoader`. See that interface for details. */ abstract loadDims(loadSpec: LoadSpec): Promise; + /** + * Creates an `ImageInfo` object from a `LoadSpec`, which may be passed to the `Volume` constructor to create an + * empty volume that can accept data loaded with the given `LoadSpec`. + * + * Also returns a new `LoadSpec` that may have been modified from the input `LoadSpec` to reflect the constraints or + * abilities of the loader. This new `LoadSpec` should be used when constructing the `Volume`, _not_ the original. + */ abstract createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]>; + /** + * Begins loading per-channel data for the volume specified by `imageInfo` and `loadSpec`. + * + * Returns a promise that resolves to reflect any modifications to `imageInfo` and/or `loadSpec` that need to be made + * based on this load. Actual loaded channel data is passed to `onData` as it is loaded. Depending on the format, + * the returned array may be in simple 3d dimension order or reflect a 2d atlas. If the latter, the dimensions of the + * atlas (in tiles) are passed as the third argument to `onData`. + */ abstract loadRawChannelData( imageInfo: ImageInfo, loadSpec: LoadSpec, From a298649c65376eb89142279e83ddf09d628190f1 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 17 Jan 2024 10:11:25 -0800 Subject: [PATCH 22/35] small docs update --- src/loaders/IVolumeLoader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loaders/IVolumeLoader.ts b/src/loaders/IVolumeLoader.ts index 77024f9a..4cbeb52b 100644 --- a/src/loaders/IVolumeLoader.ts +++ b/src/loaders/IVolumeLoader.ts @@ -85,7 +85,7 @@ export abstract class ThreadableVolumeLoader implements IVolumeLoader { * Returns a promise that resolves to reflect any modifications to `imageInfo` and/or `loadSpec` that need to be made * based on this load. Actual loaded channel data is passed to `onData` as it is loaded. Depending on the format, * the returned array may be in simple 3d dimension order or reflect a 2d atlas. If the latter, the dimensions of the - * atlas (in tiles) are passed as the third argument to `onData`. + * atlas are passed as the third argument to `onData`. */ abstract loadRawChannelData( imageInfo: ImageInfo, From efdbe648b89878c32c537396aa8a2864d492758d Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 17 Jan 2024 10:23:49 -0800 Subject: [PATCH 23/35] more loader documentation updates --- src/loaders/JsonImageInfoLoader.ts | 6 +++--- src/loaders/TiffLoader.ts | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/loaders/JsonImageInfoLoader.ts b/src/loaders/JsonImageInfoLoader.ts index 1edb2fa7..1930004b 100644 --- a/src/loaders/JsonImageInfoLoader.ts +++ b/src/loaders/JsonImageInfoLoader.ts @@ -185,9 +185,9 @@ class JsonImageInfoLoader extends ThreadableVolumeLoader { /** * load per-channel volume data from a batch of image files containing the volume slices tiled across the images - * @param {Volume} volume * @param {Array.<{name:string, channels:Array.}>} imageArray - * @param {PerChannelCallback} onChannelLoaded Per-channel callback. Called when each channel's atlased volume data is loaded + * @param {RawChannelDataCallback} onData Per-channel callback. Called when each channel's atlased volume data is loaded + * @param {VolumeCache} cache * @example loadVolumeAtlasData([{ * "name": "AICS-10_5_5.ome.tif_atlas_0.png", * "channels": [0, 1, 2] @@ -257,7 +257,7 @@ class JsonImageInfoLoader extends ThreadableVolumeLoader { } } - // done with img, iData, and canvas now. + // done with `iData` and `canvas` now. for (let ch = 0; ch < Math.min(image.channels.length, 4); ++ch) { const chindex = image.channels[ch]; diff --git a/src/loaders/TiffLoader.ts b/src/loaders/TiffLoader.ts index 89f36c7b..c575ff3c 100644 --- a/src/loaders/TiffLoader.ts +++ b/src/loaders/TiffLoader.ts @@ -61,6 +61,8 @@ function getOMEDims(imageEl: Element): OMEDims { const getBytesPerSample = (type: string): number => (type === "uint8" ? 1 : type === "uint16" ? 2 : 4); +// Despite the class `TiffLoader` extends, this loader is not threadable, since geotiff internally uses features that +// aren't available on workers. It uses its own specialized workers anyways. class TiffLoader extends ThreadableVolumeLoader { url: string; dims?: OMEDims; @@ -171,7 +173,6 @@ class TiffLoader extends ThreadableVolumeLoader { const u8 = e.data.data; const channel = e.data.channel; onData(channel, u8); - // make up a unique name? or have caller pass this in? worker.terminate(); }; worker.onerror = (e) => { From 2c470884074f2c16452371acafd518160522d9b8 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 17 Jan 2024 10:31:35 -0800 Subject: [PATCH 24/35] bit of documentation in worker utils --- src/workers/util.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/workers/util.ts b/src/workers/util.ts index 051e527e..8417f0ff 100644 --- a/src/workers/util.ts +++ b/src/workers/util.ts @@ -2,6 +2,7 @@ import { Box3, Vector2, Vector3 } from "three"; import { LoadSpec } from "../loaders/IVolumeLoader"; import { ImageInfo } from "../Volume"; +/** Recreates a `LoadSpec` that has just been sent to/from a worker to restore three.js object prototypes */ export function rebuildLoadSpec(spec: LoadSpec): LoadSpec { return { ...spec, @@ -9,6 +10,7 @@ export function rebuildLoadSpec(spec: LoadSpec): LoadSpec { }; } +/** Recreates an `ImageInfo` that has just been sent to/from a worker to restore three.js object prototypes */ export function rebuildImageInfo(imageInfo: ImageInfo): ImageInfo { return { ...imageInfo, From 1e075c589e29622b7bfd5db2182f6e30066fcae9 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 17 Jan 2024 16:23:38 -0800 Subject: [PATCH 25/35] document `SubscribableRequestQueue` constructor overload --- src/utils/SubscribableRequestQueue.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/SubscribableRequestQueue.ts b/src/utils/SubscribableRequestQueue.ts index 4b120c2c..20d7555b 100644 --- a/src/utils/SubscribableRequestQueue.ts +++ b/src/utils/SubscribableRequestQueue.ts @@ -27,6 +27,10 @@ export default class SubscribableRequestQueue { /** Map from "inner" request (managed by `queue`) to "outer" promises generated per-subscriber. */ private requests: Map; + /** + * Since `SubscribableRequestQueue` wraps `RequestQueue`, its constructor may either take the same arguments as the + * `RequestQueue` constructor and create a new `RequestQueue`, or it may take an existing `RequestQueue` to wrap. + */ constructor(maxActiveRequests?: number, maxLowPriorityRequests?: number); constructor(inner: RequestQueue); constructor(maxActiveRequests?: number | RequestQueue, maxLowPriorityRequests?: number) { From 15a8a6fed097d86d12c0a2fde1b2fd40aa8a0cdb Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 23 Jan 2024 15:59:41 -0800 Subject: [PATCH 26/35] export worker loader and loader worker --- src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.ts b/src/index.ts index 57eafed8..2c5a3315 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { LoadSpec } from "./loaders/IVolumeLoader"; import { OMEZarrLoader } from "./loaders/OmeZarrLoader"; import { JsonImageInfoLoader } from "./loaders/JsonImageInfoLoader"; import { TiffLoader } from "./loaders/TiffLoader"; +import LoadWorker from "./workers/LoadWorkerHandle"; import { Light, AREA_LIGHT, SKY_LIGHT } from "./Light"; @@ -20,6 +21,7 @@ export type { ControlPoint, Lut } from "./Histogram"; export type { CreateLoaderOptions } from "./loaders"; export type { IVolumeLoader, PerChannelCallback } from "./loaders/IVolumeLoader"; export type { ZarrLoaderFetchOptions } from "./loaders/OmeZarrLoader"; +export type { WorkerLoader } from "./workers/LoadWorkerHandle"; export { Histogram, View3d, @@ -32,6 +34,7 @@ export { OMEZarrLoader, JsonImageInfoLoader, TiffLoader, + LoadWorker, VolumeFileFormat, createVolumeLoader, Channel, From b8ac1a8af5843346766296d53357d34a33a07aa1 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 23 Jan 2024 16:06:05 -0800 Subject: [PATCH 27/35] rename: `WorkerResponseKind` -> `WorkerResponseResult` --- src/workers/LoadWorkerHandle.ts | 6 +++--- src/workers/VolumeLoadWorker.ts | 8 ++++---- src/workers/types.ts | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/workers/LoadWorkerHandle.ts b/src/workers/LoadWorkerHandle.ts index 06cddfee..7be2ba84 100644 --- a/src/workers/LoadWorkerHandle.ts +++ b/src/workers/LoadWorkerHandle.ts @@ -9,7 +9,7 @@ import { WorkerResponse, WorkerResponsePayload, ChannelLoadEvent, - WorkerResponseKind, + WorkerResponseResult, } from "./types"; import { rebuildImageInfo, rebuildLoadSpec } from "./util"; @@ -70,7 +70,7 @@ class SharedLoadWorkerHandle { } private receiveMessage({ data }: MessageEvent>): void { - if (data.responseKind === WorkerResponseKind.EVENT) { + if (data.responseKind === WorkerResponseResult.EVENT) { this.onChannelData?.(data); } else { const prom = this.pendingRequests[data.msgId]; @@ -82,7 +82,7 @@ class SharedLoadWorkerHandle { throw new Error(`Received response of type ${data.type} for message of type ${prom.type}`); } - if (data.responseKind === WorkerResponseKind.ERROR) { + if (data.responseKind === WorkerResponseResult.ERROR) { prom.reject(data.payload); } else { prom.resolve(data.payload); diff --git a/src/workers/VolumeLoadWorker.ts b/src/workers/VolumeLoadWorker.ts index ec48973b..3dbdcf20 100644 --- a/src/workers/VolumeLoadWorker.ts +++ b/src/workers/VolumeLoadWorker.ts @@ -8,7 +8,7 @@ import { WorkerRequest, WorkerRequestPayload, WorkerResponse, - WorkerResponseKind, + WorkerResponseResult, WorkerResponsePayload, } from "./types"; import { rebuildImageInfo, rebuildLoadSpec } from "./util"; @@ -66,7 +66,7 @@ const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { rebuildLoadSpec(loadSpec), (channelIndex, data, atlasDims) => { const message: WorkerResponse = { - responseKind: WorkerResponseKind.EVENT, + responseKind: WorkerResponseResult.EVENT, loaderId, loadId, channelIndex, @@ -85,9 +85,9 @@ self.onmessage = async ({ data }: MessageEvent = WorkerMsgBase>; export type WorkerResponse = - | ({ responseKind: WorkerResponseKind.SUCCESS } & WorkerMsgBase>) - | ({ responseKind: WorkerResponseKind.ERROR } & WorkerMsgBase) - | ({ responseKind: WorkerResponseKind.EVENT } & ChannelLoadEvent); + | ({ responseKind: WorkerResponseResult.SUCCESS } & WorkerMsgBase>) + | ({ responseKind: WorkerResponseResult.ERROR } & WorkerMsgBase) + | ({ responseKind: WorkerResponseResult.EVENT } & ChannelLoadEvent); From 7b96f5f727a3aca97c1e80d2b7c06c4091b8b08c Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 24 Jan 2024 09:59:59 -0800 Subject: [PATCH 28/35] document loader worker message types --- src/workers/types.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/workers/types.ts b/src/workers/types.ts index 48b45447..64213524 100644 --- a/src/workers/types.ts +++ b/src/workers/types.ts @@ -2,6 +2,7 @@ import { ImageInfo } from "../Volume"; import { CreateLoaderOptions } from "../loaders"; import { LoadSpec, VolumeDims } from "../loaders/IVolumeLoader"; +/** The types of requests that can be made to the worker. Mostly corresponds to methods on `IVolumeLoader`. */ export const enum WorkerMsgType { INIT, CREATE_LOADER, @@ -10,18 +11,21 @@ export const enum WorkerMsgType { LOAD_VOLUME_DATA, } +/** The kind of response a worker can return - `SUCCESS`, `ERROR`, or `EVENT`. */ export const enum WorkerResponseResult { SUCCESS, ERROR, EVENT, } +/** All messages to/from a worker carry a `msgId`, a `type`, and a `payload` (whose type is determined by `type`). */ type WorkerMsgBase = { msgId: number; type: T; payload: P; }; +/** Maps each `WorkerMsgType` to the type of the payload of requests of that type. */ export type WorkerRequestPayload = { [WorkerMsgType.INIT]: { maxCacheSize?: number; @@ -42,6 +46,7 @@ export type WorkerRequestPayload = { }; }[T]; +/** Maps each `WorkerMsgType` to the type of the payload of responses of that type. */ export type WorkerResponsePayload = { [WorkerMsgType.INIT]: void; [WorkerMsgType.CREATE_LOADER]: boolean; @@ -50,6 +55,7 @@ export type WorkerResponsePayload = { [WorkerMsgType.LOAD_VOLUME_DATA]: [ImageInfo | undefined, LoadSpec | undefined]; }[T]; +/** Currently the only event a loader can produce is a `ChannelLoadEvent` when a single channel loads. */ export type ChannelLoadEvent = { loaderId: number; loadId: number; @@ -58,7 +64,9 @@ export type ChannelLoadEvent = { atlasDims?: [number, number]; }; +/** All valid types of worker requests, with some `WorkerMsgType` and a matching payload type. */ export type WorkerRequest = WorkerMsgBase>; +/** All valid types of worker responses: `SUCCESS` with a matching payload, `ERROR` with a message, or an `EVENT`. */ export type WorkerResponse = | ({ responseKind: WorkerResponseResult.SUCCESS } & WorkerMsgBase>) | ({ responseKind: WorkerResponseResult.ERROR } & WorkerMsgBase) From 6550adc82ae816129463e6aa9373d5991f0348c6 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 25 Jan 2024 12:17:15 -0800 Subject: [PATCH 29/35] don't bother checking load worker if loader is OpenCell --- public/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/index.ts b/public/index.ts index de806946..49c0a05a 100644 --- a/public/index.ts +++ b/public/index.ts @@ -1008,11 +1008,12 @@ function createTestVolume() { } async function createLoader(data: TestDataSpec): Promise { - await loadWorker.onOpen(); if (data.type === "opencell") { return new OpenCellLoader(); } + await loadWorker.onOpen(); + let path: string | string[] = data.url; if (data.type === VolumeFileFormat.JSON) { path = []; From 1e64f83729937997afda15f8b762d9abb3db1f52 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 25 Jan 2024 15:12:01 -0800 Subject: [PATCH 30/35] rename: `LoadWorker` -> `VolumeLoaderContext` --- public/index.ts | 4 ++-- src/index.ts | 4 ++-- src/workers/LoadWorkerHandle.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/index.ts b/public/index.ts index 49c0a05a..596c3c8d 100644 --- a/public/index.ts +++ b/public/index.ts @@ -21,7 +21,7 @@ import { import { OpenCellLoader } from "../src/loaders/OpenCellLoader"; import { State, TestDataSpec } from "./types"; import { getDefaultImageInfo } from "../src/Volume"; -import LoadWorker from "../src/workers/LoadWorkerHandle"; +import VolumeLoaderContext from "../src/workers/LoadWorkerHandle"; const CACHE_MAX_SIZE = 1_000_000_000; const CONCURRENCY_LIMIT = 8; @@ -86,7 +86,7 @@ const TEST_DATA: Record = { let view3D: View3d; -const loadWorker = new LoadWorker(CACHE_MAX_SIZE, CONCURRENCY_LIMIT, PREFETCH_CONCURRENCY_LIMIT); +const loadWorker = new VolumeLoaderContext(CACHE_MAX_SIZE, CONCURRENCY_LIMIT, PREFETCH_CONCURRENCY_LIMIT); const myState: State = { file: "", diff --git a/src/index.ts b/src/index.ts index 2c5a3315..7ab95e9b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ import { LoadSpec } from "./loaders/IVolumeLoader"; import { OMEZarrLoader } from "./loaders/OmeZarrLoader"; import { JsonImageInfoLoader } from "./loaders/JsonImageInfoLoader"; import { TiffLoader } from "./loaders/TiffLoader"; -import LoadWorker from "./workers/LoadWorkerHandle"; +import VolumeLoaderContext from "./workers/LoadWorkerHandle"; import { Light, AREA_LIGHT, SKY_LIGHT } from "./Light"; @@ -34,7 +34,7 @@ export { OMEZarrLoader, JsonImageInfoLoader, TiffLoader, - LoadWorker, + VolumeLoaderContext, VolumeFileFormat, createVolumeLoader, Channel, diff --git a/src/workers/LoadWorkerHandle.ts b/src/workers/LoadWorkerHandle.ts index 7be2ba84..33083a50 100644 --- a/src/workers/LoadWorkerHandle.ts +++ b/src/workers/LoadWorkerHandle.ts @@ -92,7 +92,7 @@ class SharedLoadWorkerHandle { } } -class LoadWorker { +class VolumeLoaderContext { private workerHandle: SharedLoadWorkerHandle; private openPromise: Promise; @@ -203,5 +203,5 @@ class WorkerLoader extends ThreadableVolumeLoader { } } -export default LoadWorker; +export default VolumeLoaderContext; export type { WorkerLoader }; From 3ec2a78e119761f559d79bb473c534fab519f471 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 25 Jan 2024 15:20:45 -0800 Subject: [PATCH 31/35] remove unnecessary `await` on JSON data load --- src/loaders/JsonImageInfoLoader.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/loaders/JsonImageInfoLoader.ts b/src/loaders/JsonImageInfoLoader.ts index 1930004b..cabe5cf0 100644 --- a/src/loaders/JsonImageInfoLoader.ts +++ b/src/loaders/JsonImageInfoLoader.ts @@ -199,12 +199,12 @@ class JsonImageInfoLoader extends ThreadableVolumeLoader { * "channels": [6, 7, 8] * }], mycallback); */ - static async loadVolumeAtlasData( + static loadVolumeAtlasData( imageArray: PackedChannelsImage[], onData: RawChannelDataCallback, cache?: VolumeCache - ): Promise { - const loadPromises = imageArray.map(async (image) => { + ): void { + imageArray.forEach(async (image) => { // Because the data is fetched such that one fetch returns a whole batch, // if any in batch is cached then they all should be. So if any in batch is NOT cached, // then we will have to do a batch request. This logic works both ways because it's all or nothing. @@ -266,8 +266,6 @@ class JsonImageInfoLoader extends ThreadableVolumeLoader { onData(chindex, channelsBits[ch], [bitmap.width, bitmap.height]); } }); - - await Promise.all(loadPromises); } } From 5c462d25bc9d6dfa4ba9d157359f016878b2ee5a Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 25 Jan 2024 16:18:31 -0800 Subject: [PATCH 32/35] add a bunch of docs to LoadWorkerHandle.ts --- src/workers/LoadWorkerHandle.ts | 49 +++++++++++++++++++++++++++++---- src/workers/VolumeLoadWorker.ts | 6 ++-- src/workers/types.ts | 6 ++-- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/src/workers/LoadWorkerHandle.ts b/src/workers/LoadWorkerHandle.ts index 33083a50..4e5ede93 100644 --- a/src/workers/LoadWorkerHandle.ts +++ b/src/workers/LoadWorkerHandle.ts @@ -20,9 +20,15 @@ type StoredPromise = { }; /** - * A handle that holds the worker and lets us interact with it through async calls and events rather than messages. - * This is separate from `LoadWorker` so that `sendMessage` and `onChannelData` can be shared with `WorkerLoader`s - * without leaking the API outside this file. + * A handle that holds the worker and manages requests and messages to/from it. + * + * `VolumeLoaderContext` and every `LoaderWorker` it creates all hold references to one `SharedLoadWorkerHandle`. + * They use it to interact with the worker via `sendMessage`, which speaks the protocol defined in ./types.ts and + * converts messages received from the worker into resolved `Promise`s and called callbacks. + * + * This class exists to represent that access to the worker is shared between `VolumeLoaderContext` and any + * `LoaderWorker`s. This is as opposed to implementing `sendMessage` directly on `VolumeLoaderContext`, where making + * the method public to `LoaderWorker`s would require also allowing access to users of the class. */ class SharedLoadWorkerHandle { private worker: Worker; @@ -52,11 +58,16 @@ class SharedLoadWorkerHandle { return this.workerOpen; } + /** Close the worker. */ close(): void { this.worker.terminate(); this.workerOpen = false; } + /** + * Send a message of type `T` to the worker. + * Returns a `Promise` that resolves with the worker's response, or rejects with an error message. + */ sendMessage(type: T, payload: WorkerRequestPayload): Promise> { let msgId = -1; const promise = new Promise>((resolve, reject) => { @@ -69,8 +80,9 @@ class SharedLoadWorkerHandle { return promise; } + /** */ private receiveMessage({ data }: MessageEvent>): void { - if (data.responseKind === WorkerResponseResult.EVENT) { + if (data.responseResult === WorkerResponseResult.EVENT) { this.onChannelData?.(data); } else { const prom = this.pendingRequests[data.msgId]; @@ -82,7 +94,7 @@ class SharedLoadWorkerHandle { throw new Error(`Received response of type ${data.type} for message of type ${prom.type}`); } - if (data.responseKind === WorkerResponseResult.ERROR) { + if (data.responseResult === WorkerResponseResult.ERROR) { prom.reject(data.payload); } else { prom.resolve(data.payload); @@ -92,6 +104,20 @@ class SharedLoadWorkerHandle { } } +/** + * A context in which volume loaders can be run, which allows loading to run on a WebWorker (where it won't block + * rendering or UI updates) and loaders to share a single `VolumeCache` and `RequestQueue`. + * + * ### To use: + * 1. Create a `VolumeLoaderContext` with the desired cache and queue configuration. + * 2. Before creating a loader, await `onOpen` to ensure the worker is ready. + * 3. Create a loader with `createLoader`. This accepts nearly the same arguments as `createVolumeLoader`, but without + * options to directly link to a cache or queue (the loader will always be linked to the context's shared instances + * of these if possible). + * + * The returned `WorkerLoader` can be used like any other `IVolumeLoader` and acts as a handle to the actual loader + * running on the worker. + */ class VolumeLoaderContext { private workerHandle: SharedLoadWorkerHandle; private openPromise: Promise; @@ -108,6 +134,7 @@ class VolumeLoaderContext { }); } + /** Returns a `Promise` that resolves when the worker is ready. `await` it before trying to create a loader. */ onOpen(): Promise { if (!this.workerHandle.isOpen) { return Promise.reject("Worker is closed"); @@ -115,11 +142,17 @@ class VolumeLoaderContext { return this.openPromise; } + /** Close this context, its worker, and any active loaders. */ close(): void { this.workerHandle.close(); this.activeLoader?.close(); } + /** + * Create a new loader within this context. This loader will share the context's `VolumeCache` and `RequestQueue`. + * + * This works just like `createVolumeLoader`. A file format may be provided, or it may be inferred from the URL. + */ async createLoader( path: string | string[], options?: Omit @@ -143,6 +176,11 @@ class VolumeLoaderContext { } } +/** + * A handle to an instance of `IVolumeLoader` (technically, a `ThreadableVolumeLoader`) running on a WebWorker. + * + * Created with `VolumeLoaderContext.createLoader`. See its documentation for more. + */ class WorkerLoader extends ThreadableVolumeLoader { private isOpen = true; private currentLoadId = -1; @@ -159,6 +197,7 @@ class WorkerLoader extends ThreadableVolumeLoader { } } + /** Close and permanently invalidate this loader. */ close(): void { this.isOpen = false; } diff --git a/src/workers/VolumeLoadWorker.ts b/src/workers/VolumeLoadWorker.ts index 3dbdcf20..5d0f5bee 100644 --- a/src/workers/VolumeLoadWorker.ts +++ b/src/workers/VolumeLoadWorker.ts @@ -66,7 +66,7 @@ const messageHandlers: { [T in WorkerMsgType]: MessageHandler } = { rebuildLoadSpec(loadSpec), (channelIndex, data, atlasDims) => { const message: WorkerResponse = { - responseKind: WorkerResponseResult.EVENT, + responseResult: WorkerResponseResult.EVENT, loaderId, loadId, channelIndex, @@ -85,9 +85,9 @@ self.onmessage = async ({ data }: MessageEvent = WorkerMsgBase>; /** All valid types of worker responses: `SUCCESS` with a matching payload, `ERROR` with a message, or an `EVENT`. */ export type WorkerResponse = - | ({ responseKind: WorkerResponseResult.SUCCESS } & WorkerMsgBase>) - | ({ responseKind: WorkerResponseResult.ERROR } & WorkerMsgBase) - | ({ responseKind: WorkerResponseResult.EVENT } & ChannelLoadEvent); + | ({ responseResult: WorkerResponseResult.SUCCESS } & WorkerMsgBase>) + | ({ responseResult: WorkerResponseResult.ERROR } & WorkerMsgBase) + | ({ responseResult: WorkerResponseResult.EVENT } & ChannelLoadEvent); From 3b153c79737e6df4525e4b21ee8cd68d7687ec8b Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 25 Jan 2024 16:26:40 -0800 Subject: [PATCH 33/35] rename `loadWorker` -> `loaderContext` in test app --- public/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/index.ts b/public/index.ts index 596c3c8d..d7939999 100644 --- a/public/index.ts +++ b/public/index.ts @@ -86,7 +86,7 @@ const TEST_DATA: Record = { let view3D: View3d; -const loadWorker = new VolumeLoaderContext(CACHE_MAX_SIZE, CONCURRENCY_LIMIT, PREFETCH_CONCURRENCY_LIMIT); +const loaderContext = new VolumeLoaderContext(CACHE_MAX_SIZE, CONCURRENCY_LIMIT, PREFETCH_CONCURRENCY_LIMIT); const myState: State = { file: "", @@ -1012,7 +1012,7 @@ async function createLoader(data: TestDataSpec): Promise { return new OpenCellLoader(); } - await loadWorker.onOpen(); + await loaderContext.onOpen(); let path: string | string[] = data.url; if (data.type === VolumeFileFormat.JSON) { @@ -1023,7 +1023,7 @@ async function createLoader(data: TestDataSpec): Promise { } } - return await loadWorker.createLoader(path, { + return await loaderContext.createLoader(path, { fetchOptions: { maxPrefetchDistance: PREFETCH_DISTANCE, maxPrefetchChunks: MAX_PREFETCH_CHUNKS }, }); } From d148cd7af845f7b70efbd12f2f4e690a266b19e4 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 25 Jan 2024 16:27:46 -0800 Subject: [PATCH 34/35] remove unused import --- public/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.ts b/public/index.ts index d7939999..08d3c9c0 100644 --- a/public/index.ts +++ b/public/index.ts @@ -1,5 +1,5 @@ import "regenerator-runtime/runtime"; -import { Vector2, Vector3, Vector4 } from "three"; +import { Vector2, Vector3 } from "three"; import * as dat from "dat.gui"; import { From e39175af13184970423f234f8b4489fdddf754ac Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 25 Jan 2024 17:36:19 -0800 Subject: [PATCH 35/35] replace `[ImageInfo, LoadSpec]` tuple with record type --- src/loaders/IVolumeLoader.ts | 29 ++++++++++++++-------- src/loaders/JsonImageInfoLoader.ts | 18 +++++++++----- src/loaders/OmeZarrLoader.ts | 16 ++++++++---- src/loaders/OpenCellLoader.ts | 16 ++++++++---- src/loaders/TiffLoader.ts | 16 ++++++++---- src/workers/LoadWorkerHandle.ts | 39 +++++++++++++++++++++--------- src/workers/types.ts | 6 ++--- 7 files changed, 94 insertions(+), 46 deletions(-) diff --git a/src/loaders/IVolumeLoader.ts b/src/loaders/IVolumeLoader.ts index 4cbeb52b..a35b6ed9 100644 --- a/src/loaders/IVolumeLoader.ts +++ b/src/loaders/IVolumeLoader.ts @@ -27,6 +27,11 @@ export class VolumeDims { dataType = "uint8"; } +export type LoadedVolumeInfo = { + imageInfo: ImageInfo; + loadSpec: LoadSpec; +}; + /** * @callback PerChannelCallback * @param {string} imageurl @@ -77,7 +82,7 @@ export abstract class ThreadableVolumeLoader implements IVolumeLoader { * Also returns a new `LoadSpec` that may have been modified from the input `LoadSpec` to reflect the constraints or * abilities of the loader. This new `LoadSpec` should be used when constructing the `Volume`, _not_ the original. */ - abstract createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]>; + abstract createImageInfo(loadSpec: LoadSpec): Promise; /** * Begins loading per-channel data for the volume specified by `imageInfo` and `loadSpec`. @@ -91,17 +96,21 @@ export abstract class ThreadableVolumeLoader implements IVolumeLoader { imageInfo: ImageInfo, loadSpec: LoadSpec, onData: RawChannelDataCallback - ): Promise<[ImageInfo | undefined, LoadSpec | undefined]>; + ): Promise>; async createVolume(loadSpec: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { - const [imageInfo, adjustedSpec] = await this.createImageInfo(loadSpec); - const vol = new Volume(imageInfo, adjustedSpec, this); + const { imageInfo, loadSpec: adjustedLoadSpec } = await this.createImageInfo(loadSpec); + const vol = new Volume(imageInfo, adjustedLoadSpec, this); vol.channelLoadCallback = onChannelLoaded; vol.imageMetadata = buildDefaultMetadata(imageInfo); return vol; } - async loadVolumeData(volume: Volume, loadSpec?: LoadSpec, onChannelLoaded?: PerChannelCallback): Promise { + async loadVolumeData( + volume: Volume, + loadSpecOverride?: LoadSpec, + onChannelLoaded?: PerChannelCallback + ): Promise { const onChannelData: RawChannelDataCallback = (channelIndex, data, atlasDims) => { if (atlasDims) { volume.setChannelDataFromAtlas(channelIndex, data, atlasDims[0], atlasDims[1]); @@ -111,13 +120,13 @@ export abstract class ThreadableVolumeLoader implements IVolumeLoader { onChannelLoaded?.(volume, channelIndex); }; - const spec = { ...loadSpec, ...volume.loadSpec }; - const [adjustedImageInfo, adjustedLoadSpec] = await this.loadRawChannelData(volume.imageInfo, spec, onChannelData); + const spec = { ...loadSpecOverride, ...volume.loadSpec }; + const { imageInfo, loadSpec } = await this.loadRawChannelData(volume.imageInfo, spec, onChannelData); - if (adjustedImageInfo) { - volume.imageInfo = adjustedImageInfo; + if (imageInfo) { + volume.imageInfo = imageInfo; volume.updateDimensions(); } - volume.loadSpec = { ...adjustedLoadSpec, ...spec }; + volume.loadSpec = { ...loadSpec, ...spec }; } } diff --git a/src/loaders/JsonImageInfoLoader.ts b/src/loaders/JsonImageInfoLoader.ts index cabe5cf0..bd72bcae 100644 --- a/src/loaders/JsonImageInfoLoader.ts +++ b/src/loaders/JsonImageInfoLoader.ts @@ -1,6 +1,12 @@ import { Box3, Vector2, Vector3 } from "three"; -import { ThreadableVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; +import { + ThreadableVolumeLoader, + LoadSpec, + RawChannelDataCallback, + VolumeDims, + LoadedVolumeInfo, +} from "./IVolumeLoader"; import { ImageInfo } from "../Volume"; import VolumeCache from "../VolumeCache"; @@ -136,16 +142,16 @@ class JsonImageInfoLoader extends ThreadableVolumeLoader { return [d]; } - async createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]> { + async createImageInfo(loadSpec: LoadSpec): Promise { const jsonInfo = await this.getJsonImageInfo(loadSpec.time); - return [convertImageInfo(jsonInfo), loadSpec]; + return { imageInfo: convertImageInfo(jsonInfo), loadSpec }; } async loadRawChannelData( imageInfo: ImageInfo, loadSpec: LoadSpec, onData: RawChannelDataCallback - ): Promise<[undefined, LoadSpec | undefined]> { + ): Promise<{ loadSpec?: LoadSpec }> { // if you need to adjust image paths prior to download, // now is the time to do it. // Try to figure out the urlPrefix from the LoadSpec. @@ -154,7 +160,7 @@ class JsonImageInfoLoader extends ThreadableVolumeLoader { let images = jsonInfo?.images; if (!images) { - return [undefined, undefined]; + return {}; } const requestedChannels = loadSpec.channels; @@ -180,7 +186,7 @@ class JsonImageInfoLoader extends ThreadableVolumeLoader { // include all channels in any loaded images channels: images.flatMap(({ channels }) => channels), }; - return [undefined, adjustedLoadSpec]; + return { loadSpec: adjustedLoadSpec }; } /** diff --git a/src/loaders/OmeZarrLoader.ts b/src/loaders/OmeZarrLoader.ts index 7554b22f..d7c0d27f 100644 --- a/src/loaders/OmeZarrLoader.ts +++ b/src/loaders/OmeZarrLoader.ts @@ -10,7 +10,13 @@ import { FetchStore } from "zarrita"; import { ImageInfo } from "../Volume"; import VolumeCache from "../VolumeCache"; import SubscribableRequestQueue from "../utils/SubscribableRequestQueue"; -import { ThreadableVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; +import { + ThreadableVolumeLoader, + LoadSpec, + RawChannelDataCallback, + VolumeDims, + LoadedVolumeInfo, +} from "./IVolumeLoader"; import { composeSubregion, computePackedAtlasDims, @@ -282,7 +288,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader { return Promise.resolve(result); } - createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]> { + createImageInfo(loadSpec: LoadSpec): Promise { const [t, c, z, y, x] = this.axesTCZYX; const hasT = t > -1; const hasC = c > -1; @@ -349,7 +355,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader { subregion: new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)), }; - return Promise.resolve([imgdata, fullExtentLoadSpec]); + return Promise.resolve({ imageInfo: imgdata, loadSpec: fullExtentLoadSpec }); } private async prefetchChunk(basePath: string, coords: TCZYX, subscriber: SubscriberId): Promise { @@ -413,7 +419,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader { imageInfo: ImageInfo, loadSpec: LoadSpec, onData: RawChannelDataCallback - ): Promise<[ImageInfo, undefined]> { + ): Promise<{ imageInfo: ImageInfo }> { const maxExtent = this.maxExtent ?? new Box3(new Vector3(0, 0, 0), new Vector3(1, 1, 1)); const [z, y, x] = this.axesTCZYX.slice(2); const subregion = composeSubregion(loadSpec.subregion, maxExtent); @@ -487,7 +493,7 @@ class OMEZarrLoader extends ThreadableVolumeLoader { this.requestQueue.removeSubscriber(subscriber, CHUNK_REQUEST_CANCEL_REASON); setTimeout(() => this.beginPrefetch(keys, level), 1000); }); - return Promise.resolve([updatedImageInfo, undefined]); + return Promise.resolve({ imageInfo: updatedImageInfo }); } } diff --git a/src/loaders/OpenCellLoader.ts b/src/loaders/OpenCellLoader.ts index a9f587eb..36780c82 100644 --- a/src/loaders/OpenCellLoader.ts +++ b/src/loaders/OpenCellLoader.ts @@ -1,6 +1,12 @@ import { Vector2, Vector3 } from "three"; -import { ThreadableVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; +import { + ThreadableVolumeLoader, + LoadSpec, + RawChannelDataCallback, + VolumeDims, + LoadedVolumeInfo, +} from "./IVolumeLoader"; import { ImageInfo } from "../Volume"; import { JsonImageInfoLoader } from "./JsonImageInfoLoader"; @@ -14,7 +20,7 @@ class OpenCellLoader extends ThreadableVolumeLoader { return [d]; } - async createImageInfo(_loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]> { + async createImageInfo(_loadSpec: LoadSpec): Promise { const numChannels = 2; // we know these are standardized to 600x600, two channels, one channel per jpg. @@ -48,14 +54,14 @@ class OpenCellLoader extends ThreadableVolumeLoader { }; // This loader uses no fields from `LoadSpec`. Initialize volume with defaults. - return [imgdata, new LoadSpec()]; + return { imageInfo: imgdata, loadSpec: new LoadSpec() }; } loadRawChannelData( imageInfo: ImageInfo, _loadSpec: LoadSpec, onData: RawChannelDataCallback - ): Promise<[undefined, undefined]> { + ): Promise> { // HQTILE or LQTILE // make a json metadata dict for the two channels: const urls = [ @@ -72,7 +78,7 @@ class OpenCellLoader extends ThreadableVolumeLoader { const w = imageInfo.atlasTileDims.x * imageInfo.volumeSize.x; const h = imageInfo.atlasTileDims.y * imageInfo.volumeSize.y; JsonImageInfoLoader.loadVolumeAtlasData(urls, (ch, data) => onData(ch, data, [w, h])); - return Promise.resolve([undefined, undefined]); + return Promise.resolve({}); } } diff --git a/src/loaders/TiffLoader.ts b/src/loaders/TiffLoader.ts index c575ff3c..20b86180 100644 --- a/src/loaders/TiffLoader.ts +++ b/src/loaders/TiffLoader.ts @@ -1,7 +1,13 @@ import { fromUrl } from "geotiff"; import { Vector3 } from "three"; -import { ThreadableVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "./IVolumeLoader"; +import { + ThreadableVolumeLoader, + LoadSpec, + RawChannelDataCallback, + VolumeDims, + LoadedVolumeInfo, +} from "./IVolumeLoader"; import { computePackedAtlasDims } from "./VolumeLoaderUtils"; import { ImageInfo } from "../Volume"; @@ -100,7 +106,7 @@ class TiffLoader extends ThreadableVolumeLoader { return [d]; } - async createImageInfo(_loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]> { + async createImageInfo(_loadSpec: LoadSpec): Promise { const dims = await this.loadOmeDims(); // compare with sizex, sizey //const width = image.getWidth(); @@ -144,14 +150,14 @@ class TiffLoader extends ThreadableVolumeLoader { }; // This loader uses no fields from `LoadSpec`. Initialize volume with defaults. - return [imgdata, new LoadSpec()]; + return { imageInfo: imgdata, loadSpec: new LoadSpec() }; } async loadRawChannelData( imageInfo: ImageInfo, _loadSpec: LoadSpec, onData: RawChannelDataCallback - ): Promise<[undefined, undefined]> { + ): Promise> { const dims = await this.loadOmeDims(); // do each channel on a worker? @@ -181,7 +187,7 @@ class TiffLoader extends ThreadableVolumeLoader { worker.postMessage(params); } - return [undefined, undefined]; + return {}; } } diff --git a/src/workers/LoadWorkerHandle.ts b/src/workers/LoadWorkerHandle.ts index 4e5ede93..b347978f 100644 --- a/src/workers/LoadWorkerHandle.ts +++ b/src/workers/LoadWorkerHandle.ts @@ -1,6 +1,12 @@ import { ImageInfo } from "../Volume"; import { CreateLoaderOptions, VolumeFileFormat, pathToFileType } from "../loaders"; -import { ThreadableVolumeLoader, LoadSpec, RawChannelDataCallback, VolumeDims } from "../loaders/IVolumeLoader"; +import { + ThreadableVolumeLoader, + LoadSpec, + RawChannelDataCallback, + VolumeDims, + LoadedVolumeInfo, +} from "../loaders/IVolumeLoader"; import { TiffLoader } from "../loaders/TiffLoader"; import { WorkerMsgType, @@ -207,30 +213,39 @@ class WorkerLoader extends ThreadableVolumeLoader { return this.workerHandle.sendMessage(WorkerMsgType.LOAD_DIMS, loadSpec); } - async createImageInfo(loadSpec: LoadSpec): Promise<[ImageInfo, LoadSpec]> { + async createImageInfo(loadSpec: LoadSpec): Promise { this.checkIsOpen(); - const [imageInfo, adjustedLoadSpec] = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_VOLUME, loadSpec); - return [rebuildImageInfo(imageInfo), rebuildLoadSpec(adjustedLoadSpec)]; + const { imageInfo, loadSpec: adjustedLoadSpec } = await this.workerHandle.sendMessage( + WorkerMsgType.CREATE_VOLUME, + loadSpec + ); + return { imageInfo: rebuildImageInfo(imageInfo), loadSpec: rebuildLoadSpec(adjustedLoadSpec) }; } async loadRawChannelData( imageInfo: ImageInfo, loadSpec: LoadSpec, onData: RawChannelDataCallback - ): Promise<[ImageInfo | undefined, LoadSpec | undefined]> { + ): Promise> { this.checkIsOpen(); this.currentLoadCallback = onData; this.currentLoadId += 1; - const [newImageInfo, newLoadSpec] = await this.workerHandle.sendMessage(WorkerMsgType.LOAD_VOLUME_DATA, { - imageInfo, - loadSpec, - loaderId: this.loaderId, - loadId: this.currentLoadId, - }); + const { imageInfo: newImageInfo, loadSpec: newLoadSpec } = await this.workerHandle.sendMessage( + WorkerMsgType.LOAD_VOLUME_DATA, + { + imageInfo, + loadSpec, + loaderId: this.loaderId, + loadId: this.currentLoadId, + } + ); - return [newImageInfo && rebuildImageInfo(newImageInfo), newLoadSpec && rebuildLoadSpec(newLoadSpec)]; + return { + imageInfo: newImageInfo && rebuildImageInfo(newImageInfo), + loadSpec: newLoadSpec && rebuildLoadSpec(newLoadSpec), + }; } onChannelData(e: ChannelLoadEvent): void { diff --git a/src/workers/types.ts b/src/workers/types.ts index dd54642c..f68cf1c0 100644 --- a/src/workers/types.ts +++ b/src/workers/types.ts @@ -1,6 +1,6 @@ import { ImageInfo } from "../Volume"; import { CreateLoaderOptions } from "../loaders"; -import { LoadSpec, VolumeDims } from "../loaders/IVolumeLoader"; +import { LoadSpec, LoadedVolumeInfo, VolumeDims } from "../loaders/IVolumeLoader"; /** The types of requests that can be made to the worker. Mostly corresponds to methods on `IVolumeLoader`. */ export const enum WorkerMsgType { @@ -50,9 +50,9 @@ export type WorkerRequestPayload = { export type WorkerResponsePayload = { [WorkerMsgType.INIT]: void; [WorkerMsgType.CREATE_LOADER]: boolean; - [WorkerMsgType.CREATE_VOLUME]: [ImageInfo, LoadSpec]; + [WorkerMsgType.CREATE_VOLUME]: LoadedVolumeInfo; [WorkerMsgType.LOAD_DIMS]: VolumeDims[]; - [WorkerMsgType.LOAD_VOLUME_DATA]: [ImageInfo | undefined, LoadSpec | undefined]; + [WorkerMsgType.LOAD_VOLUME_DATA]: Partial; }[T]; /** Currently the only event a loader can produce is a `ChannelLoadEvent` when a single channel loads. */