Skip to content

perf(ui): highlight settled code on the leading edge, debounce only streaming ticks (RIG-1422) - #575

Open
rigel-mintaka wants to merge 1 commit into
mainfrom
compass-ui/rig-1422-leading-edge-highlight
Open

perf(ui): highlight settled code on the leading edge, debounce only streaming ticks (RIG-1422)#575
rigel-mintaka wants to merge 1 commit into
mainfrom
compass-ui/rig-1422-leading-edge-highlight

Conversation

@rigel-mintaka

Copy link
Copy Markdown
Contributor

A settled (non-streaming) fenced block — every historical message in a
channel — used to sit on the plain <pre> fallback for HIGHLIGHT_DEBOUNCE_MS
before Shiki colorized it, because the highlight kickoff was gated behind the
trailing debounce from the very first tick. The debounce exists only to collapse
a burst of streaming growth ticks into one tokenize pass; a block that is not
growing pays that latency for nothing.

Fire the FIRST highlight for a fresh block immediately (leading edge) and
debounce only subsequent streaming growth ticks. The distinction cannot be made
per-instance: under renderingStrategy="reconcile" solid-markdown rebuilds the
fenced subtree on every growth tick, so BlockCode is reconstructed each tick
and a per-instance firstRun flag would read true every tick — firing an
immediate highlight per tick and reintroducing the O(n^2) re-tokenize the
debounce collapses. So the leading-edge decision lives in a module-level gate,
isLeadingEdgeHighlight(lang, code, windowMs) in highlight-cache.ts (colocated
with the R1 cache — both are cross-instance highlight-scheduling state): a tick
is a growth tick only when its code strictly extends a recent same-lang snapshot
within the debounce window; everything else is a leading edge.

The gate keeps a short, window-pruned LIST of recent (lang, code) snapshots,
not a single slot. A message with more than one fence rebuilds every fence in
the same reconcile tick, in document order, so a single slot would be clobbered
by an earlier fence before a later, still-growing fence could read it — the
grower would never match its own prior code and would (mis)fire an immediate
re-tokenize every tick, the exact O(n^2) this gate exists to prevent. Matching a
tick against ANY recent same-lang snapshot lets each fence find its own prior
code regardless of siblings scheduled in between. The list is bounded by the
window (stale entries pruned) with a coarse size cap as a backstop, and uses
performance.now() (monotonic) so a wall-clock adjustment cannot reorder
snapshots. A reconstructed instance calls the gate once at construction and
either sets settled synchronously (leading edge) or arms the existing 150ms
trailing timer (growth tick). clearHighlightCache() also clears the snapshots
so nothing leaks between tests.

Post-swap remeasure was proved unnecessary rather than added: the plain
fallback pre.code-block and the resolved .code-highlight pre share one CSS
rule block with identical box metrics (padding, border-radius, font,
line-height), the outer block margin is carried identically and the inner
wrapper is margin-zeroed so it is never double-counted, only background is
dropped on swap, and Shiki preserves the source line count — so the swap is
height-identical by construction and MessageStream.tsx is untouched.

Tests: a leading-edge contract test (the cache-miss counterpart to the R1
sync-cache test) asserts a settled fence asks the highlighter and paints its
markup WITHOUT ever elapsing the debounce flush. A burst-collapse test asserts a
single fence's within-window growth ticks collapse to one leading pass plus one
trailing pass, not one per tick. A multi-fence regression test asserts a second
streaming fence beside a settled one keeps debouncing (it fails against a
single-slot gate, where the sibling clobber re-tokenizes every tick). A direct
unit test of isLeadingEdgeHighlight pins the gate's own invariants without
mocking time — lang must match, identical code is not a growth tick, an
interleaved sibling does not steal a stream's classification, a zero-width
window disables growth detection, clearHighlightCache() resets the snapshots,
and a snapshot evicted by the size cap reads as a fresh leading edge. The
existing streaming kickoff-count and stale-resolution tests are unchanged and
green — they flush 200ms (> the 150ms window) between ticks, so each tick still
classifies as a fresh leading edge and fires its own request.

Refs RIG-1422

…treaming ticks (RIG-1422)

A settled (non-streaming) fenced block — every historical message in a
channel — used to sit on the plain `<pre>` fallback for HIGHLIGHT_DEBOUNCE_MS
before Shiki colorized it, because the highlight kickoff was gated behind the
trailing debounce from the very first tick. The debounce exists only to collapse
a burst of streaming growth ticks into one tokenize pass; a block that is not
growing pays that latency for nothing.

Fire the FIRST highlight for a fresh block immediately (leading edge) and
debounce only subsequent streaming growth ticks. The distinction cannot be made
per-instance: under `renderingStrategy="reconcile"` solid-markdown rebuilds the
fenced subtree on every growth tick, so `BlockCode` is reconstructed each tick
and a per-instance `firstRun` flag would read true every tick — firing an
immediate highlight per tick and reintroducing the O(n^2) re-tokenize the
debounce collapses. So the leading-edge decision lives in a module-level gate,
`isLeadingEdgeHighlight(lang, code, windowMs)` in highlight-cache.ts (colocated
with the R1 cache — both are cross-instance highlight-scheduling state): a tick
is a growth tick only when its code strictly extends a recent same-lang snapshot
within the debounce window; everything else is a leading edge.

The gate keeps a short, window-pruned LIST of recent `(lang, code)` snapshots,
not a single slot. A message with more than one fence rebuilds every fence in
the same reconcile tick, in document order, so a single slot would be clobbered
by an earlier fence before a later, still-growing fence could read it — the
grower would never match its own prior code and would (mis)fire an immediate
re-tokenize every tick, the exact O(n^2) this gate exists to prevent. Matching a
tick against ANY recent same-lang snapshot lets each fence find its own prior
code regardless of siblings scheduled in between. The list is bounded by the
window (stale entries pruned) with a coarse size cap as a backstop, and uses
`performance.now()` (monotonic) so a wall-clock adjustment cannot reorder
snapshots. A reconstructed instance calls the gate once at construction and
either sets `settled` synchronously (leading edge) or arms the existing 150ms
trailing timer (growth tick). `clearHighlightCache()` also clears the snapshots
so nothing leaks between tests.

Post-swap remeasure was proved unnecessary rather than added: the plain
fallback `pre.code-block` and the resolved `.code-highlight pre` share one CSS
rule block with identical box metrics (padding, border-radius, font,
line-height), the outer block margin is carried identically and the inner
wrapper is margin-zeroed so it is never double-counted, only `background` is
dropped on swap, and Shiki preserves the source line count — so the swap is
height-identical by construction and MessageStream.tsx is untouched.

Tests: a leading-edge contract test (the cache-miss counterpart to the R1
sync-cache test) asserts a settled fence asks the highlighter and paints its
markup WITHOUT ever elapsing the debounce flush. A burst-collapse test asserts a
single fence's within-window growth ticks collapse to one leading pass plus one
trailing pass, not one per tick. A multi-fence regression test asserts a second
streaming fence beside a settled one keeps debouncing (it fails against a
single-slot gate, where the sibling clobber re-tokenizes every tick). A direct
unit test of `isLeadingEdgeHighlight` pins the gate's own invariants without
mocking time — lang must match, identical code is not a growth tick, an
interleaved sibling does not steal a stream's classification, a zero-width
window disables growth detection, `clearHighlightCache()` resets the snapshots,
and a snapshot evicted by the size cap reads as a fresh leading edge. The
existing streaming kickoff-count and stale-resolution tests are unchanged and
green — they flush 200ms (> the 150ms window) between ticks, so each tick still
classifies as a fresh leading edge and fires its own request.

Refs RIG-1422
@linear-code

linear-code Bot commented Aug 24, 2026

Copy link
Copy Markdown

RIG-1422

@github-actions

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-ui-rig-1422-leading.compass-eng-docs.pages.dev

Deployed from compass-ui/rig-1422-leading-edge-highlight at 1a74ef4.

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