Skip to content

perf(studio): share video sources and keep frames for timeline thumbnails - #6629

Merged
vanceingalls merged 14 commits into
mainfrom
perf/studio-thumbnail-source-cache
Oct 28, 2026
Merged

vanceingalls merged 14 commits into
mainfrom
perf/studio-thumbnail-source-cache

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

What

Timeline video thumbnails on long, many-clip projects:

  • Clips cut from the same source share one opened reader instead of each re-reading and re-parsing the source's index.
  • Decoded frames outlive the strips that showed them, so scrolling back shows them at once instead of decoding them again.
  • While the preview plays, no new timeline video decode starts; a scrub still does, and pausing drains the queue.
  • Thumbnail range reads are bounded and fail typed on a stall, with fix(studio): a clip that cannot be read gives up after two retries with one typed line #6477's retry guarantees kept per reader.

Why

A HyperFrames Desktop podcast episode has ~473 video clips cut from three 44-minute 1080p sources (2.7–3.4 GB each; one camera has its 1.44 MB moov after 3.4 GB of mdat). Its filmstrips fill slowly and refill on every scroll back:

  • every clip's job opened its own mediabunny Input, so 40 clips of one source opened it 80 times (a poster and a strip each);
  • a frame's object URL was revoked as soon as no mounted strip held it, so scrolling back past the scheduler's cache decoded the whole first pass again (80 decode passes for 40 clips);
  • each open-ended bytes=N- read was cancelled at its target after a loopback server had streamed megabytes past it: ~4 MB sent per decoded frame.

Related work

Refs #6477, whose retry guarantees (two retries per reader, then one typed line) this keeps.

How

Released-frame cache. A frame no strip shows stays shared in a least-recently-released list capped by the new sharedFrameCacheBytes budget: 32 MiB of JPEG, which at ~6 KB a frame holds ~11 frames for each clip of a 473-clip episode. Object URLs are revoked only on eviction, and a frame any strip still shows is never evicted. Which frame a strip shows is unchanged (same acceptance window).

One reader per source (thumbnailSourcePool.ts). The Input, primary track, size and duration are shared by every clip of a (source, contentVersion) and reference-counted.

  • It closes thumbnailSourceIdleMs (10 s, so a pause between scrolls reuses the index) after its last job, and is retired when the source's contentVersion changes, once the jobs still decoding on it end.
  • Each job keeps its own CanvasSink: a sink reuses one pooled canvas and a job blanks the canvases it saw on release.
  • A failed open is never pooled. Fetch, demux and decode failures retire the reader. One clip's own failed result (no frames decoded, or a frame that will not encode) keeps it.
  • A job ends its claim on the reader when it is aborted, not when it settles, so a job stuck in a read cannot keep the reader open.

Playback hold. While the player store reports isPlaying, the scheduler starts no new visible or overscan video decode. An interaction request (a scrub) still starts, decodes already running finish, and pausing drains the queue. thumbnailPlaybackHold subscribes to the store's state rather than wrapping setIsPlaying, so reset() and a project switch, which write isPlaying: false directly, also release it. VideoThumbnail binds it, which is also the path an app embedding StudioApp runs.

Thumbnail reads (mediaUrlSourceOptions("thumbnail"); playback is untouched, and the probe reader shares only the per-start retry budget below):

  • Bounded ranges, only once Content-Range is readable. A reader bounds bytes=N- to 512 KiB only after one of its own answers was a 206 whose Content-Range it could read and that starts where it asked. Without a readable Content-Range (hidden cross-origin unless the server lists it in Access-Control-Expose-Headers, as on an S3 bucket) mediabunny sizes the file from the answer's Content-Length, so a bounded answer would end the file. Any answer without one keeps that reader unbounded for good.
  • 16 MiB read cache per reader, a quarter of mediabunny's 64 MiB default, since a pooled reader outlives its clip. 8 MiB cost requests when a clip's poster and strip decode together; 16 MiB measured the same as 64.
  • Retry budget per served start byte (the probe reader's too, since it shares getRetryDelay: fix(studio): a clip that cannot be read gives up after two retries with one typed line #6477's two retries per reader are now two per start byte). A resume after progress asks for a new start and gets its own two retries; asking for the same start again uses them up, whatever else the reader reads meanwhile. A 200 is keyed to byte 0, so a server that ignores Range and breaks at the same byte gives up after three fetches.
  • Silent-body stall. A started body that sends nothing for 8 s is aborted and fails as a MediaFetchError "stalled", which mediabunny retries like any read failure; three silent waits at one start (8 + 0.5 + 8 + 1 + 8 = 25.5 s) give up inside the 30 s load timeout and retire the reader. The wait for the answer itself is not timed: it includes Chrome's queue for one of six HTTP/1.1 connections per host, which streaming previews and Studio's event stream hold.
  • cancelReads() aborts a reader's in-flight fetches when it is disposed. mediabunny's Input.dispose() alone leaves a read waiting on a stalled body.

A timeout without progress is the source's failure. The scheduler's timeout aborts with a TimeoutError reason. A job that timed out with no progress in its whole run (its source never finished opening, or it decoded no frame) logs one typed line (thumbnail range fetch failed: network timeout after 0 retries) and retires the reader: this catches a server that never answers and one that trickles bytes but never finishes. A job that waited for an open that finished, or decoded a frame, timed out on a slow link and keeps the reader and its parsed index.

Test plan

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable): not applicable. No page under docs/ describes how timeline thumbnails load; this lands in the release changelog.
  • Comments follow CONTRIBUTING.md "Comments": check-comment-citations.mjs and comment-ratchet.mjs pass

Measured in Chromium 152 (headless Chrome for Testing) with mediabunny 1.45.3 on the episode's real 1080p files. They were served by a Range server that mirrors studio-server's media route (open-ended ranges stream to the end, ETag plus Cache-Control: private, no-cache, 304 on If-None-Match). The workload is 40 clips of 5.6 s spread over one source, each a poster and a 4-frame strip (80 jobs), then the same 80 jobs again after every result was disposed (a scroll-back past the scheduler's cache). origin/main and the branch were interleaved in one sitting, two repetitions each, at load average 7–10. Numbers are relative to each other, not absolute. They were measured at 85e1df2f8, whose runtime code is this head's: the rebase brought no studio changes and the last commit only renames.

First pass, main → branch (both repetitions):

  • Camera A (moov at end), one decode at a time: 6.5 / 6.7 s → 6.8 / 5.4 s, ~1,020 → ~848 requests, 422 / 456 MB → 362 / 363 MB sent.
  • Camera A, two at a time: 6.4 / 5.6 s → 4.2 / 4.2 s, ~1,250 → ~819 requests, 602 / 579 MB → 332 / 335 MB.
  • Camera B (faststart), one at a time: 7.0 / 5.9 s → 4.7 / 4.5 s, ~947 → ~780 requests, 394 / 424 MB → 328 / 321 MB.
  • Camera B, two at a time: 5.1 / 4.9 s → 4.3 / 3.5 s, ~1,211 → ~789 requests, 537 / 556 MB → 324 / 328 MB.
  • Every run: Inputs opened 80 → 1.
  • Scroll-back: 4.7–8.3 s with 80 decode passes → 0 ms, nothing decoded or fetched.
  • Every frame shown is byte-identical to main's (SHA-1 over every tile's JPEG).

Cross-origin, with Content-Range hidden by a server that sends no Access-Control-Expose-Headers: faststart MP4, moov-at-end MP4, MOV, fragmented MP4 and WebM each decode 12/12 thumbnails with main's frames, cross-origin and same-origin. Cross-origin readers stay unbounded; same-origin readers bound.

Stalls, against a server that stalls on purpose (30 s job timeouts):

  • A one-shot stall in the open or mid-decode: the clip recovers at 8.7 s. On main the mid-decode stall left the clip still pending when the harness gave up at 35 s.
  • A source that sends headers, then nothing: each clip fails typed ("stalled") at 25.5 s and the next opens a fresh reader.
  • A source that never answers: each clip fails at 30 s with one typed line, and the next opens a fresh reader.
  • Connection pressure on a healthy server: with 5 of Chrome's 6 connections held and 20 cancel/re-ask bursts over the pass, no failures. With all 6 held for 40 s, no stall; the two clips starved for their whole 30 s fail at the timeout with one typed line each (on main the same timeout is untyped).
  • fix(studio): a clip that cannot be read gives up after two retries with one typed line #6477's give-up through real mediabunny: a server that ignores Range and breaks after 64 KiB, and a region that always answers 503 while another streams, each give up after 3 fetches.

Suites, by exit code: studio test (716 files, 8,174 tests), studio typecheck, fallow audit --base origin/main, root lint and format:check, studio build, check-comment-citations.mjs and comment-ratchet.mjs all pass. Each new test was mutation-checked against the code it guards: 86 mutants, all killed. Where one survived at first, the test was strengthened until it failed.

Follow-ups, not changed here:

  • ThumbnailScheduler.setScrolling is never called outside tests, so "pause rich work while scrolling" never fires.
  • N clips waiting on one failed open log N identical lines.
  • The body of a non-ok answer is not drained.
  • mediabunny's read-ahead worker throws an unhandled rejection when it gives up.

Before

studio-thumbnails-before.mp4

origin/main, an 80-clip timeline cut from three generated 5-minute 1080p sources, encoded like the episode's cameras (H.264 Main, 23.976 fps, ~9.4 Mb/s, a keyframe every ~2 s). (1) The strips fill: 42 of 44 tiles at 1.5 s. (2) Jump ahead and press Play: the new clips' strips load while the preview plays. (3) Pause. (4) Scroll to the end, then back to clips already seen: they come back blank and decode again, 6 of 44 tiles 0.3 s after returning and all 44 by 3.5 s. Mid-scroll, clips not seen yet are blank in both recordings while they load.

After

studio-thumbnails-after.mp4

This branch, same project and steps. (1) The strips fill: 44 of 44 tiles at 1.5 s. (2) While the preview plays, the new clips' strips start no loads. (3) On pause they fill. (4) Scrolling back to clips already seen, every strip is there at once: 44 of 44 tiles at 0.3 s, with no fetch.

🤖 Generated with Claude Code

Follow-ups from review: #6642 (a start's failures expire after a minute) and #6643 (a timed-out job counts sibling clips' frames as its reader's progress).

vanceingalls and others added 14 commits October 27, 2026 22:08
A video thumbnail frame was revoked the moment no mounted strip held it, so
scrolling back past the scheduler's cache decoded every clip again: 80 decode
passes for 40 clips on a 44-minute source, the whole first pass repeated.

Released frames now stay shared in a least-recently-released list capped by
the new sharedFrameCacheBytes budget (32 MiB of JPEG, about 11 frames for
each of a 473-clip episode's clips). Object URLs are revoked only on
eviction, and a frame any strip still shows is never evicted. Which frame a
strip shows is unchanged: the acceptance window is the same predicate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every clip's thumbnail job opened its own mediabunny Input on the source URL,
so 40 clips cut from one 44-minute source opened it 80 times, re-reading and
re-parsing its 1.44 MB index each time (behind a 304, at 13-30 ms an open).

Jobs now share one pooled reader per source: the Input, its primary video
track and the track's size and duration, reference-counted and disposed
thumbnailSourceIdleMs (10 s) after the last job ends, or at once when the
source's contentVersion changes. Each job keeps its own CanvasSink: the sink
reuses one pooled canvas and a job blanks the canvases it saw on release, so
sharing one across jobs would corrupt a neighbour's frames.

A failed open leaves the pool at once, and a job that fails for any reason but
cancellation retires its reader, so the next clip opens afresh. Retries are
still counted per reader, as mediaUrlSourceOptions has done since #6477: two
jobs reading through one reader cannot tell whose read failed, and retiring a
reader whose retries are spent keeps that budget per reader rather than letting
a spent count fail later clips at once. The pool also owns the one
import("mediabunny"), which the decoder now takes from the opened source.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A playing preview shares the machine's video decoder with timeline
thumbnails, and a long many-clip timeline queues hundreds of thumbnail decodes.
While the player store reports isPlaying, the scheduler now starts no new
visible or overscan video decode; a scrub's interaction request still starts,
decodes already running finish, and pausing drains the queue. Image,
composition and waveform work is unaffected.

The new thumbnailPlaybackHold module subscribes to the store's state rather
than wrapping setIsPlaying, so a reset or a project switch, which write
isPlaying: false directly, also release the held work. VideoThumbnail binds it
to the app's scheduler where video thumbnails load, which is also the path
HyperFrames Desktop runs (it embeds StudioApp, not Studio's main.tsx).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
mediabunny reads a URL with open-ended `Range: bytes=N-` requests and cancels
each once it has the bytes it wanted, by when a local server has streamed
megabytes past them: about 4 MB sent for each thumbnail frame decoded. The
thumbnail reader's fetch now asks for at most 512 KiB from N; a shorter answer
than mediabunny wanted makes it ask again from where it stopped (its worker
resumes a capped response when the file size is known, which the first
206's Content-Range supplies). Closed ranges and the probe reader are untouched,
and so is Studio's playback, which does not read through mediaUrlSourceOptions.

Measured on two 44-minute 1080p sources, 40 clips each (a poster and a
4-frame strip per clip), against the same branch unbounded: bytes sent fall
13-22% one decode at a time and 33% two at a time; first-pass wall time falls
4-20%. The frames decoded are byte-identical. Most of what remains is real:
a strip slot whose keyframe is outside its window decodes from that keyframe
forward through its GOP.

A pooled reader also now caches at most 16 MiB of what it read, not
mediabunny's default 64 MiB, since it outlives its clip by up to
thumbnailSourceIdleMs. 8 MiB cost requests when a clip's poster and strip
decode together; 16 MiB measured the same as 64.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A thumbnail reader bounded even its first range request to 512 KiB. When the
answer's Content-Range cannot be read, mediabunny sizes the file as the request
start plus the answer's Content-Length, assuming the answer runs to the end of
the file, so a bounded first answer made a 10 MB file end at 512 KiB. Browsers
hide Content-Range cross-origin unless the server lists it in
Access-Control-Expose-Headers, and Studio reads remote https sources such as
HeyGen's gen-os-static bucket, which does not. With the header hidden, every
format failed: faststart and fragmented MP4 decoded 1 of 12 thumbnails, and
moov-at-end MP4, MOV and WebM none ("no decodable video track").

A reader now bounds its ranges only after an answer of its own was a 206 whose
Content-Range it could read and that starts where the request asked; its
first request stays open-ended, as before the bound. Any answer without such
a Content-Range, a whole-file 200 included, keeps that reader unbounded for
good. Same-origin readers, where Content-Range is readable, still bound.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A reader's retry budget (two retries, from #6477) counted every failure over
the reader's life. Pooled, a reader serves every clip of its source, so the
third transient failure anywhere on the source failed a clip with no retry.
The count now resets whenever a read delivers bytes, so a long-lived reader
keeps the per-clip tolerance a reader had when each clip opened its own, and
three failures in a row still give up. Bytes, not an ok status: a body that
breaks on its first read must still use up the budget. A reader that gives up
is retired, as before, so the next clip opens a fresh one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…urce

The thumbnail job's signal reached only the source's open: keyframe lookups and
the decode loop ignore it, so a job whose response stalled mid-body never ended
and never released its pooled reader. The reader was never idle, so never
closed, and every later clip that needed the same bytes waited on the same
stalled read for the rest of the session. Before pooling, each clip had its own
reader, so only the stalled clip hung.

The scheduler's timeout now aborts with a TimeoutError reason. A job that times
out drops its reader at once; a job cancelled any other way that has still not
ended a load timeout later drops it then; a cancel the job answers, a scroll
away, leaves the reader pooled. Dropping disposes the reader whoever still uses
it, which fails their pending reads too.

Disposing a mediabunny Input does not end a read already waiting on a stalled
body (its orchestrator only flags the worker, which never wakes), so
mediaUrlSourceOptions gains cancelReads(), which aborts every fetch the reader
has in flight; the worker's read then fails and mediabunny rejects the reads
waiting on it. Measured in Chromium with a server that stalls one response:
the stalled clip settles at its timeout, and the same clip asked again, now
and after 11 s idle, decodes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A thumbnail job that failed for any reason but a cancel retired its pooled
reader, so one clip whose frames all failed to decode, or whose canvas would
not encode, made the next clip re-read and re-parse the source's index. Those
two failures are now a ClipResultError, which leaves the reader pooled; fetch,
demux and decoder failures still retire it.

Also pins four pool behaviours mutation testing found unguarded: a reader
closed after sitting idle has left the pool, so the next clip opens a live
one; a version change closes the old reader only once the job still decoding
on it ends; a job cancelled while the track is read stops the open there; and
a scroll-away cancel keeps the reader (the last landed with the stall fix).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
ff16aed98 reset a pooled reader's retry count whenever any read delivered
bytes. That broke the #6477 give-up in two ways, both measured with real
mediabunny:

- A server that ignores Range answers 200 from byte 0, so mediabunny resumes
  at the bytes it got and is sent byte 0 again. With a body that breaks after
  64 KiB, those bytes reset the count every time: 61 fetches in 30 s and the
  read still pending.
- mediabunny reads with two workers. A healthy stream in one region kept
  forgiving a region that always answered 503: 41 fetches in 20 s, pending.

Failures are now counted per start byte, with no reset. A resume after
progress asks for a new start and gets its own two retries; asking for the
same start again uses them up whatever else the reader reads meanwhile. The
start is the byte the answer began at: the asked start for a 206 or a failed
request, 0 for a 200. Traced for the 200 case: requests at 0, 65536 and 65536
are all keyed 0, so the third failure gives up. Both repros now give up after
3 fetches ("network TypeError after 2 retries", "HTTP 503 after 2 retries"),
and failures at different starts, one per clip, never add up.

The fetch and the answer's typing move into typedFetch and typedResponse, so
fetchFn stays under the complexity gate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
dbd820706 dropped a reader when the job using it timed out, but only once the
reader had opened: a timeout during the shared open released the job and left
the stalled open pooled, so every clip of that source joined it and timed out
in turn while clips kept arriving. Dropping on a timeout was also wrong when
bytes were still coming (a slow link's 25 s open threw its parsed index away),
and dropping a reader other clips were using failed them with "Media reads
cancelled", leaving them on "no preview" until they remounted.

A thumbnail read now fails only on silence. A request waiting 8 s for its
answer, or for its next bytes, is aborted and fails as a MediaFetchError
"stalled", which mediabunny retries like any read failure: a resume after
progress asks for a new start and continues, and three silent waits at one
start (8 + 0.5 + 8 + 1 + 8 = 25.5 s, inside the 30 s load timeout) give up,
which retires the reader. This covers the open and the decode alike, keeps a
reader whose bytes are still arriving, and never disposes a reader another
clip is using, so the job-level abandon and the scheduler's TimeoutError
reason are removed. Measured in Chromium against a stalling server:

- one-shot stall in the open: the clip decodes at 8.7 s (was: it and the next
  clip both timed out at 30 s);
- one-shot stall mid-decode: decodes at 8.7 s (main: hangs);
- a source that never sends a byte: each clip fails typed at 25.5 s and the
  next opens a fresh reader.

Each request gets its own AbortController, which follows mediabunny's signal
and the reader's cancel without AbortSignal.any; every way a request ends
unlinks it from the reader's long-lived cancel signal.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…arded

- A bounded answer's Content-Range keeps the reader bounded: a closed-range
  pattern that matched only open ranges flipped it unbounded after the first
  bounded request, undoing cf8f4554e.
- A frame that will not encode, through toBlob or OffscreenCanvas
  convertToBlob, is the clip's failure and keeps its reader. The
  convertToBlob rejection is now a ClipResultError too; it escaped as the
  browser's own error and retired the reader.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Four guarantees of the thumbnail reader's per-request watchdog had no test;
each mutant that broke one passed the suite:

- a silent body fails as "stalled" and aborts the fetch under it, so the
  stalled connection is not left open;
- every way a request ends unlinks it from mediabunny's signal: an answered
  body, a rejected fetch, a 503, no body, a broken body, a cancel;
- a probe reader's body is never timed, however slow;
- a request made after the reader's reads were cancelled starts aborted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… started

The 8 s stall rule also timed the wait for a response's headers, and that
wait includes Chrome's queue for one of six HTTP/1.1 connections per host.
Desktop serves all media from one loopback host, where Studio's event stream
and streaming previews hold connections. Measured in Chromium against a
healthy server: with five of six connections held and 20 cancel/re-ask bursts
over the 40-clip pass, no false stall; with all six held for 40 s, two clips
failed as "stalled". The watchdog now times only a started body.

A server that never answers is now caught at the job's own timeout. The
scheduler's timeout again aborts with a TimeoutError reason. A job that timed
out without progress in its whole run (its source never finished opening, or
it decoded no frame) failed because of its source: a source that never
answers, or one that trickles bytes and never finishes. It logs one typed
MediaFetchError line ("network timeout") and retires its reader. A job that
waited for an open that finished, or decoded a frame, timed out on a slow link
and keeps the reader and its parsed index. Bytes alone are not progress, or a
trickling source would keep its reader for good.

An aborted job also ends its claim on the reader at the abort, rather than
when it settles: a job stuck in a read that ignores its signal no longer keeps
the reader open. A retired reader still waits for the clips decoding on it.

Measured in Chromium: a source that never answers fails each clip with one
typed line at 30 s and the next opens a fresh reader; with all six
connections held for 40 s, the two starved clips fail typed, none as a stall.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The comment ratchet (scripts/comment-ratchet.mjs) fails a source file whose
share of comment lines rises over its merge-base, and a new file above its
package's share. Five files of this branch did. The reasoning they carried is
in the commit messages and the PR description; what stays in code is named:
SILENT_BODY_STALL_MS, POOLED_READER_CACHE_BYTES, failuresByStart,
inFlightReads, decodedByJob, timeoutWithoutProgress and madeProgress. No
behaviour changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Oct 28, 2026
…e watermarks

Commit axis to bf7ebe9 (merge 2fa8452), PR axis to heygen-com#6629 and issue
axis to heygen-com#6610, each item with its verdict, head SHA and trigger in
docs/DECISIONS.md 2026-10-28. Upstream PR heygen-com#6512 is adopted (554dbb7).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy did not run: 0 of 5 shards reported results; see the shard jobs.

Nothing was compared with the base branch.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE · 0 blocker · 0 important · 3 nits · 4cd77d31

Pool ref-counting, frame-cache accounting, and the abort/timer cleanup in mediaFetch all held up when I traced them. The bounded ranges look safe against mediabunny 1.45.3's UrlSource: they turn on only after a Content-Range with a total, so the file size is already known and an early end resumes without spending retry budget. Nits are inline. One more: the body says the probe reader is untouched, but it shares getRetryDelay, so #6477's "2 retries per reader" budget is now per start byte for the probe too. That's harmless in practice, but worth fixing in the description.

Scope

Read end to end: every runtime file in the diff. Also read: mediabunny 1.45.3 UrlSource._runWorker and retriedFetch. Ran a separate adversarial pass over pool lifecycle, the stall watchdog, the per-start retry budget, and the playback hold. CI and PR captures are green at this head. There were no prior reviews.

— Genesis reviewer

throw error;
}
const releaseOnAbort = () => {
const madeProgress = source.waitedForOpen || resources.decodedByJob > 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "progress" here only counts this job's own progress. Say a poster job gets a reader that is already open, maybe while a sibling job is decoding on it. On a slow link, or while Chrome's 6 connections are held, it takes more than 30 s to reach its one frame. It then logs network timeout after 0 retries and retires a healthy reader, so the next job has to re-read the moov (at the 3.4 GB offset for camera A). Recording progress on the pooled entry instead, as any completed read or decoded frame, would separate "source dead" from "this job was slow". That matches the slow-link case the body says keeps the reader.

export function mediaUrlSourceOptions(reader: MediaReader) {
let failures = 0;
// Per start byte: a resume asks for a new start, while asking for one again uses its budget up.
const failuresByStart = new Map<number | null, number>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: failuresByStart only grows, and a pooled reader can now live for many minutes, because each new job resets its 10 s idle timer. Two transient failures at start S early in a session (S comes back once the 16 MiB cache evicts it) make a third failure at S, 20 minutes later, give up immediately. That retires the reader and shows the clip as an error. Deleting the entry once a read from that start delivers bytes would keep "no progress at this start" accurate.

@vanceingalls
vanceingalls added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit a54921e Oct 28, 2026
111 of 112 checks passed
@vanceingalls
vanceingalls deleted the perf/studio-thumbnail-source-cache branch October 28, 2026 07:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant