From 376bce937913e43142b504ea6a2869b36b91e028 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 25 Aug 2026 00:04:25 -0700 Subject: [PATCH 01/15] create `TaskPool` --- src/data_manager/task_pool/TaskPool.ts | 127 +++++++++++++++++++++++++ src/data_manager/task_pool/registry.ts | 17 ++++ src/data_manager/task_pool/task.ts | 119 +++++++++++++++++++++++ src/data_manager/task_pool/worker.ts | 44 +++++++++ tsconfig.json | 2 +- 5 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 src/data_manager/task_pool/TaskPool.ts create mode 100644 src/data_manager/task_pool/registry.ts create mode 100644 src/data_manager/task_pool/task.ts create mode 100644 src/data_manager/task_pool/worker.ts diff --git a/src/data_manager/task_pool/TaskPool.ts b/src/data_manager/task_pool/TaskPool.ts new file mode 100644 index 00000000..b2196f64 --- /dev/null +++ b/src/data_manager/task_pool/TaskPool.ts @@ -0,0 +1,127 @@ +import { deserializeError } from "serialize-error"; +import SlotMap from "../SlotMap.js"; +import type { Task, TaskHandle, WorkerResponse, WorkerRequest, TaskArgs } from "./task.js"; +import { BorrowGuard, markBorrowed } from "./task.js"; + +type StoredPromise = { + resolve: (value: unknown) => void; + reject: (reason: unknown) => void; + borrows: BorrowGuard[]; +}; + +type WaitingTask = { + promiseId: number; + taskHandle: TaskHandle; + args: In; +}; + +export class TaskPool { + private workerCount = 0; + private idleWorkers: Worker[] = []; + private promises: SlotMap = new SlotMap(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private queue: WaitingTask[] = []; + + constructor(public maxWorkers = 4) {} + + private handleWorkerMessage(message: WorkerResponse>) { + const promise = this.promises.remove(message.id); + if (promise === undefined) { + console.error( + `TaskPool: received worker response for nonexistent task ID ${message.id} with type "${message.task}"` + ); + return; + } + + const borrowsExpected = promise.borrows.length; + const borrowsReceived = message.borrows.length; + if (borrowsExpected === borrowsReceived) { + for (let i = 0; i < borrowsReceived; i++) { + promise.borrows[i].restore(message.borrows[i]); + } + } else { + console.error( + `TaskPool: number of borrows returned from task "${message.task}" with ID ${message.id} did not match (expected ${borrowsExpected}, received ${borrowsReceived})` + ); + } + + if (message.error) { + promise.reject(deserializeError(message.result)); + } else { + promise.resolve(message.result); + } + + this.submitTasks(); + promise.borrows.forEach((borrow) => borrow.afterRestore()); + } + + private addWorker() { + const worker = new Worker(new URL("./worker", import.meta.url), { type: "module" }); + let ready = false; + worker.onmessage = (message) => { + this.idleWorkers.push(worker); + if (!ready) { + ready = true; + this.submitTasks(); + } else { + this.handleWorkerMessage(message.data); + } + }; + this.workerCount += 1; + } + + private submitTasks() { + let i = 0; + while (i < this.queue.length) { + const task = this.queue[i]; + const borrowedBuffers: ArrayBuffer[] = []; + const args: unknown[] = []; + // Handle borrowed buffers + for (const arg of task.args) { + if (BorrowGuard.isBorrowGuard(arg)) { + const array = arg.get(); + // Task is not ready -- some resource it depends on is in use by another task + if (array === undefined) { + i++; + continue; + } + args.push(markBorrowed(array)); + borrowedBuffers.push(array.buffer); + } else { + args.push(arg); + } + } + + const worker = this.idleWorkers.pop(); + if (worker === undefined) { + // We have a ready task but we're out of workers to run it on + if (this.workerCount < this.maxWorkers) { + this.addWorker(); + } + return; + } + + this.queue.splice(i, 1); + const transfer = [...borrowedBuffers, ...(task.taskHandle.transfer?.(...task.args) ?? [])]; + worker.postMessage( + { + task: task.taskHandle.id, + id: task.promiseId, + args, + } as WorkerRequest, + { transfer } + ); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public runTask(taskHandle: TaskHandle, ...args: TaskArgs): Promise { + const borrows = args.filter(BorrowGuard.isBorrowGuard); + const promise = new Promise((resolve, reject) => { + const promiseId = this.promises.insert({ resolve, reject, borrows }); + this.queue.push({ taskHandle, promiseId, args }); + }); + this.submitTasks(); + return promise as Promise; + } +} diff --git a/src/data_manager/task_pool/registry.ts b/src/data_manager/task_pool/registry.ts new file mode 100644 index 00000000..74dafac1 --- /dev/null +++ b/src/data_manager/task_pool/registry.ts @@ -0,0 +1,17 @@ +import { Task } from "./task.js"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const TASK_HANDLERS = new Map>(); + +export const task = ( + id: Id, + handler: (...args: In) => { result: Out; transfer: Transferable[] } +): Task => { + const task = handler as Task; + task.taskId = id; + return task; +}; + +export const registerTask = (task: Task) => { + TASK_HANDLERS.set(task.taskId, task); +}; diff --git a/src/data_manager/task_pool/task.ts b/src/data_manager/task_pool/task.ts new file mode 100644 index 00000000..f271c16d --- /dev/null +++ b/src/data_manager/task_pool/task.ts @@ -0,0 +1,119 @@ +import type { NumberType, TypedArray } from "../../types.js"; +import EventDispatcher from "../../EventDispatcher.js"; + +const guardMarker = Symbol.for("TaskPool.borrowGuard"); + +type BorrowGuardEvents = { restored: void }; + +/** + * Wraps a `TypedArray` that may be temporarily transferred to a worker for processing. + * + * A `TypedArray` guarded by this class must be accessed through the `get` method, which returns `undefined` when the + * buffer is on a worker. The guard will trigger the `"restored"` event when it is returned to the main thread. + */ +export class BorrowGuard< + T extends TypedArray = TypedArray, +> extends EventDispatcher { + private [guardMarker] = true as const; + private restored = false; + + constructor(private array: T) { + super(); + } + + get borrowed(): boolean { + return this.array.buffer.detached; + } + + get(): T | undefined { + if (this.borrowed) { + return undefined; + } + return this.array; + } + + restore(array: T) { + this.array = array; + this.restored = true; + } + + afterRestore() { + if (!this.borrowed && this.restored) { + this.restored = false; + this.dispatchEvent({ type: "restored" }); + } + } + + static isBorrowGuard(value: unknown): value is BorrowGuard { + return typeof value === "object" && value !== null && value[guardMarker]; + } +} + +const borrowedMarker = "TaskPool.borrowedArray"; + +export type BorrowedArray = TypedArray & { [borrowedMarker]: true }; + +export const isBorrowedArray = (value: unknown): value is BorrowedArray => { + return ArrayBuffer.isView(value) && value[borrowedMarker]; +}; + +export const markBorrowed = >(value: T): { [borrowedMarker]: T } => ({ + [borrowedMarker]: value, +}); + +export const getBorrowed = (value: unknown): BorrowedArray | undefined => { + if (typeof value === "object" && value !== null && Object.hasOwn(value, borrowedMarker)) { + const result = value[borrowedMarker]; + if (ArrayBuffer.isView(result)) { + return result as BorrowedArray; + } + } + return undefined; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Task = { + taskId: Id; + (...args: In): { result: Out; transfer: Transferable[] }; +}; + +export type TaskHandle = { + id: string; + transfer?: (...args: In) => Transferable[]; + marker?: Out; +}; + +/** Converts all `BorrowedArray`s in an argument list to the corresponding `BorrowGuard`s */ +export type TaskArgs = T extends [infer E, ...infer R] + ? E extends BorrowedArray + ? [BorrowGuard>, ...TaskArgs] + : [E, ...TaskArgs] + : []; + +export const taskHandle = ( + id: T["taskId"] & {}, + transfer?: (...args: Parameters) => Transferable[] +): TaskHandle, ReturnType["result"]> => { + return { id, transfer }; +}; + +export type WorkerRequest = { + id: number; + task: T["taskId"]; + args: Parameters; +}; + +export type WorkerResponse = { + id: number; + task: T["taskId"]; + borrows: TypedArray[]; +} & ( + | { + error: false; + result: ReturnType["result"]; + } + | { + error: true; + result: unknown; + } +); diff --git a/src/data_manager/task_pool/worker.ts b/src/data_manager/task_pool/worker.ts new file mode 100644 index 00000000..6efc85c2 --- /dev/null +++ b/src/data_manager/task_pool/worker.ts @@ -0,0 +1,44 @@ +import { serializeError } from "serialize-error"; + +import { TASK_HANDLERS } from "./registry.js"; +import { getBorrowed, type WorkerRequest, type WorkerResponse } from "./task.js"; +import type { TypedArray, NumberType } from "../../types.js"; + +import "./test_task.js"; + +const runTask = ({ id, task, args }: WorkerRequest): [WorkerResponse, Transferable[]] => { + const handler = TASK_HANDLERS.get(task); + + // Extract borrowed `TypedArray`s, which must be returned to the main thread on completion + const borrows: TypedArray[] = []; + const borrowedBuffers: ArrayBuffer[] = []; + const processedArgs = args.map((arg) => { + const borrowed = getBorrowed(arg); + if (borrowed !== undefined) { + borrows.push(borrowed); + borrowedBuffers.push(borrowed.buffer); + return borrowed; + } + return arg; + }); + + if (handler === undefined) { + const result = serializeError(new Error(`TaskPool: tried to run nonexistent task ${task}`)); + return [{ id, task, borrows, result, error: true }, borrowedBuffers]; + } + + try { + const { result, transfer } = handler(...processedArgs); + return [{ id, task, borrows, result, error: false }, [...borrowedBuffers, ...transfer]]; + } catch (e) { + const result = serializeError(e); + return [{ id, task, borrows, result, error: true }, borrowedBuffers]; + } +}; + +self.onmessage = ({ data }: MessageEvent) => { + const [result, transfer] = runTask(data); + self.postMessage(result, { transfer }); +}; + +self.postMessage(null); diff --git a/tsconfig.json b/tsconfig.json index 02a5c51b..abd0857c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "allowSyntheticDefaultImports": true, "declaration": true, "declarationDir": "es/types", - "lib": ["DOM", "ES2022", "ES6", "DOM.Iterable", "ScriptHost", "ES2016.Array.Include"], + "lib": ["DOM", "ES2022", "ES6", "DOM.Iterable", "ScriptHost", "ES2016.Array.Include", "ES2024.ArrayBuffer"], "module": "Node16", "moduleResolution": "node16", "noImplicitAny": false, From 0dc0d28e7050c06a894d27ed8bc45ea11ee4d373 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Mon, 31 Aug 2026 10:22:26 -0700 Subject: [PATCH 02/15] futzing --- src/data_manager/slot_map.ts | 1 + src/data_manager/task_pool/TaskPool.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/data_manager/slot_map.ts b/src/data_manager/slot_map.ts index cf2703ed..4a7bb10a 100644 --- a/src/data_manager/slot_map.ts +++ b/src/data_manager/slot_map.ts @@ -6,6 +6,7 @@ const isFreeSlot = (slot: Slot): slot is { [NEXT_FREE]: number } => { return slot !== undefined && slot !== null && Object.hasOwn(slot, NEXT_FREE); }; +/** A "map" that assigns values a stable numeric key on insertion that can be used to get or remove the value later. */ export default class SlotMap { private slots: Slot[] = []; private head = 0; diff --git a/src/data_manager/task_pool/TaskPool.ts b/src/data_manager/task_pool/TaskPool.ts index b2196f64..6fc6bd0d 100644 --- a/src/data_manager/task_pool/TaskPool.ts +++ b/src/data_manager/task_pool/TaskPool.ts @@ -22,7 +22,7 @@ export class TaskPool { // eslint-disable-next-line @typescript-eslint/no-explicit-any private queue: WaitingTask[] = []; - constructor(public maxWorkers = 4) {} + constructor(public maxWorkers = navigator.hardwareConcurrency ?? 4) {} private handleWorkerMessage(message: WorkerResponse>) { const promise = this.promises.remove(message.id); From 2fb932126b40d3902d2d3ac694bae78231ae0d31 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Sun, 6 Sep 2026 22:18:28 -0700 Subject: [PATCH 03/15] various docs and new get method --- src/data_manager/task_pool/registry.ts | 21 ++++++++++++ src/data_manager/task_pool/task.ts | 44 ++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/data_manager/task_pool/registry.ts b/src/data_manager/task_pool/registry.ts index 74dafac1..6de7fab5 100644 --- a/src/data_manager/task_pool/registry.ts +++ b/src/data_manager/task_pool/registry.ts @@ -3,6 +3,27 @@ import { Task } from "./task.js"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const TASK_HANDLERS = new Map>(); +/** + * Combines a `string` task `id` and a `handler` into a `Task` that can be registered and called with a `TaskPool`. + * + * Modules that define tasks that can be run on a worker should wrap the task handler in this function, then export the + * resulting `Task`'s type while passing its value to `registerTask`: + * + * ```typescript + * const addTask = task("add", (a: number, b: number) => a + b); + * export type AddTask = typeof addTask; + * registerTask(addTask); + * ``` + * + * Main thread code can then import the task's type to create a `TaskHandle`: + * + * ```typescript + * import type { AddTask } from "./add_task.js"; + * const addTask = taskHandle("add"); + * const pool = new TaskPool(); + * const three = await pool.runTask(addTask, 1, 2); + * ``` + */ export const task = ( id: Id, handler: (...args: In) => { result: Out; transfer: Transferable[] } diff --git a/src/data_manager/task_pool/task.ts b/src/data_manager/task_pool/task.ts index f271c16d..ba76ffcd 100644 --- a/src/data_manager/task_pool/task.ts +++ b/src/data_manager/task_pool/task.ts @@ -32,6 +32,19 @@ export class BorrowGuard< return this.array; } + getAsync(): Promise { + if (this.borrowed) { + return new Promise((resolve) => { + const listener = () => { + resolve(this.array); + this.removeEventListener("restored", listener); + }; + this.addEventListener("restored", listener); + }); + } + return Promise.resolve(this.array); + } + restore(array: T) { this.array = array; this.restored = true; @@ -51,12 +64,31 @@ export class BorrowGuard< const borrowedMarker = "TaskPool.borrowedArray"; +/** + * Marker type for a `TypedArray` that has been *borrowed* from the main thread by a task handler. + * + * This type is functionally identical to the corresponding `TypedArray`. However, when it is used as an argument to a + * *task handler*, the task must be invoked from the main thread using a matching `BorrowGuard`: + * + * ```typescript + * // worker code + * const fooTask = task("foo", (borrow: BorrowedArray<"uint8">) => { ... }); + * export type FooTask = typeof fooTask; + * registerTask(fooTask); + * + * // main thread code + * const fooTask = taskHandle("foo"); + * const pool = new TaskPool(); + * // Because the handler takes a `BorrowedArray`, the task must be invoked on the main thread with a `BorrowGuard`: + * const guard = new BorrowGuard(new Uint8Array([1, 2, 3, 4, 5])); + * const result = await pool.runTask("foo", guard); + * ``` + * + * When the task runs, the array inside the `BorrowGuard` is automatically *transferred* to the assigned worker, where + * it can be safely modified in-place. When the task completes, the array is transferred back into the `BorrowGuard`. + */ export type BorrowedArray = TypedArray & { [borrowedMarker]: true }; -export const isBorrowedArray = (value: unknown): value is BorrowedArray => { - return ArrayBuffer.isView(value) && value[borrowedMarker]; -}; - export const markBorrowed = >(value: T): { [borrowedMarker]: T } => ({ [borrowedMarker]: value, }); @@ -83,7 +115,7 @@ export type TaskHandle = { marker?: Out; }; -/** Converts all `BorrowedArray`s in an argument list to the corresponding `BorrowGuard`s */ +/** Converts all `BorrowedArray`s in a task handler's argument list to the corresponding `BorrowGuard`s */ export type TaskArgs = T extends [infer E, ...infer R] ? E extends BorrowedArray ? [BorrowGuard>, ...TaskArgs] @@ -91,7 +123,7 @@ export type TaskArgs = T extends [infer E, ...infer R] : []; export const taskHandle = ( - id: T["taskId"] & {}, + id: T["taskId"], transfer?: (...args: Parameters) => Transferable[] ): TaskHandle, ReturnType["result"]> => { return { id, transfer }; From c14cf39c6d260ff195bd31dbbc4c7e1fb6e387d8 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Mon, 7 Sep 2026 17:59:08 -0700 Subject: [PATCH 04/15] use a more specific type --- src/data_manager/task_pool/task.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/data_manager/task_pool/task.ts b/src/data_manager/task_pool/task.ts index ba76ffcd..ea97a777 100644 --- a/src/data_manager/task_pool/task.ts +++ b/src/data_manager/task_pool/task.ts @@ -1,3 +1,5 @@ +import type { ErrorObject } from "serialize-error"; + import type { NumberType, TypedArray } from "../../types.js"; import EventDispatcher from "../../EventDispatcher.js"; @@ -146,6 +148,6 @@ export type WorkerResponse = { } | { error: true; - result: unknown; + result: ErrorObject; } ); From a5964db4f503c1c642e7853ed52b51ffb9a9828f Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 15 Sep 2026 16:26:10 -0700 Subject: [PATCH 05/15] create tasks for indexing --- src/data_manager/indexing_task.ts | 26 ++++++++++++++++++++++++++ src/data_manager/task_pool/task.ts | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/data_manager/indexing_task.ts diff --git a/src/data_manager/indexing_task.ts b/src/data_manager/indexing_task.ts new file mode 100644 index 00000000..dd38bc7b --- /dev/null +++ b/src/data_manager/indexing_task.ts @@ -0,0 +1,26 @@ +import type { NumberType, TypedArray } from "../types.js"; +import { copyChunk, reorderChunk, type CopyChunkParams } from "./indexing.js"; +import { registerTask, task } from "./task_pool/registry.js"; +import type { BorrowedArray } from "./task_pool/task.js"; + +const copyChunkTask = task( + "copyChunk", + (src: TypedArray | BorrowedArray, dest: BorrowedArray, params: CopyChunkParams) => { + copyChunk(src, dest, params); + return { result: undefined, transfer: [] }; + } +); + +const reorderChunkTask = task( + "reorderChunk", + (src: TypedArray | BorrowedArray, shape: number[], order: number[], dtype: NumberType) => { + const result = reorderChunk(src, shape, order, dtype); + return { result, transfer: [result.buffer] }; + } +); + +registerTask(copyChunkTask); +registerTask(reorderChunkTask); + +export type CopyChunkTask = typeof copyChunkTask; +export type ReorderChunkTask = typeof reorderChunkTask; diff --git a/src/data_manager/task_pool/task.ts b/src/data_manager/task_pool/task.ts index ea97a777..4a56e5ee 100644 --- a/src/data_manager/task_pool/task.ts +++ b/src/data_manager/task_pool/task.ts @@ -89,7 +89,7 @@ const borrowedMarker = "TaskPool.borrowedArray"; * When the task runs, the array inside the `BorrowGuard` is automatically *transferred* to the assigned worker, where * it can be safely modified in-place. When the task completes, the array is transferred back into the `BorrowGuard`. */ -export type BorrowedArray = TypedArray & { [borrowedMarker]: true }; +export type BorrowedArray = TypedArray & { [borrowedMarker]: true }; export const markBorrowed = >(value: T): { [borrowedMarker]: T } => ({ [borrowedMarker]: value, From 819434ecbfc3c0056131dfeeafa3c7fcd6f8cc42 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 15 Sep 2026 17:01:13 -0700 Subject: [PATCH 06/15] properly import indexing tasks --- src/data_manager/task_pool/worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data_manager/task_pool/worker.ts b/src/data_manager/task_pool/worker.ts index 6efc85c2..88c66166 100644 --- a/src/data_manager/task_pool/worker.ts +++ b/src/data_manager/task_pool/worker.ts @@ -4,7 +4,7 @@ import { TASK_HANDLERS } from "./registry.js"; import { getBorrowed, type WorkerRequest, type WorkerResponse } from "./task.js"; import type { TypedArray, NumberType } from "../../types.js"; -import "./test_task.js"; +import "../indexing_task.js"; const runTask = ({ id, task, args }: WorkerRequest): [WorkerResponse, Transferable[]] => { const handler = TASK_HANDLERS.get(task); From 6aa09709fc8fe32fc5cf5699d28aa272cb17af2b Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 6 Oct 2026 14:22:11 -0700 Subject: [PATCH 07/15] rename: TaskPool.ts -> task_pool.ts --- src/data_manager/task_pool/{TaskPool.ts => task_pool.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/data_manager/task_pool/{TaskPool.ts => task_pool.ts} (100%) diff --git a/src/data_manager/task_pool/TaskPool.ts b/src/data_manager/task_pool/task_pool.ts similarity index 100% rename from src/data_manager/task_pool/TaskPool.ts rename to src/data_manager/task_pool/task_pool.ts From c9c7c842b3b1668a02dbea1894221f1a6f93e798 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 6 Oct 2026 15:18:55 -0700 Subject: [PATCH 08/15] try a new approach to adding tasks --- src/data_manager/task_pool/registry.ts | 38 ----------- src/data_manager/task_pool/runner.ts | 83 +++++++++++++++++++++++++ src/data_manager/task_pool/task.ts | 4 +- src/data_manager/task_pool/task_pool.ts | 7 ++- src/data_manager/task_pool/worker.ts | 44 ------------- 5 files changed, 89 insertions(+), 87 deletions(-) delete mode 100644 src/data_manager/task_pool/registry.ts create mode 100644 src/data_manager/task_pool/runner.ts delete mode 100644 src/data_manager/task_pool/worker.ts diff --git a/src/data_manager/task_pool/registry.ts b/src/data_manager/task_pool/registry.ts deleted file mode 100644 index 6de7fab5..00000000 --- a/src/data_manager/task_pool/registry.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Task } from "./task.js"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const TASK_HANDLERS = new Map>(); - -/** - * Combines a `string` task `id` and a `handler` into a `Task` that can be registered and called with a `TaskPool`. - * - * Modules that define tasks that can be run on a worker should wrap the task handler in this function, then export the - * resulting `Task`'s type while passing its value to `registerTask`: - * - * ```typescript - * const addTask = task("add", (a: number, b: number) => a + b); - * export type AddTask = typeof addTask; - * registerTask(addTask); - * ``` - * - * Main thread code can then import the task's type to create a `TaskHandle`: - * - * ```typescript - * import type { AddTask } from "./add_task.js"; - * const addTask = taskHandle("add"); - * const pool = new TaskPool(); - * const three = await pool.runTask(addTask, 1, 2); - * ``` - */ -export const task = ( - id: Id, - handler: (...args: In) => { result: Out; transfer: Transferable[] } -): Task => { - const task = handler as Task; - task.taskId = id; - return task; -}; - -export const registerTask = (task: Task) => { - TASK_HANDLERS.set(task.taskId, task); -}; diff --git a/src/data_manager/task_pool/runner.ts b/src/data_manager/task_pool/runner.ts new file mode 100644 index 00000000..cc983141 --- /dev/null +++ b/src/data_manager/task_pool/runner.ts @@ -0,0 +1,83 @@ +import { serializeError } from "serialize-error"; + +import { getBorrowed, Task, type WorkerRequest, type WorkerResponse } from "./task.js"; +import type { TypedArray, NumberType } from "../../types.js"; + +/** + * Combines a `string` task `id` and a `handler` into a `Task` that can be registered and called with a `TaskPool`. + * + * Modules that define tasks that can be run on a worker should export the task handler wrapped in this function: + * + * ``` + * // add_task.ts + * export const addTask = task("add", (a: number, b: number) => a + b); + * ``` + * + * To create a task pool and run this task, first create a worker module that passes the task to `initWorker`: + * + * ``` + * // worker.ts + * import { addTask } from "./add_task.js"; + * initWorker([addTask]); + * ``` + * + * Passing the url of this module to a new `TaskPool` creates a pool that can recognize and run this task. Main thread + * code should import *only* the task's type and use it to create a `TaskHandle`, which can be used to run the task: + * + * ``` + * // main.ts + * import type { AddTask } from "./add_task.js"; + * const addTask = taskHandle("add"); + * const pool = new TaskPool(new URL(./worker.ts, import.meta.url)); + * const three = await pool.runTask(addTask, 1, 2); + * ``` + */ +export const task = ( + id: Id, + handler: (...args: In) => { result: Out; transfer: Transferable[] } +): Task => { + const task = handler as Task; + task.taskId = id; + return task; +}; + +export const initWorker = (tasks: Task[]) => { + const handlers = new Map(tasks.map((task) => [task.taskId, task])); + + const runTask = ({ id, task, args }: WorkerRequest): [WorkerResponse, Transferable[]] => { + const handler = handlers.get(task); + + // Extract borrowed `TypedArray`s, which must be returned to the main thread on completion + const borrows: TypedArray[] = []; + const borrowedBuffers: ArrayBuffer[] = []; + const processedArgs = args.map((arg) => { + const borrowed = getBorrowed(arg); + if (borrowed !== undefined) { + borrows.push(borrowed); + borrowedBuffers.push(borrowed.buffer); + return borrowed; + } + return arg; + }); + + if (handler === undefined) { + const result = serializeError(new Error(`TaskPool: tried to run nonexistent task ${task}`)); + return [{ id, task, borrows, result, error: true }, borrowedBuffers]; + } + + try { + const { result, transfer } = handler(...processedArgs); + return [{ id, task, borrows, result, error: false }, [...borrowedBuffers, ...transfer]]; + } catch (e) { + const result = serializeError(e); + return [{ id, task, borrows, result, error: true }, borrowedBuffers]; + } + }; + + self.onmessage = ({ data }: MessageEvent) => { + const [result, transfer] = runTask(data); + self.postMessage(result, { transfer }); + }; + + self.postMessage(null); +}; diff --git a/src/data_manager/task_pool/task.ts b/src/data_manager/task_pool/task.ts index 4a56e5ee..39946c7d 100644 --- a/src/data_manager/task_pool/task.ts +++ b/src/data_manager/task_pool/task.ts @@ -13,9 +13,7 @@ type BorrowGuardEvents = { restored: void }; * A `TypedArray` guarded by this class must be accessed through the `get` method, which returns `undefined` when the * buffer is on a worker. The guard will trigger the `"restored"` event when it is returned to the main thread. */ -export class BorrowGuard< - T extends TypedArray = TypedArray, -> extends EventDispatcher { +export class BorrowGuard extends EventDispatcher { private [guardMarker] = true as const; private restored = false; diff --git a/src/data_manager/task_pool/task_pool.ts b/src/data_manager/task_pool/task_pool.ts index 6fc6bd0d..0a4f0d36 100644 --- a/src/data_manager/task_pool/task_pool.ts +++ b/src/data_manager/task_pool/task_pool.ts @@ -22,7 +22,10 @@ export class TaskPool { // eslint-disable-next-line @typescript-eslint/no-explicit-any private queue: WaitingTask[] = []; - constructor(public maxWorkers = navigator.hardwareConcurrency ?? 4) {} + constructor( + private readonly workerPath: URL, + public maxWorkers = navigator.hardwareConcurrency ?? 4 + ) {} private handleWorkerMessage(message: WorkerResponse>) { const promise = this.promises.remove(message.id); @@ -56,7 +59,7 @@ export class TaskPool { } private addWorker() { - const worker = new Worker(new URL("./worker", import.meta.url), { type: "module" }); + const worker = new Worker(this.workerPath, { type: "module" }); let ready = false; worker.onmessage = (message) => { this.idleWorkers.push(worker); diff --git a/src/data_manager/task_pool/worker.ts b/src/data_manager/task_pool/worker.ts deleted file mode 100644 index 88c66166..00000000 --- a/src/data_manager/task_pool/worker.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { serializeError } from "serialize-error"; - -import { TASK_HANDLERS } from "./registry.js"; -import { getBorrowed, type WorkerRequest, type WorkerResponse } from "./task.js"; -import type { TypedArray, NumberType } from "../../types.js"; - -import "../indexing_task.js"; - -const runTask = ({ id, task, args }: WorkerRequest): [WorkerResponse, Transferable[]] => { - const handler = TASK_HANDLERS.get(task); - - // Extract borrowed `TypedArray`s, which must be returned to the main thread on completion - const borrows: TypedArray[] = []; - const borrowedBuffers: ArrayBuffer[] = []; - const processedArgs = args.map((arg) => { - const borrowed = getBorrowed(arg); - if (borrowed !== undefined) { - borrows.push(borrowed); - borrowedBuffers.push(borrowed.buffer); - return borrowed; - } - return arg; - }); - - if (handler === undefined) { - const result = serializeError(new Error(`TaskPool: tried to run nonexistent task ${task}`)); - return [{ id, task, borrows, result, error: true }, borrowedBuffers]; - } - - try { - const { result, transfer } = handler(...processedArgs); - return [{ id, task, borrows, result, error: false }, [...borrowedBuffers, ...transfer]]; - } catch (e) { - const result = serializeError(e); - return [{ id, task, borrows, result, error: true }, borrowedBuffers]; - } -}; - -self.onmessage = ({ data }: MessageEvent) => { - const [result, transfer] = runTask(data); - self.postMessage(result, { transfer }); -}; - -self.postMessage(null); From d601af4b6ba004e6d942d3ff84cd19d7750d149d Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 6 Oct 2026 15:35:04 -0700 Subject: [PATCH 09/15] shuffling things around a bit more --- src/data_manager/indexing_task.ts | 2 +- src/data_manager/task_pool/borrow_guard.ts | 80 +++++++++++ src/data_manager/task_pool/runner.ts | 3 +- src/data_manager/task_pool/task.ts | 151 --------------------- src/data_manager/task_pool/task_pool.ts | 11 +- src/data_manager/task_pool/types.ts | 69 ++++++++++ 6 files changed, 161 insertions(+), 155 deletions(-) create mode 100644 src/data_manager/task_pool/borrow_guard.ts delete mode 100644 src/data_manager/task_pool/task.ts create mode 100644 src/data_manager/task_pool/types.ts diff --git a/src/data_manager/indexing_task.ts b/src/data_manager/indexing_task.ts index dd38bc7b..76fcdc37 100644 --- a/src/data_manager/indexing_task.ts +++ b/src/data_manager/indexing_task.ts @@ -1,7 +1,7 @@ import type { NumberType, TypedArray } from "../types.js"; import { copyChunk, reorderChunk, type CopyChunkParams } from "./indexing.js"; import { registerTask, task } from "./task_pool/registry.js"; -import type { BorrowedArray } from "./task_pool/task.js"; +import type { BorrowedArray } from "./task_pool/types.js"; const copyChunkTask = task( "copyChunk", diff --git a/src/data_manager/task_pool/borrow_guard.ts b/src/data_manager/task_pool/borrow_guard.ts new file mode 100644 index 00000000..3efe16a9 --- /dev/null +++ b/src/data_manager/task_pool/borrow_guard.ts @@ -0,0 +1,80 @@ +import type { NumberType, TypedArray } from "../../types.js"; +import EventDispatcher from "../../EventDispatcher.js"; +import { BorrowedArray } from "./types.js"; + +const guardMarker = Symbol.for("TaskPool.borrowGuard"); + +type BorrowGuardEvents = { restored: void }; + +/** + * Wraps a `TypedArray` that may be temporarily transferred to a worker for processing. + * + * A `TypedArray` guarded by this class must be accessed through the `get` method, which returns `undefined` when the + * buffer is on a worker. The guard will trigger the `"restored"` event when it is returned to the main thread. + */ +export class BorrowGuard extends EventDispatcher { + private [guardMarker] = true as const; + private restored = false; + + constructor(private array: T) { + super(); + } + + get borrowed(): boolean { + return this.array.buffer.detached; + } + + get(): T | undefined { + if (this.borrowed) { + return undefined; + } + return this.array; + } + + getAsync(): Promise { + if (this.borrowed) { + return new Promise((resolve) => { + const listener = () => { + if (!this.borrowed) { + resolve(this.array); + } + this.removeEventListener("restored", listener); + }; + this.addEventListener("restored", listener); + }); + } + return Promise.resolve(this.array); + } + + restore(array: T) { + this.array = array; + this.restored = true; + } + + afterRestore() { + if (!this.borrowed && this.restored) { + this.restored = false; + this.dispatchEvent({ type: "restored" }); + } + } + + static isBorrowGuard(value: unknown): value is BorrowGuard { + return typeof value === "object" && value !== null && value[guardMarker]; + } +} + +const borrowedMarker = "TaskPool.borrowedArray"; + +export const markBorrowed = >(value: T): { [borrowedMarker]: T } => ({ + [borrowedMarker]: value, +}); + +export const getBorrowed = (value: unknown): BorrowedArray | undefined => { + if (typeof value === "object" && value !== null && Object.hasOwn(value, borrowedMarker)) { + const result = value[borrowedMarker]; + if (ArrayBuffer.isView(result)) { + return result as BorrowedArray; + } + } + return undefined; +}; diff --git a/src/data_manager/task_pool/runner.ts b/src/data_manager/task_pool/runner.ts index cc983141..f8b24fc2 100644 --- a/src/data_manager/task_pool/runner.ts +++ b/src/data_manager/task_pool/runner.ts @@ -1,7 +1,8 @@ import { serializeError } from "serialize-error"; -import { getBorrowed, Task, type WorkerRequest, type WorkerResponse } from "./task.js"; import type { TypedArray, NumberType } from "../../types.js"; +import { getBorrowed } from "./borrow_guard.js"; +import type { Task, WorkerRequest, WorkerResponse } from "./types.js"; /** * Combines a `string` task `id` and a `handler` into a `Task` that can be registered and called with a `TaskPool`. diff --git a/src/data_manager/task_pool/task.ts b/src/data_manager/task_pool/task.ts deleted file mode 100644 index 39946c7d..00000000 --- a/src/data_manager/task_pool/task.ts +++ /dev/null @@ -1,151 +0,0 @@ -import type { ErrorObject } from "serialize-error"; - -import type { NumberType, TypedArray } from "../../types.js"; -import EventDispatcher from "../../EventDispatcher.js"; - -const guardMarker = Symbol.for("TaskPool.borrowGuard"); - -type BorrowGuardEvents = { restored: void }; - -/** - * Wraps a `TypedArray` that may be temporarily transferred to a worker for processing. - * - * A `TypedArray` guarded by this class must be accessed through the `get` method, which returns `undefined` when the - * buffer is on a worker. The guard will trigger the `"restored"` event when it is returned to the main thread. - */ -export class BorrowGuard extends EventDispatcher { - private [guardMarker] = true as const; - private restored = false; - - constructor(private array: T) { - super(); - } - - get borrowed(): boolean { - return this.array.buffer.detached; - } - - get(): T | undefined { - if (this.borrowed) { - return undefined; - } - return this.array; - } - - getAsync(): Promise { - if (this.borrowed) { - return new Promise((resolve) => { - const listener = () => { - resolve(this.array); - this.removeEventListener("restored", listener); - }; - this.addEventListener("restored", listener); - }); - } - return Promise.resolve(this.array); - } - - restore(array: T) { - this.array = array; - this.restored = true; - } - - afterRestore() { - if (!this.borrowed && this.restored) { - this.restored = false; - this.dispatchEvent({ type: "restored" }); - } - } - - static isBorrowGuard(value: unknown): value is BorrowGuard { - return typeof value === "object" && value !== null && value[guardMarker]; - } -} - -const borrowedMarker = "TaskPool.borrowedArray"; - -/** - * Marker type for a `TypedArray` that has been *borrowed* from the main thread by a task handler. - * - * This type is functionally identical to the corresponding `TypedArray`. However, when it is used as an argument to a - * *task handler*, the task must be invoked from the main thread using a matching `BorrowGuard`: - * - * ```typescript - * // worker code - * const fooTask = task("foo", (borrow: BorrowedArray<"uint8">) => { ... }); - * export type FooTask = typeof fooTask; - * registerTask(fooTask); - * - * // main thread code - * const fooTask = taskHandle("foo"); - * const pool = new TaskPool(); - * // Because the handler takes a `BorrowedArray`, the task must be invoked on the main thread with a `BorrowGuard`: - * const guard = new BorrowGuard(new Uint8Array([1, 2, 3, 4, 5])); - * const result = await pool.runTask("foo", guard); - * ``` - * - * When the task runs, the array inside the `BorrowGuard` is automatically *transferred* to the assigned worker, where - * it can be safely modified in-place. When the task completes, the array is transferred back into the `BorrowGuard`. - */ -export type BorrowedArray = TypedArray & { [borrowedMarker]: true }; - -export const markBorrowed = >(value: T): { [borrowedMarker]: T } => ({ - [borrowedMarker]: value, -}); - -export const getBorrowed = (value: unknown): BorrowedArray | undefined => { - if (typeof value === "object" && value !== null && Object.hasOwn(value, borrowedMarker)) { - const result = value[borrowedMarker]; - if (ArrayBuffer.isView(result)) { - return result as BorrowedArray; - } - } - return undefined; -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type Task = { - taskId: Id; - (...args: In): { result: Out; transfer: Transferable[] }; -}; - -export type TaskHandle = { - id: string; - transfer?: (...args: In) => Transferable[]; - marker?: Out; -}; - -/** Converts all `BorrowedArray`s in a task handler's argument list to the corresponding `BorrowGuard`s */ -export type TaskArgs = T extends [infer E, ...infer R] - ? E extends BorrowedArray - ? [BorrowGuard>, ...TaskArgs] - : [E, ...TaskArgs] - : []; - -export const taskHandle = ( - id: T["taskId"], - transfer?: (...args: Parameters) => Transferable[] -): TaskHandle, ReturnType["result"]> => { - return { id, transfer }; -}; - -export type WorkerRequest = { - id: number; - task: T["taskId"]; - args: Parameters; -}; - -export type WorkerResponse = { - id: number; - task: T["taskId"]; - borrows: TypedArray[]; -} & ( - | { - error: false; - result: ReturnType["result"]; - } - | { - error: true; - result: ErrorObject; - } -); diff --git a/src/data_manager/task_pool/task_pool.ts b/src/data_manager/task_pool/task_pool.ts index 0a4f0d36..7be2db84 100644 --- a/src/data_manager/task_pool/task_pool.ts +++ b/src/data_manager/task_pool/task_pool.ts @@ -1,7 +1,14 @@ import { deserializeError } from "serialize-error"; import SlotMap from "../SlotMap.js"; -import type { Task, TaskHandle, WorkerResponse, WorkerRequest, TaskArgs } from "./task.js"; -import { BorrowGuard, markBorrowed } from "./task.js"; +import type { Task, TaskHandle, WorkerResponse, WorkerRequest, TaskArgs } from "./types.js"; +import { BorrowGuard, markBorrowed } from "./borrow_guard.js"; + +export const taskHandle = ( + id: T["taskId"], + transfer?: (...args: Parameters) => Transferable[] +): TaskHandle, ReturnType["result"]> => { + return { id, transfer }; +}; type StoredPromise = { resolve: (value: unknown) => void; diff --git a/src/data_manager/task_pool/types.ts b/src/data_manager/task_pool/types.ts new file mode 100644 index 00000000..7448226a --- /dev/null +++ b/src/data_manager/task_pool/types.ts @@ -0,0 +1,69 @@ +import type { ErrorObject } from "serialize-error"; + +import type { TypedArray, NumberType } from "../../types.js"; +import type { BorrowGuard } from "./borrow_guard.js"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Task = { + taskId: Id; + (...args: In): { result: Out; transfer: Transferable[] }; +}; + +export type TaskHandle = { + id: string; + transfer?: (...args: In) => Transferable[]; + marker?: Out; +}; + +/** + * Marker type for a `TypedArray` that has been *borrowed* from the main thread by a task handler. + * + * This type is functionally identical to the corresponding `TypedArray`. However, when it is used as an argument to a + * *task handler*, the task must be invoked from the main thread using a matching `BorrowGuard`: + * + * ```typescript + * // worker code + * const fooTask = task("foo", (borrow: BorrowedArray<"uint8">) => { ... }); + * export type FooTask = typeof fooTask; + * registerTask(fooTask); + * + * // main thread code + * const fooTask = taskHandle("foo"); + * const pool = new TaskPool(); + * // Because the handler takes a `BorrowedArray`, the task must be invoked on the main thread with a `BorrowGuard`: + * const guard = new BorrowGuard(new Uint8Array([1, 2, 3, 4, 5])); + * const result = await pool.runTask("foo", guard); + * ``` + * + * When the task runs, the array inside the `BorrowGuard` is automatically *transferred* to the assigned worker, where + * it can be safely modified in-place. When the task completes, the array is transferred back into the `BorrowGuard`. + */ +export type BorrowedArray = TypedArray & { borrowed: true }; + +/** Converts all `BorrowedArray`s in a task handler's argument list to the corresponding `BorrowGuard`s */ +export type TaskArgs = T extends [infer E, ...infer R] + ? E extends BorrowedArray + ? [BorrowGuard>, ...TaskArgs] + : [E, ...TaskArgs] + : []; + +export type WorkerRequest = { + id: number; + task: T["taskId"]; + args: Parameters; +}; + +export type WorkerResponse = { + id: number; + task: T["taskId"]; + borrows: TypedArray[]; +} & ( + | { + error: false; + result: ReturnType["result"]; + } + | { + error: true; + result: ErrorObject; + } +); From 046f8aa948fc3e394a8d4923fd0747fe1a2c983e Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 6 Oct 2026 15:40:21 -0700 Subject: [PATCH 10/15] add an `index` --- src/data_manager/task_pool/index.ts | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/data_manager/task_pool/index.ts diff --git a/src/data_manager/task_pool/index.ts b/src/data_manager/task_pool/index.ts new file mode 100644 index 00000000..7e9c9ee7 --- /dev/null +++ b/src/data_manager/task_pool/index.ts @@ -0,0 +1,4 @@ +export { BorrowGuard } from "./borrow_guard.js"; +export { task, initWorker } from "./runner.js"; +export { taskHandle, TaskPool } from "./task_pool.js"; +export type { Task, TaskHandle, BorrowedArray } from "./types.js"; From 9d02b9aa91d5a74d1f51a2cb7adfd15285414da2 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 6 Oct 2026 15:43:21 -0700 Subject: [PATCH 11/15] fix imports in `indexing_task` --- src/data_manager/indexing_task.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/data_manager/indexing_task.ts b/src/data_manager/indexing_task.ts index 76fcdc37..00a3c4d5 100644 --- a/src/data_manager/indexing_task.ts +++ b/src/data_manager/indexing_task.ts @@ -1,9 +1,8 @@ import type { NumberType, TypedArray } from "../types.js"; import { copyChunk, reorderChunk, type CopyChunkParams } from "./indexing.js"; -import { registerTask, task } from "./task_pool/registry.js"; -import type { BorrowedArray } from "./task_pool/types.js"; +import { type BorrowedArray, task } from "./task_pool/index.js"; -const copyChunkTask = task( +export const copyChunkTask = task( "copyChunk", (src: TypedArray | BorrowedArray, dest: BorrowedArray, params: CopyChunkParams) => { copyChunk(src, dest, params); @@ -11,16 +10,10 @@ const copyChunkTask = task( } ); -const reorderChunkTask = task( +export const reorderChunkTask = task( "reorderChunk", (src: TypedArray | BorrowedArray, shape: number[], order: number[], dtype: NumberType) => { const result = reorderChunk(src, shape, order, dtype); return { result, transfer: [result.buffer] }; } ); - -registerTask(copyChunkTask); -registerTask(reorderChunkTask); - -export type CopyChunkTask = typeof copyChunkTask; -export type ReorderChunkTask = typeof reorderChunkTask; From 52a9de5119004096f755900060cbee591533aed1 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Tue, 6 Oct 2026 16:18:15 -0700 Subject: [PATCH 12/15] fix import --- src/data_manager/task_pool/task_pool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data_manager/task_pool/task_pool.ts b/src/data_manager/task_pool/task_pool.ts index 7be2db84..da250702 100644 --- a/src/data_manager/task_pool/task_pool.ts +++ b/src/data_manager/task_pool/task_pool.ts @@ -1,5 +1,5 @@ import { deserializeError } from "serialize-error"; -import SlotMap from "../SlotMap.js"; +import SlotMap from "../slot_map.js"; import type { Task, TaskHandle, WorkerResponse, WorkerRequest, TaskArgs } from "./types.js"; import { BorrowGuard, markBorrowed } from "./borrow_guard.js"; From 51455e5660f512ac40bf5a86584a7e3507ba08c5 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Wed, 14 Oct 2026 22:18:10 -0700 Subject: [PATCH 13/15] use `BorrowGuard` in `DataManager` --- src/data_manager/data_manager.ts | 25 ++++++++++++++++------ src/data_manager/task_pool/borrow_guard.ts | 2 ++ src/data_manager/types.ts | 9 ++++---- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/data_manager/data_manager.ts b/src/data_manager/data_manager.ts index b853c1f5..6affbc09 100644 --- a/src/data_manager/data_manager.ts +++ b/src/data_manager/data_manager.ts @@ -18,6 +18,7 @@ import type { DeviceInterface } from "./device_interface.js"; import { dataTypeToSize } from "./utils.js"; import { ChunkSource, type VolumeMetadata, type ExtVolumeDims } from "./sources/chunk_source.js"; import SlotMap from "./slot_map.js"; +import { BorrowGuard } from "./task_pool/borrow_guard.js"; const SUBSCRIBER_ID = Symbol("DataManager.subscriberId"); @@ -146,7 +147,6 @@ export default class DataManager { this.tryAbortRequest(entry.data.requestId); } break; - case ChunkState.WORKER: } } @@ -335,8 +335,14 @@ export default class DataManager { } const { x, y, z, dataType } = dims; - const { memory } = loadEntry.data as { memory: TypedArray }; - const texture = this.deviceInterface.createTexture(memory, dataType, [x, y, z], deviceHandle); + const { memory } = loadEntry.data as { memory: BorrowGuard }; + + const array = memory.get(); + if (array === undefined) { + continue; + } + + const texture = this.deviceInterface.createTexture(array, dataType, [x, y, z], deviceHandle); this.sources[loadId.source].subscribers.forEach((s) => s.onChunkOnGpu?.(loadId, texture)); @@ -493,8 +499,8 @@ export default class DataManager { private onChunkLoad(source: number, chunk: Chunk) { const globalId = { ...chunk.id, source }; const key = chunkIdToString(globalId); - const { data: memory, dtype } = chunk; - if (memory.byteLength > this.limits.size) { + const { data: array, dtype } = chunk; + if (array.byteLength > this.limits.size) { console.error(`received chunk ${key} which is larger than the cache limit`); return; } @@ -503,6 +509,7 @@ export default class DataManager { console.error(`received chunk ${key} with invalid source id ${source}`); return; } + const memory = new BorrowGuard(array); const data = { state: ChunkState.MEMORY as const, memory, dtype }; let chunkEntry = this.chunks.get(key); @@ -580,8 +587,7 @@ export default class DataManager { this.updateChunkPriority(chunkKey, chunkEntry); } - /** Get a chunk's data buffer, if it is in memory. */ - getChunkBuffer(chunkId: ChunkId): TypedArray | undefined { + private getChunkGuard(chunkId: ChunkId): BorrowGuard | undefined { const key = chunkIdToString(chunkId); const entry = this.chunks.get(key); if (entry === undefined) { @@ -595,6 +601,11 @@ export default class DataManager { return undefined; } + /** Get a chunk's data buffer, if it is in memory. */ + getChunkBuffer(chunkId: ChunkId): TypedArray | undefined { + return this.getChunkGuard(chunkId)?.get(); + } + /** Get a chunk's texture, if it is on the GPU. */ getChunkTexture(chunkId: ChunkId): Tex | undefined { const key = chunkIdToString(chunkId); diff --git a/src/data_manager/task_pool/borrow_guard.ts b/src/data_manager/task_pool/borrow_guard.ts index 3efe16a9..fa191541 100644 --- a/src/data_manager/task_pool/borrow_guard.ts +++ b/src/data_manager/task_pool/borrow_guard.ts @@ -15,9 +15,11 @@ type BorrowGuardEvents = { restored: void }; export class BorrowGuard extends EventDispatcher { private [guardMarker] = true as const; private restored = false; + public readonly byteLength: number; constructor(private array: T) { super(); + this.byteLength = array.byteLength; } get borrowed(): boolean { diff --git a/src/data_manager/types.ts b/src/data_manager/types.ts index c5375341..883c40f2 100644 --- a/src/data_manager/types.ts +++ b/src/data_manager/types.ts @@ -1,4 +1,5 @@ import type { NumberType, TypedArray } from "../types.ts"; +import type { BorrowGuard } from "./task_pool/borrow_guard.js"; /** Settings for the maximum resources a `DataManager` may use (memory, request concurrency, etc.) */ export type DataManagerLimits = { @@ -108,15 +109,13 @@ export const enum ChunkState { MEMORY = "memory", /** Chunk is cached in memory *and* uploaded to the GPU. */ DEVICE = "device", - /** Chunk has been temporarily handed off to a worker. Unused, for now. */ - WORKER = "worker", } type ChunkEntryData = - | { state: ChunkState.QUEUED | ChunkState.WORKER } + | { state: ChunkState.QUEUED } | { state: ChunkState.LOADING; requestId: number } - | { state: ChunkState.MEMORY; memory: TypedArray; dtype: NumberType } - | { state: ChunkState.DEVICE; memory: TypedArray; dtype: NumberType; texture: Tex }; + | { state: ChunkState.MEMORY; memory: BorrowGuard; dtype: NumberType } + | { state: ChunkState.DEVICE; memory: BorrowGuard; dtype: NumberType; texture: Tex }; export type SubscriberPriority = { subscriberId: number; From ae3cd02d397c276147cd74bab4304c4ba3fe74b9 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 15 Oct 2026 14:41:24 -0700 Subject: [PATCH 14/15] publicize `getChunkGuard` --- src/data_manager/data_manager.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/data_manager/data_manager.ts b/src/data_manager/data_manager.ts index 6affbc09..8d22cc08 100644 --- a/src/data_manager/data_manager.ts +++ b/src/data_manager/data_manager.ts @@ -587,7 +587,8 @@ export default class DataManager { this.updateChunkPriority(chunkKey, chunkEntry); } - private getChunkGuard(chunkId: ChunkId): BorrowGuard | undefined { + /** Get a chunk's data buffer wrapped in a `BorrowGuard`, if it is in memory. */ + getChunkGuard(chunkId: ChunkId): BorrowGuard | undefined { const key = chunkIdToString(chunkId); const entry = this.chunks.get(key); if (entry === undefined) { @@ -601,7 +602,11 @@ export default class DataManager { return undefined; } - /** Get a chunk's data buffer, if it is in memory. */ + /** + * Get a chunk's data buffer, if it is in memory and not in use on a worker. + * + * Shorthand for `dataManager.getChunkGuard(chunkId)?.get();` + */ getChunkBuffer(chunkId: ChunkId): TypedArray | undefined { return this.getChunkGuard(chunkId)?.get(); } From d727755f57fb60a3d6c26e9fb71e0672f56efb26 Mon Sep 17 00:00:00 2001 From: Cameron Fraser Date: Thu, 15 Oct 2026 17:56:10 -0700 Subject: [PATCH 15/15] remove comment --- src/data_manager/data_manager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/data_manager/data_manager.ts b/src/data_manager/data_manager.ts index 8d22cc08..52594a35 100644 --- a/src/data_manager/data_manager.ts +++ b/src/data_manager/data_manager.ts @@ -392,7 +392,6 @@ export default class DataManager { this.tryAbortRequest(evictEntry.data.requestId); break; default: - // TODO `WORKER` state may be weird to manage here console.error( `chunk ${evictKey} queued for eviction in an invalid state (expected "memory", found ${evictEntry.data.state})` );