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
Open
Conversation
…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
|
Compass engineering docs preview: https://compass-ui-rig-1422-leading.compass-eng-docs.pages.dev Deployed from |
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.
A settled (non-streaming) fenced block — every historical message in a
channel — used to sit on the plain
<pre>fallback for HIGHLIGHT_DEBOUNCE_MSbefore 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 thefenced subtree on every growth tick, so
BlockCodeis reconstructed each tickand a per-instance
firstRunflag would read true every tick — firing animmediate 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 (colocatedwith 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 reordersnapshots. A reconstructed instance calls the gate once at construction and
either sets
settledsynchronously (leading edge) or arms the existing 150mstrailing timer (growth tick).
clearHighlightCache()also clears the snapshotsso nothing leaks between tests.
Post-swap remeasure was proved unnecessary rather than added: the plain
fallback
pre.code-blockand the resolved.code-highlight preshare one CSSrule 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
backgroundisdropped 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
isLeadingEdgeHighlightpins the gate's own invariants withoutmocking 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