Merge pull request #7809 from nesquena/stage/0924-i

release: #6717 virtual measurement retry budget stops resetting on window oscillation (exp-v0.52.374)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-09-24 07:41:42 -07:00
commit 9a00b652c1
8 files changed
+600 -20

No files matched your search

+5
View File
@@ -36,6 +36,11 @@
rebuild going from 4.34 s to 0.49 s with a byte-identical snapshot. The interim-echo check now matches a
compact-equivalent suffix without a fixed window, so an echo stretched by interior whitespace is
no longer shown twice. (#7310, #7569 by @happy5318)
- **Long transcripts with virtualization on stop re-measuring rows in a loop.** When the rendered
window switched back and forth between two positions, each switch reset the measurement retry
budget, so opt-in transcript virtualization could keep re-measuring rows instead of settling. The
budget now resets only when the window reaches a position it hasn't just visited. (#6654, #6717 by
@webtecnica)
### Fixed
+109 -12
View File
@@ -556,7 +556,24 @@ function _messageVirtualDefaultHeightForRole(role){
role&&Object.prototype.hasOwnProperty.call(MESSAGE_VIRTUAL_DEFAULT_ROW_HEIGHTS,role)?role:'default'
];
}
const MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS=2;
// Cycle-aware measurement burst tracking (#6654/#6717): instead of a flat
// render cap, the burst remembers every window cycle key it has already seen.
// An UNSEEN key means the window is still converging forward (A->B->C->settled
// — content reflow, late fonts/images, dynamic height) and may proceed; a key
// that REPEATS means the browser is oscillating (A->B->A->B) and the burst
// terminates. Keys repeat only when geometry flaps, so legitimate convergence
// is never capped while oscillation is always bounded.
// Absolute per-burst ceiling (#6717 re-gate): the seen-key rule alone only
// ends the burst on a REPEATED key, so a browser emitting a monotonically
// changing geometry (A->B->C->D->... never repeating) would still loop without
// limit — the #6654 CPU-runaway class under a different trigger — and the
// seen-key collection would grow without bound (memory). A distinct-key
// convergence realistically settles in a handful of frames, so this
// conservative cap (the historical MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS
// was 2) ends the burst regardless of key novelty AND bounds the seen-key
// memory to the cap. Reset through _resetMessageVirtualMeasurementBurst().
const MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS=12;
let _messageVirtualMeasurementSeenKeys=[];
let _messageRenderWindowSid=null;
let _messageRenderWindowSize=MESSAGE_RENDER_WINDOW_DEFAULT;
let _messageVirtualHeightCache=[];
@@ -567,7 +584,19 @@ let _messageVirtualEstimatedRowHeight=_messageVirtualDefaultHeightForRole('defau
let _messageVirtualScrollRaf=0;
let _messageVirtualWindowKey='';
let _messageVirtualMeasurementCycleKey='';
let _messageVirtualMeasurementRetryCount=0;
let _messageVirtualMeasurementBurstActive=false;
// Provenance of the QUEUED virtualized render: 'internal' while the pending
// rAF was requested by the internal measurement chain, 'external' for any
// other trigger (user scroll / scroll-settle / message append / session
// load). The origin lives on the QUEUED render itself, NOT in a global
// consumable flag: when an internal and an external request coalesce into one
// rAF, _scheduleMessageVirtualizedRender lets EXTERNAL win, so a render that
// fires after any external trigger is never mis-attributed as internal
// (#6717 re-gate). renderMessages() resets the burst on every UNMARKED
// (externally initiated) render trigger, so an overlapping external render
// always starts a fresh cycle even when an internal measurement callback is
// still pending.
let _messageVirtualRenderQueuedOrigin=null;
let _messageVirtualScrollActive=false;
let _messageVirtualScrollSettleTimer=0;
let _messageVirtualDeferredMeasurement=null;
@@ -614,7 +643,7 @@ function _clearMessageVirtualHeightCache(){
_messageVirtualEstimatedRowHeight=_messageVirtualDefaultHeightForRole('default');
_messageVirtualWindowKey='';
_messageVirtualMeasurementCycleKey='';
_messageVirtualMeasurementRetryCount=0;
_resetMessageVirtualMeasurementBurst();
_messageVirtualScrollActive=false;
clearTimeout(_messageVirtualScrollSettleTimer);
_messageVirtualScrollSettleTimer=0;
@@ -742,6 +771,13 @@ function _messageVirtualMeasurementCycleKeyFor(windowMetrics){
windowMetrics.tailStart||0,
].join(':');
}
function _resetMessageVirtualMeasurementBurst(){
_messageVirtualMeasurementSeenKeys=[];
_messageVirtualMeasurementBurstActive=false;
// Strip any pending internal provenance: an external reset must never let
// an internal marker survive onto a render that fires later (#6717 re-gate).
_messageVirtualRenderQueuedOrigin=null;
}
function _scheduleMessageVirtualMeasurementRefresh(windowMetrics){
if(_messageVirtualScrollActive){
_messageVirtualDeferredMeasurement=windowMetrics;
@@ -750,15 +786,55 @@ function _scheduleMessageVirtualMeasurementRefresh(windowMetrics){
const cycleKey=_messageVirtualMeasurementCycleKeyFor(windowMetrics);
if(_messageVirtualMeasurementCycleKey!==cycleKey){
_messageVirtualMeasurementCycleKey=cycleKey;
_messageVirtualMeasurementRetryCount=0;
}
if(_messageVirtualMeasurementRetryCount>=MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS) return;
_messageVirtualMeasurementRetryCount++;
requestAnimationFrame(()=>{ _scheduleMessageVirtualizedRender(true); });
// Cycle-aware burst tracking (#6717 re-gate): the burst follows ONLY the
// internally measurement-scheduled chain (requestAnimationFrame ->
// _scheduleMessageVirtualizedRender -> renderMessages -> re-measure -> here).
// Within one burst we remember every cycle key already seen. An UNSEEN key
// (genuine forward convergence A->B->C->settled — content reflow, late
// fonts/images, dynamic height) may proceed, so convergence is never capped
// at a flat render count; a key that REPEATS (WebKit's A->B->A->B window
// oscillation) ends the burst, so the rAF/measure loop can never run forever
// (#6654). The burst starts fresh on every EXTERNALLY initiated render
// (session load, message append, real content change — see renderMessages)
// and on measurement settlement — never on a cycle-key change alone.
if(!_messageVirtualMeasurementBurstActive){
_messageVirtualMeasurementSeenKeys=[];
_messageVirtualMeasurementBurstActive=true;
}
// Absolute per-burst cap (#6717 re-gate): even an ALL-DISTINCT monotonic
// key sequence (A->B->C->D->... never repeating) must terminate — a repeated
// key is not the only way the burst can end. This also bounds the seen-key
// collection to the cap (memory). Distinct-key convergence settles in a
// handful of frames, so the cap is far above any legitimate multi-pass
// reflow while still closing the #6654 CPU-runaway class.
if(_messageVirtualMeasurementSeenKeys.length>=MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS){
_resetMessageVirtualMeasurementBurst();
return;
}
const lastKey = _messageVirtualMeasurementSeenKeys.length ? _messageVirtualMeasurementSeenKeys[_messageVirtualMeasurementSeenKeys.length - 1] : null;
if(cycleKey !== lastKey && _messageVirtualMeasurementSeenKeys.includes(cycleKey)){
// Non-consecutive repeated key (A->B->A): the window is oscillating, not
// converging. End the burst (no further internal re-render is scheduled),
// so the next externally initiated cycle starts fresh instead of being
// starved of retries. A consecutive same-key pass (A->A) proceeds because
// heights changed while the window bounds did not (e.g. row shrink across
// two passes), still bounded by the absolute per-burst cap (#6717 re-gate).
_resetMessageVirtualMeasurementBurst();
return;
}
_messageVirtualMeasurementSeenKeys.push(cycleKey);
// The internal-measurement origin travels WITH the scheduled render request
// (threaded through BOTH rAF layers into renderMessages), so coalescing can
// never consume a stale global marker onto an unrelated render. When an
// external request coalesces into the queued rAF, EXTERNAL wins and the
// render that fires degrades to an unmarked (burst-resetting) render
// (#6717 re-gate).
requestAnimationFrame(()=>{ _scheduleMessageVirtualizedRender(true,{origin:'internal'}); });
}
function _markMessageVirtualMeasurementsSettled(windowMetrics){
_messageVirtualMeasurementCycleKey=_messageVirtualMeasurementCycleKeyFor(windowMetrics);
_messageVirtualMeasurementRetryCount=0;
_resetMessageVirtualMeasurementBurst();
}
function _messageVirtualHeightEntryMatches(previousEntry, nextEntry){
return !!(
@@ -1474,7 +1550,7 @@ function _rememberRenderedUserRowIntrinsicHeights(){
}
}
}
function _scheduleMessageVirtualizedRender(force){
function _scheduleMessageVirtualizedRender(force, request){
const container=$('messages');
const inner=$('msgInner');
if(!container||!inner) return;
@@ -1486,9 +1562,28 @@ function _scheduleMessageVirtualizedRender(force){
_messageVirtualWindowKey=nextKey;
return;
}
if(_messageVirtualScrollRaf) return;
// The request carries its own origin: the internal measurement chain passes
// {origin:'internal'} (see _scheduleMessageVirtualMeasurementRefresh);
// every other caller is external by default.
const requestOrigin=(request&&request.origin==='internal')?'internal':'external';
if(_messageVirtualScrollRaf){
// Coalescing into an already-queued rAF: the queued render is shared, so
// merge the provenance. EXTERNAL always wins — the render that actually
// fires must reset the burst, and an internal marker must never survive
// onto an external render (#6717 re-gate). An internal request joining a
// queued render never downgrades an already-external one.
if(requestOrigin==='external') _messageVirtualRenderQueuedOrigin='external';
return;
}
_messageVirtualRenderQueuedOrigin=requestOrigin;
_messageVirtualScrollRaf=requestAnimationFrame(()=>{
_messageVirtualScrollRaf=0;
// The provenance belongs to THIS queued render: it was set when the render
// was scheduled (and possibly flipped to 'external' by a coalesced
// external request). Consume it here — no global consumable flag that an
// unrelated render could steal (#6717 re-gate).
const internalMeasurement=_messageVirtualRenderQueuedOrigin==='internal';
_messageVirtualRenderQueuedOrigin=null;
const liveVisWithIdx=_getVisibleMessagesWithIdx();
const liveWindow=_currentMessageVirtualWindow(liveVisWithIdx,_messageVirtualKeepTailCount());
const liveKey=_messageVirtualWindowKeyFor(liveWindow);
@@ -1496,14 +1591,14 @@ function _scheduleMessageVirtualizedRender(force){
if(_scrollbarDragActive){
_programmaticScroll=true;
_programmaticScrollSetAt=performance.now();
_compensateScrollForMeasurementDelta(()=>{ renderMessages({ preserveScroll:true }); });
_compensateScrollForMeasurementDelta(()=>{ renderMessages({ preserveScroll:true, _internalMeasurement: internalMeasurement }); });
_deferClearProgrammaticScroll();
_messageVirtualWindowKey=liveKey;
return;
}
_msgNodeRecycleEnabled=true;
try{
_compensateScrollForMeasurementDelta(()=>{ renderMessages({ preserveScroll:true }); });
_compensateScrollForMeasurementDelta(()=>{ renderMessages({ preserveScroll:true, _internalMeasurement: internalMeasurement }); });
}
finally{ _msgNodeRecycleEnabled=false; }
});
@@ -17216,6 +17311,8 @@ function _insertSegmentBlock(seg, html){
}
function renderMessages(options){
_lastMessageRenderAt=performance.now();
// typeof guard: node harnesses extract renderMessages() without its helpers (#6717).
if(!(options&&options._internalMeasurement) && typeof _resetMessageVirtualMeasurementBurst==='function'){ _resetMessageVirtualMeasurementBurst(); }
const preserveScroll=!!(options&&options.preserveScroll);
const virtualFallback=!!(options&&options._virtualFallback);
// Capture the pre-wipe scroll position when preserving OR when the reader has
+1
View File
@@ -679,6 +679,7 @@ def test_render_messages_keeps_anchor_owned_turn_out_of_legacy_activity_rebuilds
function _scrollAfterMessageRender() {{}}
function _maybeRecoverVirtualizedBlankViewport() {{ return false; }}
function _updateMessageVirtualMeasurements() {{}}
function _resetMessageVirtualMeasurementBurst() {{}}
function postProcessRenderedMessages() {{}}
function _postProcessWithAnchorSuppression() {{}}
function _formatGatewayModelLabel() {{ return ''; }}
@@ -99,7 +99,7 @@ def test_js_recycle_flag_lifecycle():
"""_scheduleMessageVirtualizedRender sets _msgNodeRecycleEnabled=true
before the compensate call and clears it in finally."""
fn_match = re.search(
r'function _scheduleMessageVirtualizedRender\(force\)\{(.+?)^(?=function )',
r'function _scheduleMessageVirtualizedRender\(force(?:, request)?\)\{(.+?)^(?=function )',
JS, re.DOTALL | re.MULTILINE
)
assert fn_match, "_scheduleMessageVirtualizedRender not found"
@@ -1101,7 +1101,7 @@ class TestScrollbarDragRenderDuringDrag:
"""_scheduleMessageVirtualizedRender must have a _scrollbarDragActive
branch that runs a full render with scroll compensation."""
fn_match = re.search(
r'function _scheduleMessageVirtualizedRender\(force\)\{(.+?)^(?=function )',
r'function _scheduleMessageVirtualizedRender\(force(?:, request)?\)\{(.+?)^(?=function )',
JS, re.DOTALL | re.MULTILINE
)
assert fn_match, "_scheduleMessageVirtualizedRender not found"
@@ -1,5 +1,6 @@
"""Regression coverage for issue #500 transcript virtualization."""
import json
import re
import shutil
import subprocess
import tempfile
@@ -1345,7 +1346,9 @@ let _messageVirtualHeightCacheSrc = {};
let _messageVirtualEstimatedRowHeight = 200;
let _messageVirtualWindowKey = 'old-key';
let _messageVirtualMeasurementCycleKey = 'old-cycle';
let _messageVirtualMeasurementRetryCount = 3;
let _messageVirtualMeasurementSeenKeys = ['stale-key'];
let _messageVirtualMeasurementBurstActive = true;
let _messageVirtualRenderQueuedOrigin = 'internal';
const MESSAGE_VIRTUAL_DEFAULT_ROW_HEIGHT = 140;
// #4367 introduced per-role default heights; the combined _clearMessageVirtualHeightCache
// resets the estimate via _messageVirtualDefaultHeightForRole('default'), so the harness
@@ -1353,6 +1356,7 @@ const MESSAGE_VIRTUAL_DEFAULT_ROW_HEIGHT = 140;
const MESSAGE_VIRTUAL_DEFAULT_ROW_HEIGHTS = {user:120, assistant:160, tool_call:400, default:140};
function clearTimeout(id){ timerCleared = (id === 99); }
eval(extractFunc('_messageVirtualDefaultHeightForRole'));
eval(extractFunc('_resetMessageVirtualMeasurementBurst'));
eval(extractFunc('_clearMessageVirtualHeightCache'));
_clearMessageVirtualHeightCache();
console.log(JSON.stringify({
@@ -1360,6 +1364,9 @@ console.log(JSON.stringify({
settleTimer: _messageVirtualScrollSettleTimer,
deferred: _messageVirtualDeferredMeasurement,
timerCleared: timerCleared,
seenKeys: _messageVirtualMeasurementSeenKeys,
burstActive: _messageVirtualMeasurementBurstActive,
queuedOrigin: _messageVirtualRenderQueuedOrigin,
}));
"""
metrics = json.loads(_run_node(source))
@@ -1375,3 +1382,425 @@ console.log(JSON.stringify({
assert metrics["timerCleared"] is True, (
"_clearMessageVirtualHeightCache must call clearTimeout on the pending settle timer"
)
assert metrics["seenKeys"] == [], (
"_clearMessageVirtualHeightCache must clear the measurement-burst seen-key "
f"memory: seenKeys {metrics['seenKeys']}"
)
assert metrics["burstActive"] is False, (
"_clearMessageVirtualHeightCache must end any active measurement burst: "
f"burstActive {metrics['burstActive']}"
)
assert metrics["queuedOrigin"] is None, (
"_clearMessageVirtualHeightCache must clear any queued internal-measurement "
f"provenance: queuedOrigin {metrics['queuedOrigin']}"
)
def _measurement_burst_harness(scenario_js: str) -> str:
"""Shared queued-rAF harness for the measurement-burst chain (#6654/#6717).
Drives the REAL production chain: the cycle-aware scheduler queues a first
rAF, _scheduleMessageVirtualizedRender queues a second rAF (the
internal-measurement provenance marker is threaded through BOTH layers),
and the renderMessages stub applies the production burst-reset contract for
every unmarked (external) render, then simulates the measure pass from the
caller-provided plan (measurePlan). Unlike the earlier synchronous-rAF
harnesses, external renders can land BETWEEN queued rAF callbacks — exactly
where the overlap/convergence regressions lived.
"""
js = UI_JS_PATH.read_text(encoding="utf-8")
cap_match = re.search(
r"MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS\s*=\s*(\d+)", js
)
assert cap_match, (
"MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS must be defined in ui.js "
"(absolute per-burst cap, #6717 re-gate)"
)
cap = int(cap_match.group(1))
return _extract_func_script(js) + (
"// Absolute per-burst ceiling, read from production ui.js (#6717 re-gate).\n"
f"const MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS = {cap};\n"
) + """
// ── queued-rAF harness ────────────────────────────────────────────────────────
let renderLog = []; // every renderMessages call: {internal, preserveScroll}
let rafQueue = [];
function requestAnimationFrame(cb){ rafQueue.push(cb); return rafQueue.length; }
function flushRaf(){ const q = rafQueue; rafQueue = []; for (const cb of q) cb(); }
let _messageVirtualScrollRaf = 0;
let _messageVirtualWindowKey = '';
let _messageVirtualMeasurementCycleKey = '';
let _messageVirtualMeasurementSeenKeys = [];
let _messageVirtualMeasurementBurstActive = false;
let _messageVirtualRenderQueuedOrigin = null;
let _messageVirtualScrollActive = false;
let _messageVirtualDeferredMeasurement = null;
let _scrollbarDragActive = false;
let _msgNodeRecycleEnabled = false;
let _programmaticScroll = false;
let _programmaticScrollSetAt = 0;
function _deferClearProgrammaticScroll(){}
function $(id){ return {}; }
function _getVisibleMessagesWithIdx(){ return []; }
function _messageVirtualKeepTailCount(){ return 0; }
function _currentMessageVirtualWindow(){
return { virtualized:true, start:0, end:10, topPad:0, bottomPad:0, tailStart:10, total:20 };
}
function _messageVirtualMeasurementCycleKeyFor(w){ return (w && w.key) || ''; }
function _compensateScrollForMeasurementDelta(fn){ return fn(); }
// Simulated measure pass: after each render, the plan yields the next measured
// window cycle key; undefined means the measurements settled (unchanged).
let measurePlan = [];
let measureIdx = 0;
function renderMessages(options){
const internal = !!(options && options._internalMeasurement);
renderLog.push({ internal: internal, preserveScroll: !!(options && options.preserveScroll) });
// Contract of production renderMessages(): every UNMARKED (external) render
// trigger starts a fresh measurement burst, overriding any pending internal
// callback.
if(!internal) _resetMessageVirtualMeasurementBurst();
const next = measurePlan[measureIdx++];
if(next === undefined){
_markMessageVirtualMeasurementsSettled({ key: 'settled' });
}else{
_scheduleMessageVirtualMeasurementRefresh({ key: next });
}
}
// Production functions under test.
eval(extractFunc('_resetMessageVirtualMeasurementBurst'));
eval(extractFunc('_messageVirtualWindowKeyFor'));
eval(extractFunc('_scheduleMessageVirtualMeasurementRefresh'));
eval(extractFunc('_scheduleMessageVirtualizedRender'));
eval(extractFunc('_markMessageVirtualMeasurementsSettled'));
""" + scenario_js
def test_oscillating_cycle_keys_terminate_burst_with_queued_raf():
"""#6654/#6717 re-gate (c): a repeated oscillating window key (A->B->A->B)
must terminate the measurement burst even though every key was unseen on
first sight — the burst tracks SEEN keys, so geometry that flaps between
two states can never renew the chain and keep the rAF/measure loop alive
(~30% WebContent CPU, issue #6654). The queued-rAF harness drives the
oscillation through the real two-rAF-layer chain."""
source = _measurement_burst_harness("""
measurePlan = ['B','A','B','A','B','A'];
renderMessages({}); // external trigger: measures B -> burst starts
flushRaf(); flushRaf(); // layer-1 then layer-2 rAF -> internal render (A)
flushRaf(); flushRaf(); // next cycle -> internal render (B repeats) -> burst ends
flushRaf(); flushRaf(); // nothing left queued
console.log(JSON.stringify({
renders: renderLog.length,
internal: renderLog.filter(r => r.internal).length,
burstActive: _messageVirtualMeasurementBurstActive,
seenKeys: _messageVirtualMeasurementSeenKeys,
queuedOrigin: _messageVirtualRenderQueuedOrigin,
}));
""")
metrics = json.loads(_run_node(source))
assert metrics["internal"] == 2, (
"A->B->A->B oscillation must stay bounded: a repeated key ends the burst. "
f"scheduled {metrics['internal']} internal re-renders (expected 2, then termination)"
)
assert metrics["renders"] == 3, (
"oscillation must not keep scheduling renders: "
f"{metrics['renders']} renders (expected 3 = 1 external + 2 internal)"
)
assert metrics["burstActive"] is False, (
"the burst must end once the repeated key is detected so the next "
f"externally initiated cycle starts fresh: burstActive {metrics['burstActive']}"
)
assert metrics["seenKeys"] == [], (
"termination must clear the seen-key memory: "
f"seenKeys {metrics['seenKeys']}"
)
assert metrics["queuedOrigin"] is None, (
"no internal render may remain queued after termination: "
f"queuedOrigin {metrics['queuedOrigin']}"
)
def test_consecutive_same_key_row_shrink_converges_to_settled():
"""#6717 re-gate: consecutive same-key passes (e.g. A->A->B->settled from
a row shrinking across two measurement passes with unchanged window bounds)
must NOT be cut short as oscillation. Only non-consecutive repeats (A->B->A)
represent cyclic oscillation; consecutive same-key passes continue toward
convergence while remaining bounded by the absolute cap."""
source = _measurement_burst_harness("""
measurePlan = ['A', 'A', 'B']; // R0 measures A, R1 internal measures A (consecutive), R2 internal measures B, then settled
renderMessages({}); // external trigger: measures A -> burst starts
flushRaf(); flushRaf(); // L1 -> L2: internal render measures A again (consecutive same key) -> must proceed!
flushRaf(); flushRaf(); // L1 -> L2: internal render measures B -> proceeds!
flushRaf(); flushRaf(); // L1 -> L2: internal render measures settled
console.log(JSON.stringify({
renders: renderLog.length,
internal: renderLog.filter(r => r.internal).length,
burstActive: _messageVirtualMeasurementBurstActive,
seenKeys: _messageVirtualMeasurementSeenKeys,
queuedOrigin: _messageVirtualRenderQueuedOrigin,
}));
""")
metrics = json.loads(_run_node(source))
assert metrics["internal"] == 3, (
"consecutive same-key pass (row shrink) must NOT be cut short as oscillation: "
f"got {metrics['internal']} internal renders (expected 3)"
)
assert metrics["renders"] == 4, (
f"burst must converge to settled: {metrics['renders']} renders (expected 4 = 1 external + 3 internal)"
)
assert metrics["burstActive"] is False, (
f"burst must settle: burstActive {metrics['burstActive']}"
)
assert metrics["seenKeys"] == [], (
f"settlement must clear seen-key memory: seenKeys {metrics['seenKeys']}"
)
def test_genuine_convergence_exceeding_two_renders_completes():
"""#6717 re-gate (b): a legitimate forward convergence A->B->C->settled
(content reflow, fonts/images loading late, dynamic height) must be allowed
MORE than two measurement passes. The old flat two-render burst cap left
the geometry mis-measured (wrong row heights / clipped content); cycle-aware
tracking lets every UNSEEN key proceed until the measurements settle."""
source = _measurement_burst_harness("""
measurePlan = ['B','C','D']; // three distinct forward keys, then settled
renderMessages({}); // external trigger: measures B
flushRaf(); flushRaf(); // internal render: measures C
flushRaf(); flushRaf(); // internal render: measures D
flushRaf(); flushRaf(); // internal render: measurements settle
console.log(JSON.stringify({
renders: renderLog.length,
internal: renderLog.filter(r => r.internal).length,
lastInternal: renderLog[renderLog.length - 1].internal,
burstActive: _messageVirtualMeasurementBurstActive,
seenKeys: _messageVirtualMeasurementSeenKeys,
queuedOrigin: _messageVirtualRenderQueuedOrigin,
}));
""")
metrics = json.loads(_run_node(source))
assert metrics["internal"] == 3, (
"genuine A->B->C->settled convergence must complete with MORE than the "
"old two-render cap: "
f"{metrics['internal']} internal re-renders (expected 3)"
)
assert metrics["renders"] == 4, (
"the convergence chain must run to completion: "
f"{metrics['renders']} renders (expected 4 = 1 external + 3 internal)"
)
assert metrics["lastInternal"] is True, (
"the final render of the convergence chain must still be part of the "
"internal measurement chain (settled via _markMessageVirtualMeasurementsSettled)"
)
assert metrics["burstActive"] is False, (
"settlement must end the burst: "
f"burstActive {metrics['burstActive']}"
)
assert metrics["seenKeys"] == [] and metrics["queuedOrigin"] is None, (
"settlement must clear the seen-key memory and any queued provenance: "
f"seenKeys {metrics['seenKeys']}, queuedOrigin {metrics['queuedOrigin']}"
)
def test_overlapping_external_append_receives_fresh_budget():
"""#6717 re-gate (a): an externally initiated render (message append,
scroll-settle, genuine content change) that lands while an internal
measurement callback is still PENDING in the rAF queue must reset the burst
— external provenance overrides a pending internal callback — so the new
cycle receives a fresh budget instead of being starved by the stale burst's
seen-key memory (0 retries).
Phase 1: the append lands between the two rAF layers and measures key B
(already seen in the stale burst). Only the external reset lets B proceed;
without it the repeated key would terminate the append's cycle immediately.
The stale layer-2 callback (queued for the DEAD burst) must then degrade to
an UNMARKED render — the append's own measurement must not revive an
internal marker onto it, because the provenance belongs to the queued
render, not to a global flag (#6717 re-gate MUST-FIX 2) — and the fresh
cycle it starts completes with its own internal render, so the append is
never starved of retries.
Phase 2: an external render that measures SETTLED lands while a layer-2
callback is pending; the stale callback must then degrade to an UNMARKED
render (the external reset consumed its provenance), and the fresh cycle it
starts must complete with its own internal render."""
source = _measurement_burst_harness("""
measurePlan = ['B','B','C', undefined, 'E', undefined, 'F', undefined];
renderMessages({}); // R0 external: burst starts, seen=[B], L1 queued
flushRaf(); // L1 fires -> L2 (internal callback) now pending
renderMessages({}); // R1 EXTERNAL APPEND lands before L2 fires:
// resets the burst -> fresh cycle, seen=[B] again
flushRaf(); flushRaf(); // stale L2 fires UNMARKED (R2, external won),
// fresh cycle completes (R3 internal) -> settles
renderMessages({}); // R4 external: new cycle, L1 queued
flushRaf(); // L1 fires -> L2 (internal callback) now pending
renderMessages({}); // R5 external scroll-settle: resets the burst,
// measures SETTLED (no new schedule)
flushRaf(); // stale L2 fires -> marker consumed as FALSE ->
// R6 UNMARKED render (external provenance won)
flushRaf(); flushRaf(); // R6's fresh cycle completes (R7 internal) -> settles
console.log(JSON.stringify({
renders: renderLog.length,
internal: renderLog.filter(r => r.internal).length,
appendExternal: renderLog[1].internal,
staleL2Unmarked: renderLog[2].internal,
postAppendCycleInternal: renderLog[3].internal,
staleCallbackUnmarked: renderLog[6].internal,
postResetCycleInternal: renderLog[7].internal,
burstActive: _messageVirtualMeasurementBurstActive,
seenKeys: _messageVirtualMeasurementSeenKeys,
queuedOrigin: _messageVirtualRenderQueuedOrigin,
}));
""")
metrics = json.loads(_run_node(source))
assert metrics["renders"] == 8, (
"the overlapping append must NOT starve the new cycle: the chain must "
"continue past the append to completion "
f"({metrics['renders']} renders; expected 8)"
)
assert metrics["appendExternal"] is False, (
"the append is an unmarked external render"
)
assert metrics["staleL2Unmarked"] is False, (
"the stale layer-2 callback (queued for the burst the append reset) "
"must degrade to an UNMARKED render: the append's own measurement "
"must not revive an internal marker onto it — provenance belongs to "
"the queued render, external wins (#6717 re-gate MUST-FIX 2)"
)
assert metrics["postAppendCycleInternal"] is True, (
"the fresh cycle after the degraded stale callback must complete with "
"its own internal render (fresh budget — the repeated key B was not "
"treated as oscillation because the external render reset the burst)"
)
assert metrics["staleCallbackUnmarked"] is False, (
"external provenance must override the pending internal callback: the "
"stale internal rAF callback must degrade to an unmarked render after "
"the external render reset the burst"
)
assert metrics["postResetCycleInternal"] is True, (
"the cycle started by the degraded stale callback must complete with "
"its own internal render (retries were not starved)"
)
assert metrics["burstActive"] is False, (
"the fresh cycles must end settled: "
f"burstActive {metrics['burstActive']}"
)
assert metrics["seenKeys"] == [] and metrics["queuedOrigin"] is None, (
"settlement must clear the seen-key memory and any queued provenance: "
f"seenKeys {metrics['seenKeys']}, queuedOrigin {metrics['queuedOrigin']}"
)
def test_all_distinct_keys_terminate_burst_with_bounded_storage():
"""#6717 re-gate MUST-FIX 1: an ALL-DISTINCT monotonic key sequence
(A->B->C->D->... never repeating) must still terminate. The seen-key rule
alone only ends the burst on a REPEATED key, so a browser emitting a
never-repeating geometry would loop without limit — the #6654 CPU-runaway
class under a different trigger — while _messageVirtualMeasurementSeenKeys
grows without bound (memory). The absolute per-burst cap ends the burst
regardless of key novelty and bounds the seen-key collection to the cap.
Driven through the real two-rAF-layer chain with MORE distinct keys than
the cap, and asserting the burst stops at exactly the cap with no rAF left
queued and the seen-key memory cleared."""
source = _measurement_burst_harness("""
measurePlan = [];
for(let i=0;i<40;i++) measurePlan.push('K'+i); // 40 distinct keys, well over the cap
renderMessages({}); // external trigger: starts the burst
for(let i=0;i<40;i++) flushRaf(); // drain both rAF layers repeatedly
console.log(JSON.stringify({
renders: renderLog.length,
internal: renderLog.filter(r => r.internal).length,
burstActive: _messageVirtualMeasurementBurstActive,
seenKeys: _messageVirtualMeasurementSeenKeys,
queuedOrigin: _messageVirtualRenderQueuedOrigin,
rafPending: rafQueue.length,
cap: MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS,
}));
""")
metrics = json.loads(_run_node(source))
cap = metrics["cap"]
assert metrics["internal"] == cap, (
"an all-distinct sequence must be cut off by the absolute per-burst "
f"cap: {metrics['internal']} internal renders (expected exactly {cap})"
)
assert metrics["renders"] == cap + 1, (
"the all-distinct burst must terminate after the cap (1 external + cap "
f"internal renders): {metrics['renders']} renders (expected {cap + 1})"
)
assert metrics["rafPending"] == 0, (
"no rAF may remain queued after the cap ends the burst: "
f"rafPending {metrics['rafPending']}"
)
assert metrics["burstActive"] is False, (
"the cap must end the burst: "
f"burstActive {metrics['burstActive']}"
)
assert metrics["seenKeys"] == [], (
"termination must clear the seen-key memory (bounded storage — at most "
f"cap keys were ever retained): seenKeys {metrics['seenKeys']}"
)
assert metrics["queuedOrigin"] is None, (
"no internal provenance may remain queued after the cap ends the burst: "
f"queuedOrigin {metrics['queuedOrigin']}"
)
def test_queued_raf_external_scroll_wins_provenance():
"""#6717 re-gate MUST-FIX 2: when an external render (user scroll /
scroll-settle) overlaps a QUEUED internal rAF, the render that actually
fires must be treated as EXTERNAL. The old global consumable flag
(_messageVirtualMeasurementRenderPending) could be consumed by an
unrelated render, marking external renders as internal so the burst was
never reset — leaving stale virtual-window / spacer geometry until the
next unrelated render.
Sequence: external R0 measures A (L2 queued internal); an external R1
lands while L2 is pending — it resets the burst, measures B, and its own
layer-1 rAF COALESCES into the pending L2 (the early-return path where
the old flag leaked). The L2 that fires must be UNMARKED — external
provenance wins — so the A it measures starts a fresh burst instead of
terminating on the repeated key with zero retry queued."""
source = _measurement_burst_harness("""
measurePlan = ['A','B','A', undefined];
renderMessages({}); // R0 external: measures A -> burst, L1 queued
flushRaf(); // L1 -> L2 queued with internal provenance
renderMessages({}); // R1 EXTERNAL scroll lands while L2 pending:
// resets the burst, measures B -> L1 queued
flushRaf(); // B's L1 coalesces into the pending L2 (no new rAF)
flushRaf(); // L2 fires -> must be UNMARKED (external won),
// measures A -> fresh burst -> L1 queued
flushRaf(); flushRaf(); // A's fresh cycle: L2 internal render -> settles
console.log(JSON.stringify({
renders: renderLog.length,
internal: renderLog.filter(r => r.internal).length,
r0External: renderLog[0].internal,
r1External: renderLog[1].internal,
coalescedL2Unmarked: renderLog[2].internal,
freshCycleInternal: renderLog[3].internal,
burstActive: _messageVirtualMeasurementBurstActive,
seenKeys: _messageVirtualMeasurementSeenKeys,
queuedOrigin: _messageVirtualRenderQueuedOrigin,
}));
""")
metrics = json.loads(_run_node(source))
assert metrics["renders"] == 4, (
"the scroll overlap chain must run to completion: "
f"{metrics['renders']} renders (expected 4)"
)
assert metrics["r0External"] is False and metrics["r1External"] is False, (
"R0 and R1 are unmarked external renders"
)
assert metrics["coalescedL2Unmarked"] is False, (
"the queued internal rAF must degrade to an UNMARKED render when an "
"external request coalesced into it — external provenance wins and an "
"internal marker must never survive onto an external render"
)
assert metrics["freshCycleInternal"] is True, (
"the fresh cycle started by the degraded render must complete with its "
"own internal render: the repeated A key must NOT terminate it because "
"the burst was reset (zero retry queued would leave stale geometry)"
)
assert metrics["burstActive"] is False, (
"the fresh cycle must end settled: "
f"burstActive {metrics['burstActive']}"
)
assert metrics["seenKeys"] == [] and metrics["queuedOrigin"] is None, (
"settlement must clear the seen-key memory and any queued provenance: "
f"seenKeys {metrics['seenKeys']}, queuedOrigin {metrics['queuedOrigin']}"
)
+50 -3
View File
@@ -52,10 +52,57 @@ def test_virtualization_affordances_have_styling_hooks():
assert "border-radius:999px" in CSS
def test_measurement_rerenders_are_bounded_per_virtual_window_cycle():
assert "const MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS=2;" in UI_JS
def test_measurement_rerenders_are_cycle_aware_per_virtual_window_burst():
assert "function _messageVirtualMeasurementCycleKeyFor(windowMetrics)" in UI_JS
assert "function _scheduleMessageVirtualMeasurementRefresh(windowMetrics)" in UI_JS
assert "if(_messageVirtualMeasurementRetryCount>=MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS) return;" in UI_JS
assert "_scheduleMessageVirtualMeasurementRefresh(virtualWindow);" in UI_JS
assert "_markMessageVirtualMeasurementsSettled(virtualWindow);" in UI_JS
# Cycle-aware burst tracking: unseen keys proceed, repeated keys terminate,
# and the seen-key memory lives in _messageVirtualMeasurementSeenKeys.
assert "_messageVirtualMeasurementSeenKeys.includes(cycleKey)" in UI_JS
assert "_messageVirtualMeasurementSeenKeys.push(cycleKey)" in UI_JS
assert "function _resetMessageVirtualMeasurementBurst()" in UI_JS
def test_measurement_burst_not_reset_by_cycle_key_change():
"""#6654/#6717: the cycle-key branch of _scheduleMessageVirtualMeasurementRefresh
must record the new key but never reset the burst state. Resetting on key
change lets WebKit's A->B->A->B metric oscillation renew the chain forever,
keeping the rAF/measure loop alive. The ONLY resets allowed inside the
scheduler are: the repeated-key termination (oscillation) and the fresh-burst
start (when no burst is active), guarded by if(!_messageVirtualMeasurementBurstActive)."""
idx = UI_JS.index("function _scheduleMessageVirtualMeasurementRefresh(windowMetrics)")
end = UI_JS.index("function _markMessageVirtualMeasurementsSettled", idx)
body = UI_JS[idx:end]
# The key-change branch must only assign the key.
assert "if(_messageVirtualMeasurementCycleKey!==cycleKey){" in body
key_branch_start = body.index("if(_messageVirtualMeasurementCycleKey!==cycleKey){")
key_branch_end = body.index("}", key_branch_start)
key_branch = body[key_branch_start:key_branch_end]
assert "_messageVirtualMeasurementSeenKeys" not in key_branch, (
"cycle-key change must not reset the burst's seen-key memory (issue #6654)"
)
assert "_messageVirtualMeasurementBurstActive=false" not in key_branch, (
"cycle-key change must not end the burst (issue #6654)"
)
# The fresh-burst reset is allowed only at the start of a new external cycle.
assert "if(!_messageVirtualMeasurementBurstActive){" in body
guard_pos = body.index("if(!_messageVirtualMeasurementBurstActive){")
seen_reset_pos = body.index("_messageVirtualMeasurementSeenKeys=[];")
assert guard_pos < seen_reset_pos, (
"the seen-key reset must be guarded by the burst-active check "
"(per-burst lifecycle, issue #6717)"
)
# A NON-consecutive repeat (A -> B -> A) is oscillation and terminates the
# burst. A consecutive repeat (same window needing a second re-measure, e.g.
# a row shrinking 1500 -> 1400 -> 100 px) proceeds; unseen keys proceed.
assert "if(cycleKey !== lastKey && _messageVirtualMeasurementSeenKeys.includes(cycleKey)){" in body
assert "_messageVirtualMeasurementSeenKeys.push(cycleKey);" in body
# The internal-measurement origin travels WITH the scheduled render request
# (threaded through both rAF layers), never as a global consumable flag that
# a coalesced external render could steal (#6717 re-gate MUST-FIX 2).
assert "origin:'internal'" in body
assert "_messageVirtualMeasurementRenderPending" not in UI_JS
# Absolute per-burst ceiling: even an all-distinct key sequence terminates,
# bounding the seen-key memory (#6717 re-gate MUST-FIX 1).
assert "MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS" in UI_JS
+1 -1
View File
@@ -70,7 +70,7 @@ let _visWithIdxCacheLen = 0;
let _visWithIdxCacheSrc = null;
const heightStart = src.indexOf('const MESSAGE_RENDER_WINDOW_DEFAULT');
const heightEnd = src.indexOf('const MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS', heightStart);
const heightEnd = src.indexOf('let _messageVirtualMeasurementSeenKeys', heightStart);
if(heightStart !== -1 && heightEnd !== -1) eval(src.slice(heightStart, heightEnd));
if(src.indexOf('function _isProcessWakeupMessage') !== -1) eval(extractFunc('_isProcessWakeupMessage'));
eval(extractFunc('_stripWorkspaceDisplayPrefix'));
+2 -1
View File
@@ -793,7 +793,8 @@ def test_renderMessages_preserves_loading_placeholder_for_session_switch(cleanup
ui_src = (REPO_ROOT / "static/ui.js").read_text()
fn_start = ui_src.find("function renderMessages")
assert fn_start >= 0, "renderMessages() not found in ui.js"
fn_body = ui_src[fn_start:fn_start + 1400]
# Window sized to reach the render-window reset; renderMessages() preamble grows (#6717).
fn_body = ui_src[fn_start:fn_start + 2400]
compact = re.sub(r"\s+", "", fn_body)
assert (