Files
hyperframes/packages/studio/src/player/lib/thumbnailScheduler.test.ts
T
Miguel ÁngelandCodex 96861cbafc perf(studio-server): coordinate cancelable thumbnail generation (#2720)
* perf(studio): schedule adaptive timeline thumbnails

* perf(studio): bound thumbnail decoding resources

* perf(studio): virtualize timeline thumbnail media

* perf(studio): prioritize timeline thumbnail work

* perf(studio-server): coordinate cancelable thumbnail generation

---------

Co-authored-by: Codex <codex@local>
2026-08-05 20:41:35 -07:00

389 lines
13 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { resolveTimelineViewportBudgets } from "./timelineViewportBudgets";
import {
createThumbnailKey,
createThumbnailRequestIdentity,
ThumbnailScheduler,
type ThumbnailLoadedResult,
type ThumbnailPriority,
type ThumbnailRequest,
} from "./thumbnailScheduler";
function deferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((accept, fail) => {
resolve = accept;
reject = fail;
});
return { promise, resolve, reject };
}
function result(name: string, weight = 1, dispose = vi.fn()): ThumbnailLoadedResult {
return { value: { kind: "image", url: name, aspect: 1 }, weight, dispose };
}
function request(
key: string,
load: ThumbnailRequest["load"],
priority: ThumbnailPriority = "visible",
overrides: Partial<ThumbnailRequest> = {},
): ThumbnailRequest {
return {
key,
projectId: "project-a",
sessionEpoch: 1,
kind: "image",
priority,
load,
...overrides,
};
}
async function flush(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
}
describe("ThumbnailScheduler", () => {
it("deduplicates identical requests and releases subscribers independently", async () => {
const scheduler = new ThumbnailScheduler();
const pending = deferred<ThumbnailLoadedResult>();
const load = vi.fn(() => pending.promise);
const listenerA = vi.fn();
const listenerB = vi.fn();
const leaseA = scheduler.acquire(request("same", load), listenerA);
const leaseB = scheduler.acquire(request("same", load), listenerB);
expect(load).toHaveBeenCalledTimes(1);
leaseA.release();
listenerA.mockClear();
pending.resolve(result("poster"));
await flush();
expect(scheduler.getSnapshot(request("same", load))).toMatchObject({
status: "ready",
value: { url: "poster" },
});
expect(listenerA).not.toHaveBeenCalled();
expect(listenerB).toHaveBeenCalled();
leaseB.release();
expect(scheduler.getDiagnostics().leases).toBe(0);
});
it("keeps snapshots referentially stable and supports independent leases sharing a callback", async () => {
const scheduler = new ThumbnailScheduler();
const listener = vi.fn();
const shared = request("stable", async () => result("stable"));
const first = scheduler.acquire(shared, listener);
const second = scheduler.acquire(shared, listener);
await flush();
expect(scheduler.getSnapshot(shared)).toBe(scheduler.getSnapshot(shared));
listener.mockClear();
scheduler.invalidateProject("project-a");
expect(listener).not.toHaveBeenCalled();
expect(scheduler.getDiagnostics().leases).toBe(2);
first.release();
second.release();
scheduler.invalidateProject("project-a");
expect(scheduler.getSnapshot(shared).status).toBe("idle");
});
it("scopes the same caller key by work shape", async () => {
const scheduler = new ThumbnailScheduler();
const poster = request("shared", async () => result("poster"));
const strip = request("shared", async () => result("strip"), "visible", {
kind: "video",
rich: true,
});
scheduler.acquire(poster, vi.fn());
scheduler.acquire(strip, vi.fn());
await flush();
expect(scheduler.getSnapshot(poster)).toMatchObject({
status: "ready",
value: { url: "poster" },
});
expect(scheduler.getSnapshot(strip)).toMatchObject({
status: "ready",
value: { url: "strip" },
});
});
it("orders interaction before visible before overscan when a slot opens", async () => {
const scheduler = new ThumbnailScheduler(
resolveTimelineViewportBudgets({ concurrentMetadataJobs: 1 }),
);
const first = deferred<ThumbnailLoadedResult>();
const starts: string[] = [];
const load = (name: string, pending?: ReturnType<typeof deferred<ThumbnailLoadedResult>>) =>
vi.fn(() => {
starts.push(name);
return pending?.promise ?? Promise.resolve(result(name));
});
scheduler.acquire(request("first", load("first", first)), vi.fn());
scheduler.acquire(request("overscan", load("overscan"), "overscan"), vi.fn());
scheduler.acquire(request("visible", load("visible"), "visible"), vi.fn());
scheduler.acquire(request("interaction", load("interaction"), "interaction"), vi.fn());
expect(starts).toEqual(["first"]);
first.resolve(result("first"));
await vi.waitFor(() => {
expect(starts).toEqual(["first", "interaction", "visible", "overscan"]);
});
});
it("suspends new rich work while scrolling but lets posters proceed", async () => {
const scheduler = new ThumbnailScheduler();
const richLoad = vi.fn(async () => result("rich"));
const posterLoad = vi.fn(async () => result("poster"));
scheduler.setScrolling(true);
scheduler.acquire(request("rich", richLoad, "interaction", { rich: true }), vi.fn());
scheduler.acquire(request("poster", posterLoad), vi.fn());
await flush();
expect(richLoad).not.toHaveBeenCalled();
expect(posterLoad).toHaveBeenCalledTimes(1);
scheduler.setScrolling(false);
await flush();
expect(richLoad).toHaveBeenCalledTimes(1);
});
it("aborts queued and active jobs after the final release", async () => {
const scheduler = new ThumbnailScheduler(
resolveTimelineViewportBudgets({ concurrentVideoDecodes: 1 }),
);
const active = deferred<ThumbnailLoadedResult>();
let activeSignal: AbortSignal | undefined;
const activeLease = scheduler.acquire(
request(
"active",
(signal) => {
activeSignal = signal;
return active.promise;
},
"visible",
{ kind: "video" },
),
vi.fn(),
);
const queuedLoad = vi.fn(async () => result("queued"));
const queuedLease = scheduler.acquire(
request("queued", queuedLoad, "visible", { kind: "video" }),
vi.fn(),
);
queuedLease.release();
activeLease.release();
expect(activeSignal?.aborted).toBe(true);
expect(queuedLoad).not.toHaveBeenCalled();
active.reject(new DOMException("aborted", "AbortError"));
await flush();
expect(scheduler.getDiagnostics()).toMatchObject({ queued: 0, active: 0, leases: 0 });
});
it("disposes a late result exactly once after its final lease releases", async () => {
const scheduler = new ThumbnailScheduler();
const pending = deferred<ThumbnailLoadedResult>();
const dispose = vi.fn();
const lease = scheduler.acquire(
request("late", () => pending.promise),
vi.fn(),
);
lease.release();
pending.resolve(result("late", 1, dispose));
await flush();
expect(dispose).toHaveBeenCalledTimes(1);
expect(scheduler.getSnapshot(request("late", () => pending.promise))).toEqual({
status: "idle",
});
});
it("preserves a synchronous re-acquire when an expired failure is replaced", async () => {
vi.useFakeTimers();
const scheduler = new ThumbnailScheduler(
resolveTimelineViewportBudgets({ metadataFailureTtlMs: 10 }),
);
const load = vi
.fn<ThumbnailRequest["load"]>()
.mockRejectedValueOnce(new Error("temporary"))
.mockResolvedValue(result("recovered"));
const failed = request("retry", load);
let reacquireOnNotify = false;
let nestedLease: ReturnType<ThumbnailScheduler["acquire"]> | undefined;
const firstLease = scheduler.acquire(failed, () => {
if (!reacquireOnNotify) return;
reacquireOnNotify = false;
nestedLease = scheduler.acquire(failed, vi.fn());
});
await flush();
vi.advanceTimersByTime(11);
reacquireOnNotify = true;
const outerLease = scheduler.acquire(failed, vi.fn());
await flush();
expect(load).toHaveBeenCalledTimes(2);
expect(scheduler.getSnapshot(failed)).toMatchObject({
status: "ready",
value: { url: "recovered" },
});
expect(scheduler.getDiagnostics().leases).toBe(2);
firstLease.release();
nestedLease?.release();
outerLease.release();
vi.useRealTimers();
});
it("times out a hung loader, frees its bucket, and disposes a late result", async () => {
vi.useFakeTimers();
const scheduler = new ThumbnailScheduler(
resolveTimelineViewportBudgets({
concurrentVideoDecodes: 1,
thumbnailLoadTimeoutMs: 10,
}),
);
const hung = deferred<ThumbnailLoadedResult>();
const dispose = vi.fn();
let signal: AbortSignal | undefined;
const hungRequest = request(
"hung",
(activeSignal) => {
signal = activeSignal;
return hung.promise;
},
"visible",
{ kind: "video" },
);
const nextLoad = vi.fn(async () => result("next"));
const nextRequest = request("next", nextLoad, "visible", { kind: "video" });
const hungLease = scheduler.acquire(hungRequest, vi.fn());
const nextLease = scheduler.acquire(nextRequest, vi.fn());
await vi.advanceTimersByTimeAsync(11);
await flush();
expect(signal?.aborted).toBe(true);
expect(nextLoad).toHaveBeenCalledTimes(1);
expect(scheduler.getSnapshot(hungRequest).status).toBe("error");
hung.resolve(result("late", 1, dispose));
await flush();
expect(dispose).toHaveBeenCalledTimes(1);
hungLease.release();
nextLease.release();
vi.useRealTimers();
});
it("isolates late results by session epoch", async () => {
const scheduler = new ThumbnailScheduler();
const old = deferred<ThumbnailLoadedResult>();
const oldRequest = request("poster", () => old.promise, "visible", { sessionEpoch: 1 });
const nextRequest = request("poster", async () => result("next"), "visible", {
sessionEpoch: 2,
});
scheduler.acquire(oldRequest, vi.fn());
scheduler.acquire(nextRequest, vi.fn());
await flush();
old.resolve(result("old"));
await flush();
expect(scheduler.getSnapshot(nextRequest)).toMatchObject({
status: "ready",
value: { url: "next" },
});
});
it("turns synchronous loader errors into bounded failure snapshots", async () => {
const scheduler = new ThumbnailScheduler();
const bad = request("throws", () => {
throw new Error("sync failure");
});
const lease = scheduler.acquire(bad, vi.fn());
await flush();
expect(scheduler.getSnapshot(bad)).toMatchObject({ status: "error" });
expect(scheduler.getDiagnostics().active).toBe(0);
lease.release();
});
it("disposes uncached results after the final lease and tolerates broken disposers", async () => {
const scheduler = new ThumbnailScheduler(
resolveTimelineViewportBudgets({ thumbnailCacheBytes: 1 }),
);
const dispose = vi.fn(() => {
throw new Error("cleanup failure");
});
const oversized = request("oversized", async () => result("large", 2, dispose));
const lease = scheduler.acquire(oversized, vi.fn());
await flush();
expect(scheduler.getSnapshot(oversized).status).toBe("ready");
expect(() => lease.release()).not.toThrow();
expect(dispose).toHaveBeenCalledTimes(1);
expect(scheduler.getSnapshot(oversized).status).toBe("idle");
});
it("evicts least-recently-used unleased values by bytes, count, and project count", async () => {
const scheduler = new ThumbnailScheduler(
resolveTimelineViewportBudgets({
thumbnailCacheBytes: 6,
thumbnailCacheEntries: 2,
thumbnailCacheEntriesPerProject: 2,
}),
);
const disposals = [vi.fn(), vi.fn(), vi.fn()];
for (let index = 0; index < 3; index++) {
const lease = scheduler.acquire(
request(`key-${index}`, async () => result(`url-${index}`, 3, disposals[index])),
vi.fn(),
);
await flush();
lease.release();
}
expect(scheduler.getDiagnostics()).toMatchObject({ cacheEntries: 2, cacheBytes: 6 });
expect(disposals[0]).toHaveBeenCalledTimes(1);
expect(disposals[1]).not.toHaveBeenCalled();
expect(disposals[2]).not.toHaveBeenCalled();
});
it("keeps failed requests quiet until the TTL expires", async () => {
vi.useFakeTimers();
const scheduler = new ThumbnailScheduler(
resolveTimelineViewportBudgets({ metadataFailureTtlMs: 100 }),
);
const load = vi.fn(async () => {
throw new Error("unsupported");
});
const first = scheduler.acquire(request("failed", load), vi.fn());
await flush();
first.release();
const second = scheduler.acquire(request("failed", load), vi.fn());
expect(load).toHaveBeenCalledTimes(1);
second.release();
vi.advanceTimersByTime(101);
scheduler.acquire(request("failed", load), vi.fn());
await flush();
expect(load).toHaveBeenCalledTimes(2);
vi.useRealTimers();
});
});
describe("createThumbnailKey", () => {
it("normalizes field order and preserves sentinel zero values", () => {
expect(createThumbnailKey({ source: "clip a", time: 0, missing: undefined })).toBe(
createThumbnailKey({ time: 0, source: "clip a" }),
);
});
it("includes the work shape in request identity", () => {
const poster = request("shared", vi.fn(), "visible", { kind: "video" });
const strip = { ...poster, rich: true };
expect(createThumbnailRequestIdentity(poster)).not.toBe(createThumbnailRequestIdentity(strip));
});
});