Streaming appends ship O(K) delta frames (protocol v8)#191
Merged
Conversation
A direct-tier append still re-shipped the whole affected trace: appending one point to a 100k scatter cost ~783 KiB. Now the kernel emits an append_rows delta - only the K new rows cross the wire - when the shipped representation is a pure tail extension of what the client holds: - Kernel: _append_rows_message checks strict, recorded eligibility (direct tier, no tier flip, no keyed/configured animation, no per-point style channels, no step/smooth expansion, no log axes, fully-finite rows, raw-encoded channels) and an offset-drift budget: tails encode with the offsets the client holds, capped at 1024x the encoded half-span recorded at the last full ship (~0.25 px worst case at 2048 px), after which one full re-ship re-centers like deep zoom (§4/§16). Every fallback reason rides the full message as append.delta_fallback (§28). - Client: _applyAppendRows extends GPU buffers (capacity-doubling bufferSubData) and retained CPU spans (same doubling - context restore stays whole) in place, advances spec metas, applies grown channel domains as uniform updates (the v7 raw wire is what makes this possible), keeps enabled attribute ranges covering n (selection mask zero-fill), and follows append's home/live-edge/history policy from the fresh axis ranges. prev_marks and encode params must match exactly; any disagreement sends one refresh instead of writing a torn tail. - Fixed in passing: appending to a trace whose x and y alias one deduplicated canonical column (scatter(v, v)) silently interleaved both axes into it; now rejected like cross-trace sharing, with a test. Protocol bumps to v8 (a pre-v8 bundle would drop delta frames on the floor mid-stream). Measured (median/tick, data-level): direct scatter 100k appending 1 pt: 783 KiB -> 383 bytes (~2000x); 10x50k dashboard: 397 KiB -> ~0.4 KiB; kernel build 0.20 -> 0.14 ms. Decimated/density tiers unchanged by design - their full payload is already O(pixels). Closes #163 for the streaming path.
Merging this PR will not alter performance
Comparing Footnotes
|
Greptile SummaryThis PR adds compact delta frames for streaming appends. The main changes are:
Confidence Score: 5/5This looks safe to merge. Delta roles are validated before retained columns are changed. Selection growth preserves the existing mask and tracks the real GPU allocation. No blocking issue remains in the updated paths.
What T-Rex did
Important Files Changed
Reviews (3): Last reviewed commit: "Merge feat/shader-channel-domains: paylo..." | Re-trigger Greptile |
…torn tail Two review findings on the delta apply path: - Growing selBuf re-uploaded an all-zero prefix on every capacity realloc (including the first delta, since _applySelMask never recorded a cap), visually clearing the live selection; and a selection reply's bufferData shrank the allocation behind a recorded cap, so the next delta's bufferSubData wrote past the end (GL_INVALID_VALUE) and left the attrib buffer smaller than n. _applySelMask now retains the mask as a CPU mirror and records the true allocation size; the delta grow path extends the mirror with zeros and re-uploads it on realloc. - The role loop validated each column only after earlier roles were already extended, so a bad y/channel reference left x's meta.len advanced against an unchanged g.n — a torn tail the pending refresh could not un-write. All roles (including tail byte length vs len*bpv) now validate before any state mutates. Verified with a browser probe against real kernel payloads: pre-fix bundle wipes the mask, raises GL 1281, and over-extends x after a bad delta; fixed bundle preserves the mask across both grow paths, refuses the bad delta with exactly one refresh and no mutation, and applies the next valid delta cleanly.
Alek99
added a commit
that referenced
this pull request
Jul 23, 2026
* Ship raw channel values; map domains in the shader (protocol v7) Continuous color/size channels shipped pre-normalized [0,1] f32, so any domain change - streaming growth, a colormap re-bind - invalidated every encoded value and forced an O(N) re-encode and re-ship. Now they ship raw data-unit f32 with enc:"raw" and the client maps them through the spec domain in the vertex shader ((v - d0) * 1/span into the LUT / px-range lookup); a domain change becomes a uniform update. This is the prerequisite for O(K) delta appends: a grown channel domain no longer touches shipped bytes. - New native kernel xy_sanitize_f32 (ABI v38): fused f64->f32 cast with non-finite scrubbed to the domain floor, keeping the §19 invariant (NaN never reaches a vertex buffer) without CPU normalization. The shader maps the floor to LUT coordinate 0 - the legacy visual for NaN rows. - f32-hostile domains (magnitude beyond f32, degenerate span) fall back to the legacy unit encode with no enc marker; the client's identity map draws both encodings pixel-identically. - Client: u_cvalMap/u_svalMap uniforms in every continuous-channel program (points, segments, mesh, rect, bar, pick), derived per bound channel spec (first paint, drill, sample overlay); standalone hover readouts return exact data units for raw buffers and denormalize only unit-encoded ones (heatmap grids unchanged). - Protocol bumps to v7: a pre-v7 bundle would clamp raw values as unit coordinates and render wrong colors silently - the class of failure the first-paint handshake exists to catch. Verified pixel-identical in a browser probe: the same 4k-point continuous-color+size scatter rendered from the raw wire and from a hand-built legacy unit wire differs in 0 of 541k pixels (max channel delta 0). Full suite 2200 passed; abi_smoke 123 checks. * Streaming appends ship O(K) delta frames (protocol v8) (#191) * Streaming appends ship O(K) delta frames (protocol v8) A direct-tier append still re-shipped the whole affected trace: appending one point to a 100k scatter cost ~783 KiB. Now the kernel emits an append_rows delta - only the K new rows cross the wire - when the shipped representation is a pure tail extension of what the client holds: - Kernel: _append_rows_message checks strict, recorded eligibility (direct tier, no tier flip, no keyed/configured animation, no per-point style channels, no step/smooth expansion, no log axes, fully-finite rows, raw-encoded channels) and an offset-drift budget: tails encode with the offsets the client holds, capped at 1024x the encoded half-span recorded at the last full ship (~0.25 px worst case at 2048 px), after which one full re-ship re-centers like deep zoom (§4/§16). Every fallback reason rides the full message as append.delta_fallback (§28). - Client: _applyAppendRows extends GPU buffers (capacity-doubling bufferSubData) and retained CPU spans (same doubling - context restore stays whole) in place, advances spec metas, applies grown channel domains as uniform updates (the v7 raw wire is what makes this possible), keeps enabled attribute ranges covering n (selection mask zero-fill), and follows append's home/live-edge/history policy from the fresh axis ranges. prev_marks and encode params must match exactly; any disagreement sends one refresh instead of writing a torn tail. - Fixed in passing: appending to a trace whose x and y alias one deduplicated canonical column (scatter(v, v)) silently interleaved both axes into it; now rejected like cross-trace sharing, with a test. Protocol bumps to v8 (a pre-v8 bundle would drop delta frames on the floor mid-stream). Measured (median/tick, data-level): direct scatter 100k appending 1 pt: 783 KiB -> 383 bytes (~2000x); 10x50k dashboard: 397 KiB -> ~0.4 KiB; kernel build 0.20 -> 0.14 ms. Decimated/density tiers unchanged by design - their full payload is already O(pixels). Closes #163 for the streaming path. * fix(client): append_rows keeps the selection mask and never writes a torn tail Two review findings on the delta apply path: - Growing selBuf re-uploaded an all-zero prefix on every capacity realloc (including the first delta, since _applySelMask never recorded a cap), visually clearing the live selection; and a selection reply's bufferData shrank the allocation behind a recorded cap, so the next delta's bufferSubData wrote past the end (GL_INVALID_VALUE) and left the attrib buffer smaller than n. _applySelMask now retains the mask as a CPU mirror and records the true allocation size; the delta grow path extends the mirror with zeros and re-uploads it on realloc. - The role loop validated each column only after earlier roles were already extended, so a bad y/channel reference left x's meta.len advanced against an unchanged g.n — a torn tail the pending refresh could not un-write. All roles (including tail byte length vs len*bpv) now validate before any state mutates. Verified with a browser probe against real kernel payloads: pre-fix bundle wipes the mask, raises GL 1281, and over-extends x after a bad delta; fixed bundle preserves the mask across both grow paths, refuses the bad delta with exactly one refresh and no mutation, and applies the next valid delta cleanly.
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.
Stacked on #187 (base:
feat/shader-channel-domains). Final PR of the streaming-append stack (#178 → #186 → #187 → this): takes the append hot path from O(N) to O(rows appended).What
A direct-tier append still re-shipped the whole affected trace — appending one point to a 100k scatter cost ~783 KiB. Now the kernel emits an
append_rowsdelta (only the K new rows) whenever the shipped representation is a pure tail extension of what the client holds.Kernel — strict, recorded eligibility (every exclusion rides the full message as
append.delta_fallback, per §28): direct-tier scatter/line; no tier flip; no keyed/configured animation; no per-point style channels; no step/smooth vertex expansion; no log axes; fully-finite rows (a drop would fork shipped row order); raw-encoded channels only. Plus an offset-drift budget: tails encode with the offsets the client already holds, capped at 1024× the encoded half-span recorded at the last full ship (~0.25 px worst-case at 2048 px) — past it, one full re-ship re-centers exactly like deep zoom (§4/§16). Screen-bounded tiers (decimated/density) always take the full path: their whole payload is already O(pixels).Client —
_applyAppendRowsis an in-place tail write, not a payload swap: capacity-doubling GPU buffers (bufferSubData, one amortized re-upload per doubling), retained CPU spans grown the same way (GL context restore stays whole, §27), spec metas advanced, grown channel domains applied as uniform updates — this is what #187's raw channel wire bought: domain growth costs zero re-encoded bytes. Enabled attribute ranges keep covering n (selection masks zero-fill new rows), and the home/live-edge/history follow policy runs off the fresh axis ranges.prev_marksand the encode params must match exactly; any disagreement (missed message, drifted baseline) sends onerefreshinstead of writing a torn tail.Fixed in passing: appending to a trace whose x and y alias one deduplicated canonical column (
scatter(v, v)) silently interleaved both axes into that single column — pre-existing corruption, surfaced by a new test fixture. Now rejected like cross-trace sharing, with a test.Protocol bumps to v8 — a pre-v8 bundle would silently drop delta frames mid-stream.
Measured
Median per tick, same bench as the stack (base = #187):
Cumulative across the stack for the dashboard case: 7.8 MB → 0.4 KiB per tick (#178 halved it, #186 removed the unchanged traces, this PR removes the O(N) re-ship of the affected trace).
Verified
prev_markstracking, pick-after-delta exactness, offset-drift fallback + recovery, per-reason fallbacks (non-finite tail, keyed animation, tier flip), post-delta full-build coherence, post-delta neighbor-append re-ships the streamed trace, the x≡y alias rejection).refreshand no tear. All green.ty(25 pre-existing, none new) /node js/build.mjs.Spec
wire-protocol.md§4 (append_rowscontract + eligibility + drift budget), §7 (v8);rust-engine.md§5 (Python-side delta landed;stream.rsremains the native follow-up). CHANGELOG under Unreleased → Changed.🤖 Generated with Claude Code