Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions src/core/lib/ImageWorker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ let createObjectURL: ReturnType<typeof vi.fn>;
let revokeObjectURL: ReturnType<typeof vi.fn>;

beforeEach(() => {
// The constructor schedules a warmup `setTimeout`. Fake timers keep that
// spawn under each test's control — otherwise it fires after the test ends,
// once `unstubAllGlobals` has removed `self`/`Worker`, and throws.
vi.useFakeTimers();
FakeWorker.reset();
createObjectURL = vi.fn(() => 'blob:fake-url');
revokeObjectURL = vi.fn();
Expand All @@ -51,19 +55,39 @@ beforeEach(() => {
});

afterEach(() => {
// Discards any warmup timer the test left pending, before the globals it
// depends on are torn down.
vi.useRealTimers();
vi.unstubAllGlobals();
});

const load = (mgr: ImageWorkerManager) =>
void mgr.getImage('img.png', null, null, null, null, null);

describe('ImageWorkerManager pool spawning', () => {
it('does not spawn any workers at construction', () => {
it('does not spawn any workers synchronously at construction', () => {
new ImageWorkerManager(3, support);
// Spawning is lazy — nothing happens until the first image request.
// Spawning is deferred a macrotask so it stays off the synchronous
// renderer-construction path.
expect(FakeWorker.instances.length).toBe(0);
});

it('spawns the whole pool on the timer scheduled at construction', () => {
new ImageWorkerManager(3, support);
expect(FakeWorker.instances.length).toBe(0);
vi.runAllTimers();
expect(FakeWorker.instances.length).toBe(3);
});

it('does not respawn when the warmup timer fires after a lazy spawn', () => {
const mgr = new ImageWorkerManager(3, support);
load(mgr); // request beats the timer — spawns the pool lazily
expect(FakeWorker.instances.length).toBe(3);
vi.runAllTimers();
// Timer must be a no-op now, not a second pool.
expect(FakeWorker.instances.length).toBe(3);
});

it('spawns the whole pool at once on the first image request', () => {
const mgr = new ImageWorkerManager(3, support);
load(mgr);
Expand Down
27 changes: 24 additions & 3 deletions src/core/lib/ImageWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,32 @@ export class ImageWorkerManager {
) {
this.maxWorkers = numImageWorkers;
this.createImageBitmapSupport = createImageBitmapSupport;
// Warm the pool on the next macrotask instead of waiting for the first
// image request. Spawning costs main-thread time (blob serialization +
// one Worker construction per slot); deferring it by a task keeps it off
// the synchronous renderer-construction path, while still having the pool
// ready before textures start arriving. `getImage` keeps its lazy spawn
// for a request that beats this timer.
setTimeout(() => {
// Not a hot path (fires once, at boot). The guard matters because this
// runs detached from any caller: `new Worker(blob:)` can throw outright
// where CSP forbids blob workers, and an uncaught throw here would take
// out whatever task the timer landed in. Swallowing it leaves the pool
// empty, so `getImage` retries the spawn inside its own try/catch and
// surfaces the failure as a rejected image promise — the pre-warmup
// behavior.
try {
this.spawnWorkers();
} catch (e) {
/* empty */
}
}, 0);
}

/**
* Build the shared worker source once and spawn the full pool in a single
* burst. Called lazily on the first image request. No-op once spawned.
* burst. Scheduled from the constructor via `setTimeout`; also called
* lazily from `getImage` if a request arrives first. No-op once spawned.
*/
private spawnWorkers(): void {
if (this.workers.length > 0) {
Expand Down Expand Up @@ -333,8 +354,8 @@ export class ImageWorkerManager {
try {
let nextWorkerIndex = this.getNextWorkerIndex();
if (nextWorkerIndex === -1) {
// Pool not spawned yet — spin up all workers at once on the first
// image request, off the boot/first-render critical path.
// Request beat the constructor's scheduled warmup — spin up the
// whole pool now rather than making this image wait a task.
this.spawnWorkers();
nextWorkerIndex = this.getNextWorkerIndex();
}
Expand Down
Loading