Repository navigation
perf(core): Safari stops a long video's clips from loading the whole file at once - #7436
Conversation
e5671b8 to
3614aca
Compare
c2c2d4c to
00dcbfa
Compare
…aves short files as they were
…il each one is due
… of fetching the whole file
… began reading it at parse
6a25337 to
a97070e
Compare
…point rather than holding it
There was a problem hiding this comment.
Verdict: APPROVE · 0 blocker · 0 important · 2 nits · head 2bfc550b
The three fails without the fix: claims check out. With the giveBackWhenDue call taken out of preloadNearPlayhead, init.longSourceHold.test.ts fails 3 of 4. With giveBackWhenDue taken out of swapToProxy, the held-clip swap test fails. With !isLongSource taken out of decodesFirst, the long-file test fails. All 530 tests in the six touched suites pass at this head, and CI is green.
Scope
I read every non-test file end to end. I also checked the other places that read src: previewVideoClipsAt only sees a held clip after preloadNearPlayhead has given it back, because the same deferral rules gate both. The proxy URL keeps #t=, which does no harm because the fragment never reaches the server. I did not repeat the Simulator measurements.
What
On Safari in the player, a film cut from one long file (32 MB or more) no longer stands still for about 25 s while it loads. Only the clips on screen and the next take read the file. Every other clip of that file holds no source until it is due, and a clip opens at its in-point instead of at the head of the file. Sound for such a file plays from its element instead of first fetching the whole file to decode it. A smaller file, Chrome, Studio's preview and renders behave as before.
Why
Every clip of one long file opened the file at page load, and WebKit reads tens of MB from the head of the file for each one, whatever its preload. On top of that, the tap's sound unlock (
primeMediaForLaterStarts) callsplay()on every later clip, which opens its file even atpreload="none", and the decode path fetched the whole 68 MB file at the tap. All of this shares one link. On an 8 MB/s link the clip on screen starved and the playhead stood still for about 25 s.Measured in Safari on an iOS 26.5 Simulator, with the page's media held to 8 MB/s across all connections, over the first 25 s of a film of 10 takes cut from one 68 MB, 321 s file. 10 interleaved plays per arm, median (min to max), page readings:
Related work
Refs the Safari long-file loading work. Replaces this PR's earlier fetch-ahead with eight slots, which was bimodal (4.9 to 32 s in the same setup). #7447 (audio from the element through a decode length limit) is superseded by the long-file check here and can close.
How
preloadNearPlayhead(init.ts) already decides each tick which clips are near (on screen, armed next on its track, or next take). A held clip gets its source back there (giveBackWhenDue) before the media sync plays or seeks it. A clip that has passed is released byreleaseMedia, which holds it again instead of re-setting its source.unproxiedSrc(proxySrc.ts) returns the source the author wrote while the runtime holds a clip or has written its fragment. The clip grouping by file, load diagnostics, the picker, the timeline asset url and proxy swaps read it. A script or binding that sets another source ends the hold, and that source is the one the clip plays. A proxy swap on a held clip gives the file back first, so the copy replaces the right file.preloadMedia.tsasks once per file for its size with a one-byte range request. For a file of 32 MB or more, on WebKit and in the player frame only, a later clip that WebKit has not started reading is held with no source. Every other clip opens at#t=<in-point>. A clip WebKit already began reading at parse opens at its in-point instead of being held: holding those too cut requests from about 300 to 80 but made the stand-still 4.8 to 14.9 s, because each take then fetched its metadata under load when due.decodesFirst(init.ts) leaves a long file on its element: its decode fetches the whole file first.srcattribute exactly as before when the runtime has not touched a clip. Search first: the hold reusesstopMediaDownload/releaseMedia.git grep "bytes=0-0" packages/core/srcfinds one other one-byte request,proxyWait.ts, which polls a proxy copy until it is served and reads no size, so the size check is new.handleMetadataForProxy,handleErrorForProxy) now share one path, because the file's duplication check flagged their 19 identical lines once the file changed.@hyperframes/producerunpacked goes from a budget of 3,130,000 to 3,162,274 bytes. Base 3,160,731, head 3,162,274: the +1,543 bytes are exactly the growth ofdist/hyperframe.runtime.iife.js(651,645 to 653,188), the only producer file this changes. The producer ships that one runtime file because renders inject it into the render page, so the WebKit-only code rides along there unused. Keeping it out would mean separate preview and render runtime builds, a separate change. Main already sat 0.98 % over the old budget.Test plan
init.longSourceHold.test.ts: held past the next take and still naming its file; given back at its in-point on a paused seek; a binding set while held is kept and played; let go once passed and given back on a seek back.preloadMedia.test.ts: held later clip, held again after release, a bound clip not held, a clip WebKit already read opens at its in-point, plus the earlier in-point cases.mediaProxy.test.ts: a held clip's swap copies its own file.init.test.ts: a long file's clip stays on its element.webAudioTransport.test.ts,mediaLoadFailure.test.ts.compositionLoader.test.tsfail there the same way on unmodified main (box setup), and pass in CI.init.longSourceHold.test.tsfails 3 of 4 with thegiveBackWhenDuecall removed frompreloadNearPlayhead; "swaps a clip held with no src after giving it back its file" inmediaProxy.test.tsfails with its line removed; "leaves a clip of a long file on its element: decoding it would fetch the whole file first" ininit.test.tsfails with the long-file check removed fromdecodesFirst.