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:
8 files changed
+600
-20
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -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']}"
|
||||
)
|
||||
@@ -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
|
||||
@@ -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'));
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in new issue
Block a user