mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(studio): a slow thumbnail job keeps a reader that other clips still decode from (#6643)
* fix(studio): a slow thumbnail job keeps a reader that other clips still decode from A job that timed out judged the reader by its own progress only. A poster job that got an already-open reader while a sibling was decoding on it, and took more than its budget to reach its one frame (a slow link, Chrome's six connections held), logged a timeout and retired a healthy reader, so the next clip re-read the index. The pooled reader now counts the frames every job decodes from it, and a timed-out job retires it only when no frame came from it while the job held it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(studio): the reader's frame count says whose frames it counts in its name, not a comment Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
30df5a48af
commit
fa137cdfef
@@ -19,6 +19,7 @@ export interface ThumbnailSource extends OpenedSource {
|
||||
release(failed: boolean): void;
|
||||
/** The job waited for the reader to open: the open finishing was progress the job saw. */
|
||||
waitedForOpen: boolean;
|
||||
progress: { framesFromAnyJob: number };
|
||||
}
|
||||
|
||||
interface PooledSource {
|
||||
@@ -28,6 +29,7 @@ interface PooledSource {
|
||||
ready: boolean;
|
||||
opened: Promise<OpenedSource>;
|
||||
users: number;
|
||||
progress: { framesFromAnyJob: number };
|
||||
idleTimer: ReturnType<typeof setTimeout> | null;
|
||||
disposed: boolean;
|
||||
}
|
||||
@@ -100,6 +102,7 @@ function pooledSource(source: string, version: string | undefined): PooledSource
|
||||
ready: false,
|
||||
opened: open(source, () => entry),
|
||||
users: 0,
|
||||
progress: { framesFromAnyJob: 0 },
|
||||
idleTimer: null,
|
||||
disposed: false,
|
||||
};
|
||||
@@ -150,7 +153,8 @@ export async function acquireThumbnailSource(
|
||||
}
|
||||
};
|
||||
try {
|
||||
return { ...(await untilAborted(entry.opened, signal)), release, waitedForOpen };
|
||||
const { progress } = entry;
|
||||
return { ...(await untilAborted(entry.opened, signal)), release, waitedForOpen, progress };
|
||||
} catch (error) {
|
||||
// Timed out still waiting: the open made no progress in the job's whole run.
|
||||
release(!isAbortError(error) || timedOut(signal));
|
||||
|
||||
@@ -766,6 +766,32 @@ describe("decodeVideoThumbnail's shared source", () => {
|
||||
expect(input.getPrimaryVideoTrack).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps the reader when a clip times out while another clip decodes frames from it", async () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
recordDecodes([]);
|
||||
await decodeVideoThumbnail(clip(0), signal);
|
||||
canvasesAtTimestamps.mockImplementationOnce(async function* () {
|
||||
await new Promise(() => {});
|
||||
yield null;
|
||||
});
|
||||
canvasesAtTimestamps.mockImplementationOnce(async function* (
|
||||
timestamps: AsyncIterable<number>,
|
||||
) {
|
||||
await timestamps[Symbol.asyncIterator]().next();
|
||||
yield { canvas: document.createElement("canvas") };
|
||||
await new Promise(() => {});
|
||||
});
|
||||
const controller = new AbortController();
|
||||
void decodeVideoThumbnail(clip(6), controller.signal).catch(() => {});
|
||||
void decodeVideoThumbnail(clip(12), new AbortController().signal).catch(() => {});
|
||||
await vi.waitFor(() => expect(URL.createObjectURL).toHaveBeenCalledTimes(4));
|
||||
controller.abort(timedOut());
|
||||
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
await decodeVideoThumbnail(clip(18), signal);
|
||||
expect(input.getPrimaryVideoTrack).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("fails a job that times out waiting for its source to open, and the next clip opens afresh", async () => {
|
||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const stalled = untilDisposed();
|
||||
|
||||
@@ -79,7 +79,6 @@ interface DecodedResources {
|
||||
held: HeldFrame[];
|
||||
canvases: Set<HTMLCanvasElement | OffscreenCanvas>;
|
||||
releasedFrameBytes: number;
|
||||
decodedByJob: number;
|
||||
}
|
||||
|
||||
interface ThumbnailCanvasSink {
|
||||
@@ -175,6 +174,7 @@ async function decodeFrames(
|
||||
slots: { slot: number; key: string; decodedAt?: number }[],
|
||||
signal: AbortSignal,
|
||||
resources: DecodedResources,
|
||||
progress: ThumbnailSource["progress"],
|
||||
): Promise<void> {
|
||||
let next = 0;
|
||||
for await (const wrapped of sink.canvasesAtTimestamps(times)) {
|
||||
@@ -185,7 +185,7 @@ async function decodeFrames(
|
||||
const blob = await canvasToBlob(wrapped.canvas);
|
||||
throwIfAborted(signal);
|
||||
const frame = shareFrame(target.key, target.decodedAt, URL.createObjectURL(blob), blob.size);
|
||||
resources.decodedByJob += 1;
|
||||
progress.framesFromAnyJob += 1;
|
||||
resources.urls[target.slot] = frame.url;
|
||||
resources.held.push({ key: target.key, frame });
|
||||
}
|
||||
@@ -232,7 +232,6 @@ export async function decodeVideoThumbnail(
|
||||
held: [],
|
||||
canvases: new Set(),
|
||||
releasedFrameBytes: budgets.sharedFrameCacheBytes,
|
||||
decodedByJob: 0,
|
||||
};
|
||||
const entry = sourceInfos.get(request.source);
|
||||
const known = entry?.version === request.contentVersion ? entry?.info : undefined;
|
||||
@@ -271,8 +270,9 @@ async function decodeMissingFrames(
|
||||
if (timedOut(signal)) reportMediaFetchFailure(timeoutWithoutProgress());
|
||||
throw error;
|
||||
}
|
||||
const framesBefore = source.progress.framesFromAnyJob;
|
||||
const releaseOnAbort = () => {
|
||||
const madeProgress = source.waitedForOpen || resources.decodedByJob > 0;
|
||||
const madeProgress = source.waitedForOpen || source.progress.framesFromAnyJob > framesBefore;
|
||||
const failed = timedOut(signal) && !madeProgress;
|
||||
if (failed) reportMediaFetchFailure(timeoutWithoutProgress());
|
||||
source.release(failed);
|
||||
@@ -307,7 +307,8 @@ async function decodeMissingFrames(
|
||||
fit: request.fit ?? "cover",
|
||||
poolSize: 1,
|
||||
});
|
||||
await decodeFrames(sink, decodeTimesAtNearbyKeyframes(), slots, signal, resources);
|
||||
const times = decodeTimesAtNearbyKeyframes();
|
||||
await decodeFrames(sink, times, slots, signal, resources, source.progress);
|
||||
}
|
||||
const result = loadedResult(resources, info.aspect, budgets, timestamps.length);
|
||||
source.release(false);
|
||||
|
||||
Reference in New Issue
Block a user