Skip to content

Streaming appends ship O(K) delta frames (protocol v8)#191

Merged
Alek99 merged 3 commits into
feat/shader-channel-domainsfrom
feat/append-delta-frames
Jul 22, 2026
Merged

Streaming appends ship O(K) delta frames (protocol v8)#191
Alek99 merged 3 commits into
feat/shader-channel-domainsfrom
feat/append-delta-frames

Conversation

@Alek99

@Alek99 Alek99 commented Jul 22, 2026

Copy link
Copy Markdown
Member

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_rows delta (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_applyAppendRows is 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_marks and the encode params must match exactly; any disagreement (missed message, drifted baseline) 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 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):

Case Wire/tick Kernel ms/tick
direct scatter 100k, append 1 pt 783 KiB → 383 bytes (~2000×) 0.20 → 0.14
10×50k dashboard, 1 trace streaming 397 KiB → ~0.4 KiB (~1000×) 0.27 → 0.19
decimated line 2M 15 KiB → 15 KiB (by design) 0.52

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

  • Full suite 2206 passed (new: delta shape + consecutive prev_marks tracking, 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).
  • Browser probe (built ESM bundle, real kernel payloads): delta applies in place (GPU count 101→103→104, CPU spans + spec metas grow, tail decodes back to exact data units through the retained metas, raw color tail + moved domain updates the uniform map, home refit follows), consecutive deltas apply, and a replayed out-of-order delta is refused with exactly one refresh and no tear. All green.
  • ruff / format / pre-commit / ty (25 pre-existing, none new) / node js/build.mjs.

Spec

wire-protocol.md §4 (append_rows contract + eligibility + drift budget), §7 (v8); rust-engine.md §5 (Python-side delta landed; stream.rs remains the native follow-up). CHANGELOG under Unreleased → Changed.

🤖 Generated with Claude Code

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.
@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 feat/append-delta-frames (5199672) with feat/shader-channel-domains (df3687a)

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.

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds compact delta frames for streaming appends. The main changes are:

  • Protocol v8 support for append_rows messages.
  • Strict kernel eligibility checks with full-message fallbacks.
  • In-place CPU and GPU buffer growth on the client.
  • Selection-mask preservation and atomic delta validation.
  • Updated streaming tests, protocol docs, and generated bundles.

Confidence Score: 5/5

This 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.

T-Rex T-Rex Logs

What T-Rex did

  • Established the initial contract state: the ESM protocol 8 was built with 8 marks, retained lengths [8,8], and zero refreshes.
  • Observed the post-update state: the kernel emitted two tail-only append_rows frames carrying 8-byte and 4-byte buffers per channel, and the client reached 12 marks with the expected decoded tail.
  • Ran replay checks by applying the older prev_marks=9 delta at count 12, which caused exactly one refresh and produced no torn or duplicated tail.
  • Reviewed and organized artifacts, including logs, Python and JavaScript artifacts, an HTML resource, and a rendered image, to support inspection.
  • Overall, the gathered evidence confirms the contract-validation path behaved as described across initial, post-update, and replay scenarios.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
js/src/54_kernel.ts Applies append deltas in place and validates all referenced columns before changing retained state.
js/src/53_interaction.ts Retains selection masks and synchronizes GPU capacity after mask uploads.
python/xy/interaction.py Builds eligible append deltas, records fallback reasons, and rejects aliased x/y columns.
python/xy/_payload.py Records full-ship geometry spans for the delta drift limit.
python/xy/config.py Updates the Python wire protocol version to v8.

Reviews (3): Last reviewed commit: "Merge feat/shader-channel-domains: paylo..." | Re-trigger Greptile

Comment thread js/src/54_kernel.ts Outdated
Comment thread js/src/54_kernel.ts
Alek99 added 2 commits July 22, 2026 10:21
…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
Alek99 merged commit b9b9061 into feat/shader-channel-domains Jul 22, 2026
24 checks passed
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.
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