Skip to content

Streaming appends: tail-only GPU uploads, coalesced tier refines, throttled reopen re-sync#188

Open
Alek99 wants to merge 5 commits into
mainfrom
claude/trace-append-perf-sbf211
Open

Streaming appends: tail-only GPU uploads, coalesced tier refines, throttled reopen re-sync#188
Alek99 wants to merge 5 commits into
mainfrom
claude/trace-append-perf-sbf211

Conversation

@Alek99

@Alek99 Alek99 commented Jul 22, 2026

Copy link
Copy Markdown
Member

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)

  • Kernel: Column.suggest_offset is 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.
  • Client: an affected direct scatter/line whose encoding is unchanged (same column offset/scale/dtype, same channel shapes/domains, same style, no transition keys, no curve:"smooth"/step) extends its existing GPU buffers with a tail-only bufferSubData. Buffer objects are retained so VAO attachments stay valid; data stores grow with doubling capacity, mirroring Column.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)

  • _applyAppend no longer re-requests the view with delay: 0; it uses the existing debounce machinery with delay: 120 / maxWait: 300, so a continuous stream pays at most one M4/density round trip per 300 ms.
  • Decimated line/area entries now record 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.append used to re-sync the spec/buffers traits 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):

metric before after
steady-state GPU upload per tick ~485 KB, 6 × bufferData reallocs 4.8 KB tail-only, 0 reallocs (~100×; ratio scales with N)
view round trips, at-home 5-tick stream 5/5 0/5
view round trips, zoomed 6-tick burst 6/6 2/6
pixels after N in-place appends vs fresh render byte-identical (0 mismatches)

Widget wire accounting (CI run on this branch, Figure.scatter + 1-row appends):

N append_data kernel time wire/tick before (2×) wire/tick after (1×)
10k (direct) 0.50 ms 162.7 KB 81.3 KB
100k (direct) 0.97 ms 1.60 MB 0.80 MB
1M (density tier) 9.67 ms 532 KB 266 KB

Tests / verification

  • New CI browser gate scripts/append_stream_smoke.py (stdlib-only, like render_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 into ci.yml, make check-browser, and the contributing table.
  • render_smoke_nonumpy stream 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_px recorded.
  • tests/test_widget.py: throttled reopen re-sync (burst parks, trailing flush lands the final state) and immediate-flush behavior without an event loop.
  • Full pytest suite, repo hooks, ruff, both Chromium smokes, and the JS bundle freshness check ran green on this branch (the committed static/*.js bundles were regenerated with the pinned vite toolchain).
  • Specs updated: 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

Alek99 added 5 commits July 22, 2026 07:56
…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-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR reduces the client-side cost of streaming appends. The main changes are:

  • Sticky column offsets for prefix-compatible append payloads.
  • Tail-only GPU uploads for compatible scatter and line traces.
  • Coalesced tier refinement with recorded decimation widths.
  • Throttled notebook reopen-state synchronization.
  • New browser, streaming, and widget tests.

Confidence Score: 5/5

This looks safe to merge.

No blocking issues found in the changed code.

T-Rex T-Rex Logs

What T-Rex did

  • The append stream smoke test was executed by running the Python script against the Chromium build, finishing with exit status 0 and a result that confirms tail-only uploads, pixel-identical outputs, and coalesced refinements.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
js/src/54_kernel.ts Adds guarded in-place GPU buffer growth and coalesced post-append refinement.
python/xy/columns.py Keeps shipped encoding offsets stable while the documented precision bound holds.
python/xy/_payload.py Records the pixel width used to produce decimated line and area payloads.
python/xy/widget.py Coalesces reopen-state trait updates while preserving every live append message.
scripts/append_stream_smoke.py Adds browser checks for tail uploads, pixel equivalence, and refinement cadence.
tests/test_streaming.py Tests offset stability, payload prefixes, channel-domain fallback, and decimation metadata.
tests/test_widget.py Tests leading and trailing reopen-state synchronization with and without an event loop.
.github/workflows/ci.yml Adds the streaming append Chromium smoke test to CI.

Reviews (1): Last reviewed commit: "Remove the temporary branch verification..." | Re-trigger Greptile

@codspeed-hq

codspeed-hq Bot commented Jul 22, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 97 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing claude/trace-append-perf-sbf211 (d88e55c) with main (2a7d898)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant