Streaming appends: tail-only GPU uploads, coalesced tier refines, throttled reopen re-sync#188
Open
Alek99 wants to merge 5 commits into
Open
Streaming appends: tail-only GPU uploads, coalesced tier refines, throttled reopen re-sync#188Alek99 wants to merge 5 commits into
Alek99 wants to merge 5 commits into
Conversation
…loads, coalesced tier refines, throttled reopen re-sync Streaming appends paid three per-tick O(N) costs beyond the wire payload itself: - The client destroyed and rebuilt every affected GPU trace with fresh bufferData allocations per tick. Now kernel encode offsets stay sticky across appends (Column.suggest_offset keeps the last shipped offset while every value stays within one span of it, at most 1 f32 mantissa bit vs a fresh midpoint), so consecutive append payloads keep byte-identical prefixes, and the client extends a direct scatter/line in place: the same buffer objects (VAO attachments stay valid) grow a capacity-doubling data store, mirroring Column.append, and each tick uploads only the appended tail via bufferSubData. Any encoding change (re-centered offset, expanded color/size domain, tier/style/keys change) falls back to the full rebuild. - Every append re-requested the current view with delay 0, so a streaming decimated line paid a full M4 round trip per tick. Appends now coalesce through the existing debounce machinery (delay 120 ms, maxWait 300 ms), and an at-home stream skips the decimated re-request entirely when the payload's recorded decimation_px (new spec field, §28) already covers the plot width. - FigureWidget.append re-synced the spec/buffers traits on every tick beside the custom message, shipping every payload twice. Live clients ignore trait updates, so the reopen-state re-sync now coalesces to at most one per second (leading edge immediate; trailing flush on the running event loop so the final streamed state always lands). Measured in headless Chromium (scripts/append_stream_smoke.py, 20k rows x 6 buffers, 200-row ticks): steady-state GPU upload drops from ~485 KB and 6 buffer reallocations per tick to 4.8 KB tail-only (~100x, scaling with N); at-home view round trips 5/5 -> 0/5, zoomed burst 6/6 -> 2/6; widget wire bytes per tick halve. The smoke also pins pixel-equivalence between N in-place appends and a fresh render of the final payload, and runs in CI beside the other stdlib gates.
Greptile SummaryThis PR reduces the client-side cost of streaming appends. The main changes are:
Confidence Score: 5/5This looks safe to merge. No blocking issues found in the changed code.
What T-Rex did
Important Files Changed
Reviews (1): Last reviewed commit: "Remove the temporary branch verification..." | Re-trigger Greptile |
Merging this PR will not alter performance
Comparing Footnotes
|
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.
Follow-up to the streaming-append audit: fixes items #3–#5 (the client-side O(N)-per-tick costs). The wire-level delta path (#2) and the multi-trace multiplier (#163) remain future work; with these three fixes the per-tick client cost drops from O(N) to O(rows appended) everywhere except the payload bytes themselves.
What changed
#3 — GPU buffers grow in place instead of realloc-per-tick (
js/src/54_kernel.ts,python/xy/columns.py)Column.suggest_offsetis now sticky across appends — once shipped, a column keeps its encode offset while every value stays within one span of it (≤1 f32 mantissa bit vs a fresh midpoint; a right-growing stream never exceeds that bound, and §16 deep-zoom re-centering is untouched). This makes consecutive append payloads byte-identical prefixes of each other.offset/scale/dtype, same channel shapes/domains, same style, no transition keys, nocurve:"smooth"/step) extends its existing GPU buffers with a tail-onlybufferSubData. Buffer objects are retained so VAO attachments stay valid; data stores grow with doubling capacity, mirroringColumn.append. Any precondition failure (re-centered offset, expanded continuous color/size domain, tier flip past a threshold, etc.) falls back to the full rebuild, which is always correct. The client derives prefix-compatibility entirely from the previous vs fresh spec — no wire-protocol change.#4 — tiered refines coalesce to burst cadence instead of one kernel round trip per tick (
js/src/54_kernel.ts,python/xy/_payload.py)_applyAppendno longer re-requests the view withdelay: 0; it uses the existing debounce machinery withdelay: 120 / maxWait: 300, so a continuous stream pays at most one M4/density round trip per 300 ms.decimation_px(§28: every decimation decision is recorded). Parked at home, the append payload was already M4-decimated over exactly that view — when the recorded px covers the plot width, the client skips the decimated re-request entirely.#5 — the widget no longer ships every append payload twice (
python/xy/widget.py)FigureWidget.appendused to re-sync thespec/bufferstraits and send the same spec+blob as the custom message every tick. Live clients ignore trait updates (they exist only for notebook-reopen state), so the trait re-sync now coalesces to at most one per second: leading edge immediate, trailing flush armed on the running event loop so the final streamed state always lands. Without a running loop (plain scripts, tests) every append flushes immediately — the old behavior.Measured
Headless-Chromium smoke (
scripts/append_stream_smoke.py, new CI gate: 20k rows, 6 per-point buffers, 200-row ticks, run before/after on the same harness):bufferDatareallocsWidget wire accounting (CI run on this branch,
Figure.scatter+ 1-row appends):append_datakernel timeTests / verification
scripts/append_stream_smoke.py(stdlib-only, likerender_smoke_nonumpy): asserts tail-only uploads after warm-up, pixel-equivalence between incremental appends and a fresh render, zero at-home re-requests, and burst coalescing. Wired intoci.yml,make check-browser, and the contributing table.render_smoke_nonumpystream probe updated to the new contract: in-place growth when encoding is unchanged, rebuild when a payload re-encodes (new offset).tests/test_streaming.py: sticky offsets + byte-identical prefixes across appends (geometry and stable-domain channels), expanding channel domain rewrites only that channel, re-centering guard,decimation_pxrecorded.tests/test_widget.py: throttled reopen re-sync (burst parks, trailing flush lands the final state) and immediate-flush behavior without an event loop.static/*.jsbundles were regenerated with the pinned vite toolchain).wire-protocol.md§4/§5,rust-engine.md§5, design-dossier §4, contributing gate table.Known scope limits (documented in the spec text): the fast path covers direct scatter/line — smooth/step-expanded lines, keyed transitions, and expanding continuous color/size domains rebuild as before (a shader-side normalization that would make channel domains tail-stable belongs to the #2 delta-frame redesign).
Generated by Claude Code