fix(io): stop re-fetching bytes the open already has - #285
Conversation
Measured with aetherctl against a Range-logging origin while looking for what was left of #281. Two defects in the persistent read loop, both of which turn one round trip into three on a container with a large trailing object. A range that has been delivered IN FULL clears `activeTask` exactly as a dropped one does. The no-connection branch reconnected at the READ position regardless, which resets `winStart` and drops everything still resident, so a consumer slower than the transfer re-fetched what it had just been handed. The parse pass is exactly that consumer: it reads one 256 KB AVIO buffer at a time while the transfer runs at line rate. A 764450 B trailing `moov` cost three connections and 1506918 delivered bytes, 1.97x its own size, each reconnect landing exactly one AVIO buffer further on. The branch already excluded the hard-cap end for this precise reason; the reasoning was never extended to a range that simply finished. The EOF decision then sat BELOW that reconnect, so a read at exactly `fileSize` issued `bytes=<fileSize>-` first and took an empty 206, whose reconnect reset `winStart` past the last byte and dropped the window the parse was still reading. That was the third connection. Serving what is in hand first also revives the #220 frontier refill, which could not run while the no-connection branch preempted it on every completed range. Measured before and after on four MP4 layouts, all above the 32 MB range size so their tails lie outside the first window: moov at end, 764450 B 5 requests -> 3 fragmented, mfra at end 5 requests -> 4 moov at end, 23337 B 2 requests -> 2 (already optimal) faststart 2 requests -> 2 (already optimal) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE
#281 parked the open window across a parse seek, cut from `winStart`, on the reasoning that the demuxer returns to the window's start. Measured against four MP4 layouts with aetherctl and a Range-logging origin, it returns to the FILE's start: landings of 48, 1161, 5752, and 265159 in a field trace. Those coincide only while the parse seeks away before reading anything, which is the moov-at-end case and nothing else. A fragmented MP4 reads 33 MB of the head first, so `winStart` has long left byte zero and the parked copy begins megabytes past everything the return trip asks for. It covered nothing. The head is now collected as the data connection delivers it, capped at the same 4 MB and released with the rest of the open-phase state. Collecting it later is not possible: `trimWindowLocked` drops the head as the parse reads forward, so by the time a seek asks for a copy there is nothing left to copy. Measured against an origin with 300 ms of latency per request, since a loopback one cannot show an overlap: fragmented, mfra at end 1732ms / 4 requests -> 1219ms / 3 moov at end, 764450 B 678ms / 3 requests -> 708ms / 3 moov at end, 23337 B 390ms / 2 requests -> 385ms / 2 faststart 420ms / 2 requests -> 420ms / 2 The parked window stays. It covers a return to a mid-file offset, which the head cannot, and it did not fire on any layout measured here, so it is kept on the argument rather than on evidence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE
Kept in the previous commit on an argument rather than on evidence: it covers a return to a mid-file offset, which the retained head cannot. Measured across six container layouts, four MP4 and two MKV, it served no read at all. The demuxer returns to the head of the FILE in every one of them, which is what the retained head covers, so the park was carrying a premise the measurement contradicts and up to 4 MB of transient footprint next to the copy that actually serves. Layout matrix, 300 ms of origin latency per request, requests and which span answered: faststart.mp4 2 nothing (the suffix fetch is spent, deliberately) moovend.mp4 2 tail prefetch (trailing moov, 23337 B) bigmoov_end.mp4 3 retained head (trailing moov, 764450 B, too big to fit) frag.mp4 3 tail prefetch (mfra) + retained head cues_end.mkv 2 nothing; matroska does not read its Cues at open cues_front.mkv 2 nothing Request counts are unchanged by the removal on all six. Also measured, and the reason the speculative suffix fetch stays despite paying off on only two of the six: at 250 KB/s with 300 ms latency it costs 252 ms on a layout that does not use it (64 KB of link time, exactly) and saves 360 ms on one that does. Those are equal at about 2 Mbit/s, which cannot stream the material this engine opens; at 10 Mbit/s the cost is 51 ms against a round trip that the #281 field trace measured at 438 ms. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE
|
Both points I had left on an argument rather than on a measurement are now measured, and one of them did not survive. The parked window is goneIt served no read on any of six container layouts. The demuxer returns to the head of the FILE in every one of them, which the retained head covers, so the park was carrying a premise the measurement contradicts plus up to 4 MB of transient footprint next to the copy that actually answers. Request counts are unchanged by the removal on all six.
Worth recording separately: matroska does not read its Cues at open, wherever they sit. The speculative fetch is spent on both MKV layouts. The speculative suffix fetch stays, and now there is a number for itIt pays off on two of the six, so the question was whether the other four are paying for them. Five runs per point at 250 KB/s with 300 ms of latency, spread under 30 ms:
The cost is exactly the link time of 64 KB (64 KB / 250 KB/s = 256 ms). The benefit is one round trip plus the sequential tail fetch it replaces. They are equal at about 2 Mbit/s, which cannot stream the material this engine opens; at 10 Mbit/s the cost is 51 ms against a round trip the #281 field trace measured at 438 ms. Keeping it. 1435 tests green. |
…a suffix-blind origin is asked once #281 retest, two findings from the reporter's 6.5.2 traces. The head was released one read too early. `markOpenPhaseFinished` dropped it on the last read of the parse, on the reasoning that a far seek from there on is a scrub. It is not: after a trailing-index parse the anchored connection sits at the END of the file, so playback's FIRST read is a backward one, and it lands at the head on every layout measured (48 and 263303 under aetherctl, 5752 in the field trace, where it cost a fresh connection whose first byte took 865 ms). No probe-based measurement could see this, because `probe` exits at exactly that call, which is why the PR #285 matrix reported the layouts as settled. Measured against a 300 ms origin on a fragmented fixture, playback's first read moves from a new connection at 263303 to a copy out of the head, and the next connection is deferred to 4194304: one round trip off the first frame and 3.93 MB that were already resident no longer re-fetched. The head is now released by the first post-open read it cannot answer instead. The reporter's origin answers `bytes=-65536` with a 200 and the whole file, on every open. The delegate still hangs up at the response header, so no body is taken, but the request was being paid per open: a second connection opened at the same instant as the data connection whose first byte IS the cold start, sharing that uplink, against a server that had already shown it cannot serve it. Its answer is now remembered per origin for the session, and the log says so rather than going quiet. Only the origin's own answer latches on the first occurrence; a transport failure takes two, since a link bad enough to lose this request loses others. Verification lines for the field: `retained file head served playback's first read at <offset>` and `tail prefetch skipped: this origin declined suffix ranges earlier this session (<reason>)`, both of which exist before being named this time. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE
Follow-up to #281, from measuring rather than reasoning. aetherctl against a Range-logging origin, four MP4 layouts, all sized above the 32 MB range so their tails lie outside the first window.
Three defects, all the same shape
A range that finished delivering was fetched again. A completed range clears
activeTaskexactly as a dropped one does, and the no-connection branch reconnected at the READ position regardless, which resetswinStartand drops everything still resident. The parse pass is precisely the consumer this bites: it reads one 256 KB AVIO buffer at a time while the transfer runs at line rate. A 764450 B trailingmoovcost three connections and 1506918 delivered bytes, 1.97x its own size, each reconnect landing exactly one AVIO buffer further on. The branch already excluded the hard-cap end for this reason; the reasoning was never extended to a range that simply finished. Serving what is in hand first also revives the #220 frontier refill, which could not run while this branch preempted it on every completed range.A read at EOF opened a connection for it. The EOF decision sat below that reconnect, so a position at exactly
fileSizeissuedbytes=<fileSize>-and took an empty 206, whose reconnect resetwinStartpast the last byte and dropped the window the parse was still reading.The parked window was cut from the wrong place. #281 parks at seek time from
winStart, on the reasoning that the demuxer returns to the window's start. It returns to the FILE's start: landings of 48, 1161, 5752 here and 265159 in the field trace on #281. Those coincide only when the parse seeks away before reading anything. A fragmented MP4 reads 33 MB first, so the parked copy begins megabytes past everything the return asks for and covered nothing. The head is now collected as the data connection delivers it, which is the only point at which it can be:trimWindowLockeddrops it as the parse moves forward.Measured
Request counts, loopback:
Wall clock, same origin with 300 ms of latency per request (loopback cannot show an overlap):
Tests
Three new, each verified red against the old behaviour: a completed range is read out of the window, a read at EOF opens nothing, and the return to the head survives a parse that read past the window's start. 1435 green.
Kept on the argument rather than on evidence
The parked window stays. It covers a return to a mid-file offset, which the retained head cannot, and it did not fire on any layout measured here.
🤖 Generated with Claude Code
https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE