fix(perf): cycle-aware measurement burst tracking (#6654)

Replace the flat 2-render burst cap with cycle-aware tracking: unseen
monotonic window keys proceed (genuine convergence A->B->C->settled is
never capped), while repeated oscillating keys (A->B->A->B) terminate the
burst so the rAF/measure loop stays bounded. Thread an internal-measurement
provenance marker through both rAF layers into renderMessages() and reset
burst state on every unmarked (externally initiated) render trigger, so a
pending internal callback can never starve a legitimate new cycle.
This commit is contained in:
webtecnica committed 2026-08-30 21:29:24 -03:00
1 parent f88cc78286
commit e5742d5dda
4 files changed
+319 -142

No files matched your search

+64 -22
View File
@@ -536,7 +536,14 @@ 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.
let _messageVirtualMeasurementSeenKeys=[];
let _messageRenderWindowSid=null;
let _messageRenderWindowSize=MESSAGE_RENDER_WINDOW_DEFAULT;
let _messageVirtualHeightCache=[];
@@ -547,8 +554,13 @@ let _messageVirtualEstimatedRowHeight=_messageVirtualDefaultHeightForRole('defau
let _messageVirtualScrollRaf=0;
let _messageVirtualWindowKey='';
let _messageVirtualMeasurementCycleKey='';
let _messageVirtualMeasurementRetryCount=0;
let _messageVirtualMeasurementBurstActive=false;
// Provenance marker: true while an internally measurement-scheduled render is
// queued in the rAF layers. renderMessages() resets the burst on every
// UNMARKED (externally initiated) render trigger, so a message append /
// scroll-settle / other overlapping external render always starts a fresh
// cycle even when an internal measurement callback is still pending.
let _messageVirtualMeasurementRenderPending=false;
let _messageVirtualScrollActive=false;
let _messageVirtualScrollSettleTimer=0;
let _messageVirtualDeferredMeasurement=null;
@@ -595,8 +607,7 @@ function _clearMessageVirtualHeightCache(){
_messageVirtualEstimatedRowHeight=_messageVirtualDefaultHeightForRole('default');
_messageVirtualWindowKey='';
_messageVirtualMeasurementCycleKey='';
_messageVirtualMeasurementRetryCount=0;
_messageVirtualMeasurementBurstActive=false;
_resetMessageVirtualMeasurementBurst();
_messageVirtualScrollActive=false;
clearTimeout(_messageVirtualScrollSettleTimer);
_messageVirtualScrollSettleTimer=0;
@@ -721,6 +732,11 @@ function _messageVirtualMeasurementCycleKeyFor(windowMetrics){
windowMetrics.tailStart||0,
].join(':');
}
function _resetMessageVirtualMeasurementBurst(){
_messageVirtualMeasurementSeenKeys=[];
_messageVirtualMeasurementBurstActive=false;
_messageVirtualMeasurementRenderPending=false;
}
function _scheduleMessageVirtualMeasurementRefresh(windowMetrics){
if(_messageVirtualScrollActive){
_messageVirtualDeferredMeasurement=windowMetrics;
@@ -730,32 +746,39 @@ function _scheduleMessageVirtualMeasurementRefresh(windowMetrics){
if(_messageVirtualMeasurementCycleKey!==cycleKey){
_messageVirtualMeasurementCycleKey=cycleKey;
}
// Per-burst retry budget (#6717): the counter is preserved across ONLY the
// Cycle-aware burst tracking (#6717 re-gate): the burst follows ONLY the
// internally measurement-scheduled chain (requestAnimationFrame ->
// _scheduleMessageVirtualizedRender -> renderMessages -> re-measure -> here),
// so WebKit's A/B window-metric oscillation can never renew the budget and
// keep the rAF/measure loop alive (#6654). It is reset exactly once when a
// later EXTERNALLY initiated render begins a genuinely new measurement cycle
// (session load, message append, real content change) — never on cycle-key
// changes. A cycle-key change alone just records the new key above.
// _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){
_messageVirtualMeasurementRetryCount=0;
_messageVirtualMeasurementSeenKeys=[];
_messageVirtualMeasurementBurstActive=true;
}
if(_messageVirtualMeasurementRetryCount>=MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS){
// Budget exhausted: this burst is over (no further internal re-render is
// scheduled), so the next externally initiated cycle starts fresh instead
// of being starved of its own retries.
_messageVirtualMeasurementBurstActive=false;
if(_messageVirtualMeasurementSeenKeys.includes(cycleKey)){
// Repeated key: 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.
_resetMessageVirtualMeasurementBurst();
return;
}
_messageVirtualMeasurementRetryCount++;
_messageVirtualMeasurementSeenKeys.push(cycleKey);
// Provenance marker for the internal measurement chain: the render we are
// scheduling must reach renderMessages() marked as internal (threaded
// through BOTH rAF layers via _scheduleMessageVirtualizedRender) so it
// preserves the burst instead of resetting it.
_messageVirtualMeasurementRenderPending=true;
requestAnimationFrame(()=>{ _scheduleMessageVirtualizedRender(true); });
}
function _markMessageVirtualMeasurementsSettled(windowMetrics){
_messageVirtualMeasurementCycleKey=_messageVirtualMeasurementCycleKeyFor(windowMetrics);
_messageVirtualMeasurementRetryCount=0;
_messageVirtualMeasurementBurstActive=false;
_resetMessageVirtualMeasurementBurst();
}
function _messageVirtualHeightEntryMatches(previousEntry, nextEntry){
return !!(
@@ -1486,6 +1509,13 @@ function _scheduleMessageVirtualizedRender(force){
if(_messageVirtualScrollRaf) return;
_messageVirtualScrollRaf=requestAnimationFrame(()=>{
_messageVirtualScrollRaf=0;
// Consume the internal-measurement provenance marker (set by
// _scheduleMessageVirtualMeasurementRefresh before the first rAF layer and
// threaded through it into this second rAF layer). If an EXTERNAL render
// reset the burst meanwhile, this callback degrades to an unmarked render
// and starts a fresh measurement cycle instead of continuing a stale one.
const internalMeasurement=_messageVirtualMeasurementRenderPending;
_messageVirtualMeasurementRenderPending=false;
const liveVisWithIdx=_getVisibleMessagesWithIdx();
const liveWindow=_currentMessageVirtualWindow(liveVisWithIdx,_messageVirtualKeepTailCount());
const liveKey=_messageVirtualWindowKeyFor(liveWindow);
@@ -1493,14 +1523,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; }
});
@@ -16565,6 +16595,18 @@ function _processWakeupCardHtml(info, rawText, extras){
function renderMessages(options){
_lastMessageRenderAt=performance.now();
// Measurement-burst lifecycle (#6654/#6717): every UNMARKED (externally
// initiated) render — session load, message append, scroll-settle, genuine
// content change — starts a fresh measurement burst HERE, before any rAF
// coalescing or early return, so a pending internal measurement callback can
// never carry a stale burst into the new cycle (external provenance
// overrides a pending internal callback). Only the internally
// measurement-scheduled chain (options._internalMeasurement, threaded through
// both rAF layers by _scheduleMessageVirtualMeasurementRefresh and
// _scheduleMessageVirtualizedRender) preserves the burst across renders.
if(!(options&&options._internalMeasurement)){
_resetMessageVirtualMeasurementBurst();
}
const preserveScroll=!!(options&&options.preserveScroll);
const virtualFallback=!!(options&&options._virtualFallback);
// Capture the pre-wipe scroll position when preserving OR when the reader has
+229 -100
View File
@@ -1111,8 +1111,9 @@ let _messageVirtualHeightCacheSrc = {};
let _messageVirtualEstimatedRowHeight = 200;
let _messageVirtualWindowKey = 'old-key';
let _messageVirtualMeasurementCycleKey = 'old-cycle';
let _messageVirtualMeasurementRetryCount = 3;
let _messageVirtualMeasurementBurstActive = false;
let _messageVirtualMeasurementSeenKeys = ['stale-key'];
let _messageVirtualMeasurementBurstActive = true;
let _messageVirtualMeasurementRenderPending = true;
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
@@ -1120,6 +1121,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({
@@ -1127,6 +1129,9 @@ console.log(JSON.stringify({
settleTimer: _messageVirtualScrollSettleTimer,
deferred: _messageVirtualDeferredMeasurement,
timerCleared: timerCleared,
seenKeys: _messageVirtualMeasurementSeenKeys,
burstActive: _messageVirtualMeasurementBurstActive,
renderPending: _messageVirtualMeasurementRenderPending,
}));
"""
metrics = json.loads(_run_node(source))
@@ -1142,121 +1147,245 @@ console.log(JSON.stringify({
assert metrics["timerCleared"] is True, (
"_clearMessageVirtualHeightCache must call clearTimeout on the pending settle timer"
)
def test_measurement_retry_budget_is_global_across_cycle_key_oscillation():
"""#6654: _scheduleMessageVirtualMeasurementRefresh must NOT reset
_messageVirtualMeasurementRetryCount when _messageVirtualMeasurementCycleKey
changes INSIDE one measurement burst. If WebKit alternates between two
measured window-metric sets (A -> B -> A -> B), each key transition used to
reset the two-render budget, so the rAF/geometry-measurement loop never
terminated (~30% WebContent CPU). The MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS
budget must cap the whole convergence burst; the burst ends at the cap (or
when measurement settles), so the NEXT externally initiated cycle gets a
fresh budget. The harness drives the thrash causally through the internal
chain (each scheduled render re-measures the next oscillating key)."""
js = UI_JS_PATH.read_text(encoding="utf-8")
source = _extract_func_script(js) + """
let scheduled = 0;
let _messageVirtualMeasurementCycleKey = '';
let _messageVirtualMeasurementRetryCount = 0;
let _messageVirtualMeasurementBurstActive = false;
let _messageVirtualScrollActive = false;
let _messageVirtualDeferredMeasurement = null;
const MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS = 2;
function _messageVirtualMeasurementCycleKeyFor(w) { return w.key; }
// Drive the burst causally: each internally scheduled render re-measures and
// re-enters the scheduler with the next WebKit-oscillated key.
let keys = ['B','A','B','A'];
let idx = 0;
function _scheduleMessageVirtualizedRender(force){
scheduled++;
if(idx<keys.length){
_scheduleMessageVirtualMeasurementRefresh({ key: keys[idx++] });
}
}
function requestAnimationFrame(cb){ cb(); }
eval(extractFunc('_scheduleMessageVirtualMeasurementRefresh'));
// External trigger measures key A; the internal chain then oscillates A->B->A->B.
_scheduleMessageVirtualMeasurementRefresh({ key: 'A' });
console.log(JSON.stringify({
scheduled: scheduled,
retries: _messageVirtualMeasurementRetryCount,
burstActive: _messageVirtualMeasurementBurstActive,
}));
"""
metrics = json.loads(_run_node(source))
assert metrics["scheduled"] == 2, (
"cycle-key oscillation must not reset the retry budget within a burst: "
f"scheduled {metrics['scheduled']} re-renders (expected 2, the "
"MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS cap for the whole burst)"
)
assert metrics["retries"] == 2, (
"retry count must keep accumulating across key changes within the burst: "
f"ended at {metrics['retries']} (expected 2)"
assert metrics["seenKeys"] == [], (
"_clearMessageVirtualHeightCache must clear the measurement-burst seen-key "
f"memory: seenKeys {metrics['seenKeys']}"
)
assert metrics["burstActive"] is False, (
"the burst must end once the budget is exhausted so the next externally "
f"initiated cycle can reset it: burstActive {metrics['burstActive']}"
"_clearMessageVirtualHeightCache must end any active measurement burst: "
f"burstActive {metrics['burstActive']}"
)
assert metrics["renderPending"] is False, (
"_clearMessageVirtualHeightCache must clear the internal-measurement "
f"provenance marker: renderPending {metrics['renderPending']}"
)
def test_external_measurement_after_thrash_gets_fresh_retry_budget():
"""#6717 re-gate: exhausting the budget with the WebKit A/B thrash must NOT
starve a genuinely new externally initiated measurement cycle. Drive the
A/B oscillation causally until the per-burst budget is exhausted, then start
an independent changed-C measurement WITHOUT touching private state:
(a) C must still receive its own retries, and (b) C's own internal
oscillation must remain capped at MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS."""
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")
source = _extract_func_script(js) + """
let scheduled = 0;
return _extract_func_script(js) + """
// ── 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 _messageVirtualMeasurementRetryCount = 0;
let _messageVirtualMeasurementSeenKeys = [];
let _messageVirtualMeasurementBurstActive = false;
let _messageVirtualMeasurementRenderPending = false;
let _messageVirtualScrollActive = false;
let _messageVirtualDeferredMeasurement = null;
const MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS = 2;
function _messageVirtualMeasurementCycleKeyFor(w) { return w.key; }
let keys = [];
let idx = 0;
function _scheduleMessageVirtualizedRender(force){
scheduled++;
if(idx<keys.length){
_scheduleMessageVirtualMeasurementRefresh({ key: keys[idx++] });
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 });
}
}
function requestAnimationFrame(cb){ cb(); }
// Production functions under test.
eval(extractFunc('_resetMessageVirtualMeasurementBurst'));
eval(extractFunc('_messageVirtualWindowKeyFor'));
eval(extractFunc('_scheduleMessageVirtualMeasurementRefresh'));
// Phase 1 — drive the A/B thrash causally until the burst budget is exhausted.
keys = ['B','A','B','A'];
idx = 0;
_scheduleMessageVirtualMeasurementRefresh({ key: 'A' });
const afterThrash = scheduled;
// Phase 2 — an independent genuine content change C arrives. It must NOT be
// starved: it gets a fresh per-burst budget, and its own oscillation is capped.
keys = ['D','C','D','C'];
idx = 0;
_scheduleMessageVirtualMeasurementRefresh({ key: 'C' });
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({
afterThrash: afterThrash,
scheduled: scheduled,
retries: _messageVirtualMeasurementRetryCount,
renders: renderLog.length,
internal: renderLog.filter(r => r.internal).length,
burstActive: _messageVirtualMeasurementBurstActive,
seenKeys: _messageVirtualMeasurementSeenKeys,
renderPending: _messageVirtualMeasurementRenderPending,
}));
"""
""")
metrics = json.loads(_run_node(source))
assert metrics["afterThrash"] == 2, (
"the A/B thrash burst must stop at MESSAGE_VIRTUAL_MEASUREMENT_"
f"MAX_RERENDERS: scheduled {metrics['afterThrash']} (expected 2)"
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["scheduled"] == 4, (
"a genuinely new external measurement cycle C must still receive its "
"own retries after the thrash exhausted the budget: "
f"scheduled {metrics['scheduled']} (expected 4 = 2 thrash + 2 for C)"
assert metrics["renders"] == 3, (
"oscillation must not keep scheduling renders: "
f"{metrics['renders']} renders (expected 3 = 1 external + 2 internal)"
)
assert metrics["retries"] == 2, (
"C's own internal oscillation must remain capped at "
f"MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS: retries "
f"{metrics['retries']} (expected 2)"
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["renderPending"] is False, (
"no internal render may remain pending after termination: "
f"renderPending {metrics['renderPending']}"
)
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,
renderPending: _messageVirtualMeasurementRenderPending,
}));
""")
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["renderPending"] is False, (
"settlement must clear the seen-key memory and any pending marker: "
f"seenKeys {metrics['seenKeys']}, renderPending {metrics['renderPending']}"
)
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.
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 joins the fresh cycle (R2), then R3 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,
appendCycleInternal: renderLog[2].internal && renderLog[3].internal,
staleCallbackUnmarked: renderLog[6].internal,
postResetCycleInternal: renderLog[7].internal,
burstActive: _messageVirtualMeasurementBurstActive,
seenKeys: _messageVirtualMeasurementSeenKeys,
renderPending: _messageVirtualMeasurementRenderPending,
}));
""")
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["appendCycleInternal"] is True, (
"the append's cycle must proceed with its own internal renders "
"(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["renderPending"] is False, (
"settlement must clear the seen-key memory and any pending marker: "
f"seenKeys {metrics['seenKeys']}, renderPending {metrics['renderPending']}"
)
+25 -19
View File
@@ -52,23 +52,25 @@ 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){" 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_retry_budget_not_reset_by_cycle_key_change():
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 _messageVirtualMeasurementRetryCount.
Resetting on key change lets WebKit's A->B->A->B metric oscillation renew
the two-render budget forever, keeping the rAF/measure loop alive. The ONLY
reset allowed inside the scheduler is the per-burst one: when a NEW
externally initiated cycle begins (no burst active), guarded by
if(!_messageVirtualMeasurementBurstActive)."""
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]
@@ -77,17 +79,21 @@ def test_measurement_retry_budget_not_reset_by_cycle_key_change():
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 "_messageVirtualMeasurementRetryCount=0;" not in key_branch, (
"cycle-key change must not reset the retry budget (issue #6654)"
assert "_messageVirtualMeasurementSeenKeys" not in key_branch, (
"cycle-key change must not reset the burst's seen-key memory (issue #6654)"
)
# The per-burst reset is allowed only at the start of a new external cycle.
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){")
reset_pos = body.index("_messageVirtualMeasurementRetryCount=0;")
assert guard_pos < reset_pos, (
"the budget reset must be guarded by the burst-active check "
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)"
)
# The budget guard and increment must still be present.
assert "if(_messageVirtualMeasurementRetryCount>=MESSAGE_VIRTUAL_MEASUREMENT_MAX_RERENDERS){" in body
assert "_messageVirtualMeasurementRetryCount++;" in body
# Repeated keys terminate the burst; unseen keys proceed.
assert "if(_messageVirtualMeasurementSeenKeys.includes(cycleKey)){" in body
assert "_messageVirtualMeasurementSeenKeys.push(cycleKey);" in body
assert "_messageVirtualMeasurementRenderPending=true;" in body
+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'));