Skip to content

perf(build): capture frames in parallel browsers and time-limit every step that can stall - #31

Merged
umiyosh merged 4 commits into
masterfrom
perf/parallel-capture
Oct 25, 2026
Merged

umiyosh merged 4 commits into
masterfrom
perf/parallel-capture

Conversation

@umiyosh

@umiyosh umiyosh commented Oct 25, 2026

Copy link
Copy Markdown
Owner

Summary

Renders MP4s about 4-5x faster by capturing frames in several browsers at once, and puts a time limit on every step that could stall, so a stuck film page or ffmpeg fails with its name instead of hanging.

The two go together: the time limits apply to every page the parallel capture opens, and if parallel capture had not paid off, both would have been dropped.

Closes #25. Closes #26. Part of #16.

Changes

Parallel capture (#25)

  • openFilm(dir, { pages }) opens the film in pages browsers, one page each, and hands each frame or layout sample to a free page.
  • inOrder() (new scripts/inorder.ts) keeps up to pages frames in flight and yields them in film order, so ffmpeg still gets frames in order and at most pages frames are held in memory.
  • build encodes through it; check, stills and build run the layout check through it. Samples are judged in film order, so the "two samples in a row" rule is unchanged.
  • Default: half the cores, at most 8 (7 on a 14-core machine). MOTION_RENDER_PAGES overrides it (README, usage).

Why one browser per page: several pages in one browser were no faster than one (5.5 fps for 1 page, 5.6 for 2, 3.8 for 4); the browser kept about one core busy however many pages it had. Separate browsers scale with their number:

pages (browsers) capture only, 300 frames fps CPU
1 75.5 s 4.0 1 core
2 22.4 s 13.4 2 cores
4 11.2 s 26.7 3.9 cores
6 7.6 s 39.7 5.8 cores
8 6.0 s 49.8 7.3 cores

The first launch runs alone, since it may download Chromium; the rest start in parallel.

Time limits (#26)

  • scripts/limit.ts (new): limit(promise, ms, what, onTimeout) fails with <what> did not finish in <n>s after running onTimeout to stop what is stuck.
  • Each call into a film page (load, seek(t), ready, screenshot, layout snapshot, Motion's load / frame(t) / consoleLog) is limited to 60 s and names the call and time, e.g. the film page's __film.seek(12.3) did not finish in 60s. Playwright's evaluate has no timeout of its own.
  • runFfmpeg / volumeStats kill ffmpeg after 5 min and name the file. The encoder in build is limited per frame (60 s to take each frame) and for the final mux (5 min), since it runs as long as capture.

Measurements

Build of the WIF film (57.3 s, 1719 frames) on an Apple M4 Pro (14 cores), master and this branch alternately, while other jobs kept the load average at 9-22:

round master this branch (7 pages)
1 179.2 s 35.2 s
2 242.6 s 67.0 s

With the time limits alone (1 page) builds were no slower than master (251 s vs 270 s on average in an earlier alternating run).

Output: frames are not bit-identical to master's, but master is not bit-identical to itself either (#29). PSNR master vs master: average 59.0 dB, lowest frame 52.5 dB; master vs this branch: average 54.9 dB, lowest 40.8 dB. The lowest frames differ only by sub-pixel shifts along text and box edges during a push-in; a side-by-side of both MP4s was reviewed and looks the same. In tests, frames from 2 pages are byte-identical to 1 page.

Test plan

  • make check: lint, format, typecheck, 102 tests with the coverage gate, quality gates
  • New tests: inOrder (order, at most ahead in flight, first failure passed on), capturePages (env var and its validation), 2 pages give the same frames and layout issues as 1 page, limit, a stuck ffmpeg is killed and named, a film whose seek spins forever fails within the limit naming __film.seek(1.5)
  • By hand: stills on a real film with a stuck seek exits after 61.8 s with the film page's __film.seek(2) did not finish in 60s; no browser process is left behind
  • By hand: builds of the WIF film with master and this branch, side by side
  • CI

@umiyosh
umiyosh merged commit d6037dd into master Oct 25, 2026
1 check passed
@umiyosh
umiyosh deleted the perf/parallel-capture branch October 25, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant