bend_puzzle/test/v4778-interaction-scheduler-smoke-test.js

144 lines
23 KiB
JavaScript
Raw Normal View History

2026-07-30 15:13:53 +09:00
'use strict';
const {assert,functionSource,app,css,vm}=require('./helpers/app-source');
const fs=require('fs'),path=require('path'),browserBenchmark=fs.readFileSync(path.join(__dirname,'browser-performance-benchmark.js'),'utf8');
assert(app.includes('AUXILIARY_FPS=30')&&app.includes('DRAG_TARGET_FPS=60')&&app.includes('DRAG_FRAME_INTERVAL=1000/DRAG_TARGET_FPS')&&app.includes('CAMERA_DISPLAY_WATCHDOG_MS=18'),'Interaction and auxiliary frame budgets are not separated');
assert(app.includes('BEND_INTERACTION_SCHEDULER')&&app.includes('bend-field-interaction-scheduler-variant')&&app.includes('batteryDischargingTime'),'Scheduler rollout variant or battery telemetry guardrail is missing');
assert(app.includes('DRAG_MAX_CATCHUP_CELLS=24')&&functionSource('extendPointerTo').includes('catchupPending')&&functionSource('processBoardDragFrame').includes('catchupPending'),'Large pointer jumps are not bounded and resumed across drag frames');
const cursor=functionSource('updateCustomCursorFromPointer'),cursorSync=functionSource('syncCursorAppearance');
assert(cursor.includes('requestAnimationFrame')&&!cursor.includes('FRAME_INTERVAL'),'DOM cursor fallback is not synchronized to every display frame');
assert(app.includes("addEventListener('pointermove',updateCustomCursorFromPointer")&&app.includes("addEventListener('pointerrawupdate',updateCustomCursorFromPointer"),'DOM cursor does not tolerate sparse raw-input delivery');
assert(cursor.includes('customCursorActiveUntil=perfNow()+40')&&cursor.includes('customCursorFrame=requestAnimationFrame(step)')&&cursor.includes('Math.exp(-elapsed/8)'),'DOM cursor does not keep and smooth its display-rAF loop between active pointer events');
assert(!cursor.includes("classList.contains('is-drawing')")&&!css.includes('body.is-drawing #customEmojiCursor'),'Custom cursor is hidden during pickup dragging');
assert(cursorSync.includes('nativeSupported')&&cursorSync.includes('--active-native-cursor')&&css.includes('data-cursor-mode="native"'),'Native cursor assets or the DOM fallback switch are missing');
const cursorClasses=new Set(),cursorFrames=[],cursorRafQueue=[],cursorTransforms=[],cursorPerfSamples=new Map();
let cursorClock=0,cursorRafId=0;
const cursorContext={
document:{body:{dataset:{cursorMode:'dom'}}},
customEmojiCursor:{classList:{contains:name=>cursorClasses.has(name),add:name=>cursorClasses.add(name),remove:name=>cursorClasses.delete(name)},style:{set transform(value){cursorTransforms.push(value)}}},
perfNow:()=>cursorClock,
requestAnimationFrame:callback=>{cursorRafQueue.push(callback);return++cursorRafId},
markVisualFrame:timestamp=>cursorFrames.push(timestamp),
perfObserve:(name,value)=>{const samples=cursorPerfSamples.get(name)||[];samples.push(value);cursorPerfSamples.set(name,samples);return value},
perfCount:()=>{}
};
vm.createContext(cursorContext);
vm.runInContext(`const interactionCommitAt=Object.create(null),interactionInputAt=Object.create(null);${functionSource('recordInteractionCommit')}let customCursorFrame=0,customCursorX=0,customCursorY=0,customCursorInputAt=0,customCursorActiveUntil=0,customCursorRenderedX=NaN,customCursorRenderedY=NaN,customCursorLastFrameAt=0,customCursorInputRevision=0,customCursorCommittedRevision=0;${cursor};this.updateCustomCursorFromPointer=updateCustomCursorFromPointer;`,cursorContext);
let cursorEventAt=0;
for(let frameAt=1000/60;frameAt<=1100;frameAt+=1000/60){
while(cursorEventAt<=1000&&cursorEventAt<=frameAt+.001){
cursorClock=cursorEventAt;cursorContext.updateCustomCursorFromPointer({clientX:cursorEventAt,clientY:cursorEventAt/2,timeStamp:cursorEventAt||.001,pointerType:'mouse',target:{isConnected:true}});
cursorEventAt+=1000/30;
}
cursorClock=frameAt;
for(const callback of cursorRafQueue.splice(0))callback(frameAt);
}
const cursorFrameGaps=cursorFrames.slice(1).map((timestamp,index)=>timestamp-cursorFrames[index]).sort((a,b)=>a-b),
cursorMedianGap=cursorFrameGaps[Math.floor((cursorFrameGaps.length-1)*.5)],
sortedCursorAges=[...(cursorPerfSamples.get('cursorInputAge')||[])].sort((a,b)=>a-b),cursorP95Age=sortedCursorAges[Math.floor((sortedCursorAges.length-1)*.95)],
measuredCursorGaps=[...(cursorPerfSamples.get('cursorFrameGap')||[])].sort((a,b)=>a-b),measuredCursorMedianGap=measuredCursorGaps[Math.floor((measuredCursorGaps.length-1)*.5)],
changedCursorTransforms=cursorTransforms.filter((value,index)=>index===0||value!==cursorTransforms[index-1]).length;
assert(cursorFrames.length>=60&&cursorMedianGap<=20,`Warm DOM cursor loop produced ${cursorFrames.length} frames with a ${cursorMedianGap} ms median gap`);
assert(measuredCursorGaps.length>=30&&measuredCursorMedianGap<=20,`Production cursor instrumentation captured ${measuredCursorGaps.length} gaps with a ${measuredCursorMedianGap} ms median`);
assert(changedCursorTransforms>=55,`Warm DOM cursor loop changed its transform on only ${changedCursorTransforms} display frames`);
assert(sortedCursorAges.length>=25,`Production cursor instrumentation captured only ${sortedCursorAges.length} fresh-input latency samples`);
assert(cursorP95Age<25,`Warm DOM cursor loop produced ${cursorP95Age} ms p95 input age`);
const cameraQueue=functionSource('queueCameraInteraction'),cameraApply=functionSource('applyCamera');
assert(cameraQueue.includes('pendingCameraInteraction=next')&&cameraQueue.includes('requestAnimationFrame')&&cameraQueue.includes('CAMERA_DISPLAY_WATCHDOG_MS')&&cameraQueue.includes('cancelAnimationFrame')&&cameraQueue.includes('cameraInteractionLastDraw+DRAG_FRAME_INTERVAL'),'Camera does not use a latest-input 60 Hz queue with a missed-vsync watchdog');
assert(cameraApply.includes('positionCachedWorldOverview')&&functionSource('positionCachedWorldOverview').includes('translate3d'),'Overview position is not updated in the camera fast path');
assert(functionSource('scheduleWorldOverview').includes('requestIdleCallback')&&functionSource('scheduleWorldOverview').includes("classList.contains('is-interacting')"),'Overview rebuilds are not idle and interaction-safe');
const dragSchedule=functionSource('scheduleBoardDragFrame'),dragFrame=functionSource('processBoardDragFrame');
assert(dragSchedule.includes('requestAnimationFrame')&&dragSchedule.includes('DRAG_DISPLAY_WATCHDOG_MS')&&dragSchedule.includes('cancelAnimationFrame')&&dragSchedule.includes('clearTimeout')&&dragSchedule.includes('b.dragVisualLastFrameAt+DRAG_FRAME_INTERVAL')&&dragSchedule.includes('INTERACTION_FRAME_TOLERANCE_MS')&&dragFrame.includes('DRAG_FRAME_INTERVAL'),'Pickup visuals or logic are not capped to their 60 Hz lane');
assert(functionSource('queueBoardPointerMove').includes('appendBoardPointerSamples')&&functionSource('queueBoardPointerMove').includes('commitBoardDragFromInputDeadline')&&functionSource('queueBoardPointerMove').includes('scheduleBoardDragFrame')&&!functionSource('queueBoardPointerMove').includes('updatePickupHandleOverlay')&&!functionSource('queueBoardPointerMove').includes('markVisualFrame')&&!functionSource('queueBoardPointerMove').includes('extendPointerTo'),'Pointer events mutate presentation or model work outside the capped scheduler');
assert(functionSource('commitBoardDragFromInputDeadline').includes('b.dragVisualLastFrameAt+DRAG_FRAME_INTERVAL')&&functionSource('commitBoardDragFromInputDeadline').includes('INTERACTION_FRAME_TOLERANCE_MS')&&functionSource('commitBoardDragFromInputDeadline').includes('processBoardDragFrame(b,now)'),'Pickup input deadline fallback is not governed by the 60 Hz presentation ceiling');
assert(!app.includes('DRAG_INPUT_RESCUE_MS')&&!app.includes('function rescuePickupVisualFromInput('),'An input-rate pickup presentation path can still exceed 60 Hz');
assert(functionSource('setBoardPointerSample').includes('dragVisualActiveUntil=perfNow()+40')&&dragFrame.includes('Math.exp(-visualElapsed/8)')&&dragFrame.includes('updateDrawingHandlePosition')&&dragFrame.includes('perfNow()<b.dragVisualActiveUntil'),'Pickup visual lane does not remain warm and ease toward sparse pointer samples at display cadence');
assert(functionSource('renderDragFrame').includes('translate3d')&&functionSource('renderDragFrame').includes('tailNow-drawing.lastTailRenderAt>=32'),'Complex pickup handles are not compositor-driven or the SVG live tail is repainted every display frame');
assert(dragFrame.includes('updatePickupHandleOverlay')&&dragFrame.includes('usesLightweightDragOverlay')&&dragFrame.includes('dirtyBoards.delete(b)')&&app.includes('LIGHTWEIGHT_DRAG_BOARD_CELLS=120')&&functionSource('updatePickupHandleOverlay').includes('translate3d')&&css.includes('#pickupHandleOverlay{position:fixed'),'Large-board pickup still repaints the SVG during the gesture instead of using the lightweight display-rate handle');
assert(functionSource('activateBoardPointerDrag').includes('setPickupScenePresentation(b,true)')&&functionSource('setLightweightDragPresentation').includes("style.opacity=next?'0':''")&&functionSource('setPickupScenePresentation').includes('setLightweightDragPresentation(board,next&&board===activeBoard)')&&functionSource('scheduleInteractionSettlePresentation').includes('setPickupScenePresentation(null,false)'),'Active large-board decorative layers are not compositor-suppressed during pickup and restored after settlement');
assert(dragFrame.includes("recordInteractionCommit('pickupVisual'")&&dragFrame.includes('workDuration'),'Pickup cadence, input age, or callback work is not instrumented');
assert(dragFrame.includes('timestamp<=b.dragVisualActiveUntil')&&dragFrame.includes("recordInteractionCommit('pickupVisual',timestamp,move.inputAt,false)")&&css.includes('transition:transform 16.67ms linear'),'Pickup sample gaps are not compositor-interpolated or warm visual ticks are not measured');
assert(dragFrame.includes('freshVisualInput')&&dragFrame.includes('freshLogicalInput'),'Pickup latency instrumentation resamples stale input on warm display frames');
assert(app.includes('DRAG_MAX_LOGICAL_SAMPLES_PER_FRAME=4')&&app.includes('DRAG_MODEL_BUDGET_MS=.5')&&dragFrame.includes('b.pointerMoveSamples.shift()')&&dragFrame.includes('perfNow()-modelStarted<DRAG_MODEL_BUDGET_MS')&&dragFrame.includes('logicalSlotDue&&(freshLogicalInput||velocity[0]||velocity[1]||b.drawing?.catchupPending||b.pointerMoveSamples?.length)'),'Pickup logic does not time-bound ordered-sample catch-up or warm frames still run model traversal without pending work');
assert(functionSource('setBoardPointerSample').includes('trimBoardPointerSamples(samples)')&&functionSource('trimBoardPointerSamples').includes('leastTurn')&&functionSource('trimBoardPointerSamples').includes('samples.splice(removeIndex,1)'),'Bounded pickup input overflow does not preserve sharp turns');
assert(dragFrame.includes('pointerInsideBoardScreen(b,logicalMove)?eventToSvg')&&functionSource('pointerInsideBoardScreen').includes('boardScreenRect'),'Off-board edge panning can still traverse and prematurely finish the pickup path');
assert(dragFrame.includes("perfEnd('pickupModelWork'")&&dragFrame.includes("perfEnd('pickupVisualWork'"),'Pickup model and visual callback costs are not independently instrumented');
assert(functionSource('clearDragRender').includes("style.display='none'")&&!functionSource('clearDragRender').includes('replaceChildren'),'Drag cache nodes are destroyed between gestures');
const bind=functionSource('bindBoard');
assert(bind.indexOf('beginPendingClaimPointer')<bind.indexOf('await ensureBoardClaimForInput'),'Shared-session pickup preview does not start before the ownership round-trip');
assert(bind.includes('updatePendingClaimPointer')&&bind.includes('b.pendingClaimPointer?.pointerId')&&functionSource('updatePendingClaimPointer').includes('pending.samples')&&functionSource('updatePendingClaimPointer').includes('trimBoardPointerSamples'),'Pending claim movement, turns, or release are not buffered');
assert(bind.includes('activateBoardPointerDrag')&&functionSource('activateBoardPointerDrag').includes('cancelBoardDragFrame(b)')&&functionSource('activateBoardPointerDrag').includes('for(const sample of samples)setBoardPointerSample')&&functionSource('activateBoardPointerDrag').includes('scheduleBoardDragFrame(b)'),'Approved pending claims do not enter the ordered drag fast path');
assert(functionSource('selectBoard').includes('alreadySelected')&&functionSource('renderDragFrame').includes('pickupStartFullBoardRenders'),'Pickup start does not avoid and measure full-board redraws');
assert(functionSource('applyBoardCommand').includes('pointerInteraction&&b.drawing?.pointerId==null')&&bind.includes('b.drawing=null;b.armedGate=null;refreshInteractionState()')&&bind.includes('scheduleBoardCommandSettlement(b,{paint:true,invalidate:true,pathIndex:finishedPathIndex})'),'Pickup completion does not end interaction state before deferring broad render and cache work');
assert(functionSource('safeRelease').includes('scheduleInteractionSettlePresentation')&&!functionSource('safeRelease').includes('queueMicrotask')&&functionSource('scheduleInteractionSettlePresentation').includes('requestAnimationFrame')&&!functionSource('scheduleInteractionSettlePresentation').includes('ensureBoards()'),'Pickup release still runs broad settlement work inside the pointer task');
assert(functionSource('finalizeAtGate').includes('deferSettlement:true')&&functionSource('scheduleBoardCommandSettlement').includes('requestAnimationFrame')&&functionSource('scheduleBoardCommandSettlement').includes("classList.contains('is-interacting')"),'Pickup completion still performs render, persistence, or solve settlement inside the active drag callback');
assert(functionSource('finalizeAtGate').includes('usesLightweightDragOverlay(b)')&&functionSource('finalizeAtGate').includes('requestAnimationFrame(()=>gateConnectEffect'),'Large-board gate decoration still mutates the SVG inside the finishing interaction task');
assert(functionSource('runDeferredSave').includes("classList?.contains('is-interacting')"),'Ordinary persistence is not deferred during interactions');
assert(functionSource('applyWorldSignal').includes('await waitForInteractionSettle()')&&functionSource('pullCloudWorld').includes('await waitForInteractionSettle()'),'Cross-tab or cloud reconciliation can still run broad refresh work during a gesture');
assert(functionSource('createPuzzleWorker').includes("perfCount('workerResultsDeferredDuringInteraction')")&&functionSource('createPuzzleWorker').includes('waitForInteractionSettle().then(deliver)'),'Puzzle-worker promise continuations can still run during an active gesture');
assert(functionSource('desiredInteractiveBoardIds').includes('for(const id of ids)')&&!app.includes('INTERACTIVE_DETAIL_CELL_BUDGET')&&!functionSource('ensureBoards').includes('makeStaticBoard(meta)'),'Visible puzzles can still be hidden behind a clicked-only summary LOD');
assert(functionSource('observeInteractionFrame').includes('workDuration>slowWorkThreshold')&&!functionSource('observeInteractionFrame').includes('elapsed>24'),'Quality fallback still mistakes frame spacing for callback overload');
assert(functionSource('refreshInteractionState').includes('interactionQualityDowngradePending'),'Quality changes are not deferred until the gesture ends');
assert(functionSource('refreshInteractionState').includes("topbar?.classList.toggle('drawing-active'")&&functionSource('refreshInteractionState').includes("world?.classList.toggle('camera-interacting'")&&functionSource('refreshInteractionState').includes('!usesLightweightDragOverlay(drawingBoard)')&&!css.includes('body.is-interacting .gate-dot')&&!css.includes('body.is-drawing #topbar'),'Pickup styling still invalidates the entire document or a lightweight large-board subtree');
assert(functionSource('recordInteractionCommit').includes('InputAge')&&functionSource('observeInteractionFrame').includes('interactionDroppedFrameRatio')&&app.includes("perfCount('longTasks')"),'Interaction latency, dropped-frame, or long-task diagnostics are missing');
assert(functionSource('ensureBoards').includes("classList?.contains?.('is-interacting')")&&functionSource('ensureBoards').includes('lodPassesDeferredDuringInteraction'),'Detailed and overview LOD work is not deferred during gestures');
assert(functionSource('drawingForPath').includes('pathCellIndex:new Map')&&functionSource('extendOne').includes('pathCellIndex?.set'),'Long pickup paths do not retain a cell index');
assert(functionSource('rewindActivePathToCell').includes('index===path.cells.length-1)return false'),'A pickup hovering on its current tip is incorrectly classified as a geometry-changing rewind');
assert(functionSource('makeBoard').includes('gateIndexesByCell')&&functionSource('gateCandidatesInCell').includes('gateIndexesByCell'),'Gate lookup is not indexed by cell');
assert(functionSource('makeBoard').includes('endpointIndexesByCell')&&functionSource('nearestEndpointAtPoint').includes('endpointCandidatesNearPoint'),'Open endpoint lookup is not indexed by cell');
assert(app.includes('REALTIME_CURSOR_INTERVAL=50')&&functionSource('queueRealtimeCursor').includes('vx:')&&functionSource('applyRemotePlayer').includes('lead=.035'),'Remote cursors are not sent at 20 Hz with velocity-based short extrapolation');
for(const probe of['measureDisplayCadence','measureCursorCadence','claimLatencyProbe','pickupEdgePanProbe','pinchZoomProbe','panCadenceProbe'])
assert(browserBenchmark.includes(`function ${probe}`)||browserBenchmark.includes(`function ${probe}(`),`Browser performance matrix is missing ${probe}`);
assert(browserBenchmark.includes('--force-device-scale-factor=1'),'Browser performance matrix must normalize device scale for stable display-cadence measurements');
const pickupCadenceProbeSource=browserBenchmark.slice(browserBenchmark.indexOf('async function pickupCadenceProbe'),browserBenchmark.indexOf('async function claimLatencyProbe'));
assert(pickupCadenceProbeSource.includes("deepClone(metaState('B0'))")&&pickupCadenceProbeSource.includes('data.states.B0=')&&pickupCadenceProbeSource.includes("persistNow({skipCloud:true})")&&pickupCadenceProbeSource.includes("snapshot:await client.evaluate('BEND_PERF.snapshot()')"),'Continuous pickup benchmark does not capture clean cadence data and restore its original board state');
assert(browserBenchmark.includes('async function restoreOriginState')&&browserBenchmark.includes('replaceObjectContents(current,deepClone(snapshot))')&&browserBenchmark.includes('setTimeout(resolve,80)')&&browserBenchmark.includes("applySnapshot();changed('B0')")&&browserBenchmark.includes("centerMeta(data.metas.B0,{select:false})")&&browserBenchmark.match(/restoreOriginState\(client,pristineOriginState\)/g)?.length>=4,'Browser matrix does not settle, revision-stamp, and canonically reset origin state and camera between pickup probes');
assert(browserBenchmark.includes('pickup probes did not restore a pristine origin board')&&browserBenchmark.includes("metaState('B0').solved===true"),'Browser matrix does not verify scenario isolation and solve completion before panning');
assert(browserBenchmark.includes("spawnSync(taskkill,['/pid',String(child.pid),'/T','/F']")&&browserBenchmark.includes('stopBrowserTree(edge)')&&browserBenchmark.includes('fs.rmSync(temporaryRoot,{recursive:true,force:true})'),'Browser benchmark does not guarantee process-tree and temporary-profile cleanup');
for(const profile of['small','medium','large','complex','long-line'])assert(browserBenchmark.includes(`name:'${profile}'`),`Browser benchmark profile ${profile} is missing`);
const longCells=Array.from({length:250},(_,index)=>[0,index]),longPath={startGate:0,endGate:null,openGate:null,cells:longCells.map(cell=>[...cell])},
longState={paths:[longPath]},longOccupancy=new Map(longCells.map(cell=>[`${cell[0]},${cell[1]}`,0])),longDrawing={pathIndex:0,pointerId:1,pathCellIndex:new Map(longCells.map((cell,index)=>[`${cell[0]},${cell[1]}`,index]))},
longBoard={id:'L',p:{g:Array.from({length:100},()=>[0,0,'N'])},drawing:longDrawing,crossingKeySet:new Set()},longContext={
activePath:()=>longPath,ckey:(r,c)=>`${r},${c}`,warpPairForCell:()=>null,isWarpTransition:()=>false,pathIndexesAtCell:()=>[],metaState:()=>longState,
applyCalls:0,applyBoardCommand:(_board,mutate)=>{longContext.applyCalls++;mutate();return true}
};
vm.createContext(longContext);vm.runInContext(`${functionSource('rewindActivePathToCell')}\nthis.rewindActivePathToCell=rewindActivePathToCell;`,longContext);
assert(longContext.rewindActivePathToCell(longBoard,[0,10],longOccupancy)&&longPath.cells.length===11&&longDrawing.pathCellIndex.size===11&&longOccupancy.size===11&&longContext.applyCalls===1,'A 250-cell pickup does not rewind with one suffix splice');
const traversalContext={PAD:0,CELL:1,ckey:(r,c)=>`${r},${c}`};vm.createContext(traversalContext);
vm.runInContext(`${functionSource('cellsCrossedBySegment')}\nthis.cellsCrossedBySegment=cellsCrossedBySegment;`,traversalContext);
const validLongCells=new Set(longCells.map(([r,c])=>`${r},${c}`)),forward=traversalContext.cellsCrossedBySegment([.5,.5],[249.5,.5],{preferredAxis:'H',validCells:validLongCells});
assert(forward.length===249&&forward[248][1]===249,'A 250-cell pointer segment does not preserve every forward grid cell');
let remainingCatchup=forward.length,batches=0,maxBatch=0;while(remainingCatchup){const batch=Math.min(24,remainingCatchup);maxBatch=Math.max(maxBatch,batch);remainingCatchup-=batch;batches++}
assert(maxBatch===24&&batches===11,'A 250-cell catch-up is not bounded to constant-size frame batches');
const endpointState={paths:Array.from({length:100},(_,index)=>({endGate:null,detachedStart:index%2===0,cells:[[0,index],[1,index]]}))},
endpointContext={Map,ckey:(r,c)=>`${r},${c}`,metaState:()=>endpointState};
vm.createContext(endpointContext);vm.runInContext(`${functionSource('rebuildEndpointIndexes')}\nthis.rebuildEndpointIndexes=rebuildEndpointIndexes;`,endpointContext);
const endpointBoard={id:'E',endpointIndexesByCell:new Map()},endpointIndex=endpointContext.rebuildEndpointIndexes(endpointBoard);
assert(endpointIndex.size===150&&endpointIndex.get('1,99')[0].index===99,'The 100-path endpoint index is incomplete');
const remoteContext={Number,Map,realtimePresenceId:null,remotePlayers:new Map(),schedulePresenceRender:()=>{},scheduleMinimap:()=>{}};
vm.createContext(remoteContext);vm.runInContext(`${functionSource('applyRemotePlayer')}\nthis.applyRemotePlayer=applyRemotePlayer;`,remoteContext);
assert(remoteContext.applyRemotePlayer({presenceId:'R',x:10,y:20,vx:2,vy:-1,sentAt:123,name:'R'}),'Remote cursor sample was rejected');
const remoteSample=remoteContext.remotePlayers.get('R');
assert(Math.abs(remoteSample.targetX-10.07)<1e-9&&Math.abs(remoteSample.targetY-19.965)<1e-9&&remoteSample.sentAt===123,'Remote cursor velocity extrapolation is incorrect');
for(const refreshRate of[60,120,144]){
const displayInterval=1000/refreshRate,visualCommits=[],logicalCommits=[];let nextVisual=0,nextLogical=0;
for(let timestamp=0;timestamp<1000;timestamp+=displayInterval){
if(!nextVisual||timestamp+.01>=nextVisual){visualCommits.push(timestamp);do nextVisual=(nextVisual||timestamp)+1000/60;while(nextVisual<=timestamp+.01)}
if(!nextLogical||timestamp+2>=nextLogical){logicalCommits.push(timestamp);do nextLogical=(nextLogical||timestamp)+1000/60;while(nextLogical<=timestamp+2)}
}
assert(visualCommits.length>=59&&visualCommits.length<=61,`${refreshRate} Hz display produced ${visualCommits.length} pickup presentation frames instead of about 60`);
assert(logicalCommits.length>=59&&logicalCommits.length<=61,`${refreshRate} Hz display does not retain a 60 Hz pickup logic lane`);
}
console.log('Display-rate cursor/pan and 60 Hz pickup scheduler regression test passed');