Read a window by playing it, not by seeking to every frame - #55
Merged
Conversation
Pinning was the slowest thing in the app and it was also wrong. Measured on a 1080p clip of forty seconds: reading it took 20.5s and pinning the twenty-six moments took 40.1s -- longer than the video. At the candidate ceiling of a hundred and sixty that is four minutes. The reason is that it seeked to each frame in turn and gave each seek 400ms before drawing whatever was on screen anyway. A seek on a clip that size takes 342ms on average, and 56 of 84 of them ran past the allowance -- every one with the element still seeking when the frame was taken. So two thirds of the comparisons made by the pass whose whole job is landing on the exact frame were between stale pictures. It now seeks once per run of nearby moments and reads the stretch by playing it, taking frames through `requestVideoFrameCallback`. That only fires for a frame that was really presented and carries the frame's own time, so a frame the machine could not keep up with goes missing rather than being mistaken for its neighbour. Windows within four frames of each other are read together, since stopping costs more than reading a few frames nobody asked for. Played at half speed, which sounds backwards for the pass that was slow. At full speed one frame in eighty was never presented, and the frames a decoder struggles with most are the ones straight after a cut -- which is to say the frames this exists to find. At half, none went missing, and it costs almost nothing because the window is three or four frames wide and the seek that reaches it dominates either way. pass 1 20.5s -> 20.4s pass 2 40.1s -> 19.8s total 60.7s -> 40.2s stalls 25 long tasks, 1901ms -> 4 long tasks, 242ms Two other faults fell out of it: - Stopping during the pinning pass did not stop. The guard on the button admits it, so the screen cleared and said "stopped reading the video", and then the pass -- which was never given the signal -- ran to the end and put its moments up regardless. It takes the signal now, and the caller checks it before publishing. - Asking a video element for the time it is already showing fires nothing, so the wait ran to its cap. The first stretch of a pass often starts at zero, which is where a clip already sits. Seven unit tests on which stretches are read together, each checked by putting the fault back. Two browser tests: one that a moment lands on the frame the picture changed on, one that stopping during pinning stops. The first fails on the code this replaces, at 5 of 8 moments within a frame against 7 of 8. It reads 1080p in real time, so it wants the machine to itself and skips above one worker; CI runs one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ux1kydvUkLRoMbHp82ofDi
The test that says a moment landed on the frame the picture changed on read the clip back by playing it and taking frames as they came. That passed on its own and failed in a full run of the suite: eight of nine readings came back with a hole in them, because a 1080p frame the machine is too busy to present is one the reader never sees, and a hole shifts every frame after it one place along. Guarding it with a skip above one worker was not enough -- the failure came from a single-worker run. So the check seeks instead, one frame at a time, waiting on each seek with nothing to run out. That is the thing the code under test stopped doing, and for a good reason: it is slow. It is the right way round here. This has nine moments to look at rather than a hundred and sixty, and a check has to be sounder than the thing it is checking. It now discriminates plainly and repeatably. Against the pass this replaces, twice: frames off: 1 3 1 3 1 2 2 1 3 frames off: -2 1 -2 2 -2 2 2 -1 2 Against the pass that is here, nothing off by more than one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ux1kydvUkLRoMbHp82ofDi
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is the "without lagging" half of your request. I measured where the scan's time actually goes on the size of clip you named — 1080p, several minutes — and one of the two passes turned out to be both the slow one and a wrong one.
What the measurement said
A 1080p clip of forty seconds:
Pinning twenty-six moments took longer than the video itself. At the candidate ceiling of a hundred and sixty that is four minutes.
Why, and the part I did not expect
It seeked to each frame in turn and gave each seek 400ms before drawing whatever was on screen anyway. A seek on a clip that size takes 342ms on average, and I measured 56 of 84 running past the allowance — every one with the element still seeking when the frame was taken.
So two thirds of the comparisons made by the pass whose entire job is landing on the exact frame were between stale pictures. It survived that because a wasted comparison scores about zero and simply loses, so the answer degraded rather than collapsing — but the window was really being sampled at a third of the frames it thought, with nothing saying so.
The size matters, and it is exactly your case:
What it does now
One seek per run of nearby moments, then the stretch is read by playing it and taking frames through
requestVideoFrameCallback— which only fires for a frame that was really presented and hands over that frame's own time. A frame the machine could not keep up with goes missing rather than being mistaken for its neighbour. Windows within four frames of each other are read together, since stopping costs more than reading a few frames nobody asked for.Played at half speed, which sounds backwards for the pass that was the slow one. At full speed one frame in eighty was never presented, and the frames a decoder struggles with most are the ones straight after a cut — which is to say precisely the frames this exists to find. A dropped one there does not read as noise, it reads as the change happening a frame later than it did.
Half costs almost nothing in the end, because the window is three or four frames wide and the seek that reaches it dominates either way — against the 4680ms a window this replaces.
Two other faults that fell out of it
Nine tests, and two wrong readings worth naming
Seven unit tests on which stretches get read together, each checked by putting the fault back — never joining, joining everything, not extending a run's end, not sorting.
Two browser tests: a moment lands on the frame the picture changed on, and stopping during pinning stops. The first fails plainly on the code it replaces, twice:
Getting there took two wrong readings, both mine and both worth recording:
379 unit tests, 102 browser tests, build clean.
What this does not do
Pass 1 is unchanged, and I do not think I can fix it here. It plays the clip at 2× and reads presented frames, and a browser presents about thirty frames a second whatever
playbackRatesays — so fifteen measurements per second of video costs half the clip's length, and that ceiling is the display, not the code. Playing faster is the trade this repo already measured: twice as fast finds 18 of 40 hits instead of 39.The one route past it is WebCodecs
VideoDecoder, which decodes independently of presentation — but it needs a demuxer for the container and is its own piece of work, not something to bolt onto this.For a five minute 1080p clip you are now looking at roughly two and a half minutes for pass 1 and about a minute and a half for pass 2, down from about four minutes total — with a stop button that genuinely stops.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Ux1kydvUkLRoMbHp82ofDi
Generated by Claude Code