d
This commit is contained in:
parent
c3a6f5ff37
commit
4c4e767ec6
73 changed files with 3502 additions and 741 deletions
|
|
@ -12,9 +12,12 @@ const sleep=milliseconds=>new Promise(resolve=>setTimeout(resolve,milliseconds))
|
|||
let serverPort=0;
|
||||
const debuggingPort=20000+Math.floor(Math.random()*1000),benchmarkHost=process.env.BEND_FIELD_BENCHMARK_HOST||'localhost';
|
||||
const startupOnly=process.env.BEND_FIELD_STARTUP_ONLY==='1';
|
||||
const effectVisualFixture=JSON.parse(fs.readFileSync(path.join(root,'test','fixtures','effect-visual-checkpoints.json'),'utf8'));
|
||||
const benchmarkOutputPath=process.env.BEND_FIELD_BENCHMARK_OUTPUT||path.join(root,'test-results','browser-performance-benchmark.json');
|
||||
function writeBenchmarkReport(report){fs.mkdirSync(path.dirname(benchmarkOutputPath),{recursive:true});fs.writeFileSync(benchmarkOutputPath,`${JSON.stringify(report,null,2)}\n`)}
|
||||
const temporaryRoot=fs.mkdtempSync(path.join(os.tmpdir(),'bend-field-browser-benchmark-'));
|
||||
const profilePath=path.join(temporaryRoot,'edge-profile');
|
||||
const worldDbName='bend-field:v30:v47-field-reset-20260728-interaction-fix:world';
|
||||
const worldDbName='bend-field:v30:linkfield-single-world-20260801:world';
|
||||
const allProfiles=[
|
||||
{name:'small',boards:16,mode:'grid'},
|
||||
{name:'medium',boards:128,mode:'grid'},
|
||||
|
|
@ -278,10 +281,10 @@ async function panCadenceProbe(client,steps=24){
|
|||
const viewport=document.querySelector('#viewport'),rect=viewport.getBoundingClientRect(),pointerId=91,startX=rect.left+rect.width*.7,startY=rect.top+rect.height*.6;
|
||||
const [worldX,worldY]=worldUnitAtClient(startX,startY),reactionNow=Date.now(),remoteId='benchmark-remote';
|
||||
applyRemotePlayer({presenceId:remoteId,playerId:'benchmark',name:'Remote',cursorStyle:'default',x:worldX,y:worldY,vx:.08,vy:.03,sentAt:reactionNow});
|
||||
applyRealtimeReaction({id:'benchmark-reaction',emoji:REACTION_EMOJIS[0],x:worldX,y:worldY,createdAt:reactionNow,expiresAt:reactionNow+Math.max(3000,${steps}*25)});
|
||||
applyRealtimeReaction({id:'benchmark-reaction',emoji:REACTION_EMOJIS[0],style:'firework',x:worldX,y:worldY,createdAt:reactionNow,expiresAt:reactionNow+Math.max(3400,${steps}*25)});
|
||||
const dispatch=(type,index,buttons)=>viewport.dispatchEvent(new PointerEvent(type,{bubbles:true,cancelable:true,pointerId,pointerType:'mouse',isPrimary:true,button:type==='pointerdown'||type==='pointerup'?2:-1,buttons,clientX:startX-index*7,clientY:startY-index*3}));
|
||||
dispatch('pointerdown',0,2);let index=0;
|
||||
const tick=()=>{index++;dispatch('pointermove',index,2);if(index===10)queueWorldSignal({sessionId:'benchmark-cross-tab',commitId:'benchmark:'+Date.now(),worldEpoch:data.worldEpoch,stateIds:['B1']});if(index<${steps})requestAnimationFrame(tick);else{dispatch('pointerup',index,0);setTimeout(()=>Promise.resolve(syncQueue).finally(()=>{remotePlayers.delete(remoteId);realtimeReactions.delete('benchmark-reaction');resolve(index)}),80)}};
|
||||
const tick=()=>{index++;dispatch('pointermove',index,2);if(index<${steps})requestAnimationFrame(tick);else{dispatch('pointerup',index,0);setTimeout(()=>{remotePlayers.delete(remoteId);realtimeReactions.delete('benchmark-reaction');resolve(index)},80)}};
|
||||
requestAnimationFrame(tick);
|
||||
})`);
|
||||
}
|
||||
|
|
@ -295,7 +298,9 @@ async function pinchZoomProbe(client){
|
|||
})`);
|
||||
}
|
||||
async function pickupEdgePanProbe(client,pathRow){
|
||||
const originalState=await client.evaluate("deepClone(metaState('B0'))"),start=await pointFor(client,`.board-card[data-id="B0"] .gate-hit[data-gate="${pathRow.startGate}"]`),
|
||||
const originalState=await client.evaluate("deepClone(metaState('B0'))");
|
||||
await client.evaluate("(()=>{const r=document.querySelector('#viewport').getBoundingClientRect(),p=worldUnitAtClient(r.left+r.width/2,r.top+r.height/2),now=trustedNow();applyRealtimeReaction({id:'benchmark-edge-pan-effect',emoji:REACTION_EMOJIS[0],style:'comet',x:p[0],y:p[1],createdAt:now,expiresAt:now+12000});return true})()");
|
||||
const start=await pointFor(client,`.board-card[data-id="B0"] .gate-hit[data-gate="${pathRow.startGate}"]`),
|
||||
edge=await client.evaluate("(()=>{const r=document.querySelector('#viewport').getBoundingClientRect();return{x:r.right-2,y:r.top+r.height/2}})()"),
|
||||
before=await client.evaluate("({x:cam.x,y:cam.y,frames:BEND_PERF.snapshot().counters.dragFrames||0})");
|
||||
await client.send('Input.dispatchMouseEvent',{type:'mousePressed',x:start.x,y:start.y,button:'left',buttons:1,clickCount:1});
|
||||
|
|
@ -306,7 +311,7 @@ async function pickupEdgePanProbe(client,pathRow){
|
|||
return{cameraMoved:Math.hypot(after.x-before.x,after.y-before.y)>5,dragFrames:after.frames-before.frames};
|
||||
}finally{
|
||||
await client.send('Input.dispatchMouseEvent',{type:'mouseReleased',x:edge.x,y:edge.y,button:'left',buttons:0,clickCount:1}).catch(()=>{});
|
||||
await client.evaluate(`(()=>{data.states.B0=${JSON.stringify(originalState)};normalizedStateObjects.add(data.states.B0);markStateDirty('B0');renderBoardNow(rendered.get('B0'));return true})()`);
|
||||
await client.evaluate(`(()=>{realtimeReactions.delete('benchmark-edge-pan-effect');data.states.B0=${JSON.stringify(originalState)};normalizedStateObjects.add(data.states.B0);markStateDirty('B0');renderBoardNow(rendered.get('B0'));return true})()`);
|
||||
await client.evaluate("(async()=>{await persistNow({skipCloud:true});return true})()");
|
||||
}
|
||||
}
|
||||
|
|
@ -314,6 +319,7 @@ async function pickupCadenceProbe(client,pathRow,steps=120,sampleDelay=8){
|
|||
const start=await pointFor(client,`.board-card[data-id="B0"] .gate-hit[data-gate="${pathRow.startGate}"]`),
|
||||
center=await pointForCell(client,'B0',pathRow.cells[0][0],pathRow.cells[0][1]),
|
||||
originalState=await client.evaluate("deepClone(metaState('B0'))");
|
||||
await client.evaluate(`(()=>{const p=worldUnitAtClient(${start.x},${start.y}),now=trustedNow();applyRealtimeReaction({id:'benchmark-pickup-effect',emoji:REACTION_EMOJIS[0],style:'firework',x:p[0],y:p[1],createdAt:now,expiresAt:now+${Math.max(12000,steps*sampleDelay*2)}});return true})()`);
|
||||
await client.send('Input.dispatchMouseEvent',{type:'mousePressed',x:start.x,y:start.y,button:'left',buttons:1,clickCount:1});
|
||||
try{
|
||||
await waitFor(()=>client.evaluate("Boolean(rendered.get('B0')?.drawing)"),{timeout:5000,label:'continuous pickup activation'});
|
||||
|
|
@ -327,7 +333,7 @@ async function pickupCadenceProbe(client,pathRow,steps=120,sampleDelay=8){
|
|||
}finally{
|
||||
await client.send('Input.dispatchMouseEvent',{type:'mouseReleased',x:center.x,y:center.y,button:'left',buttons:0,clickCount:1}).catch(()=>{});
|
||||
await sleep(120);
|
||||
await client.evaluate(`(()=>{data.states.B0=${JSON.stringify(originalState)};normalizedStateObjects.add(data.states.B0);markStateDirty('B0');renderBoardNow(rendered.get('B0'));return true})()`);
|
||||
await client.evaluate(`(()=>{realtimeReactions.delete('benchmark-pickup-effect');data.states.B0=${JSON.stringify(originalState)};normalizedStateObjects.add(data.states.B0);markStateDirty('B0');renderBoardNow(rendered.get('B0'));return true})()`);
|
||||
await client.evaluate("(async()=>{await persistNow({skipCloud:true});return true})()");
|
||||
}
|
||||
}
|
||||
|
|
@ -398,12 +404,89 @@ async function measureCursorCadence(client,steps=180){
|
|||
const snapshot=BEND_PERF.snapshot(),commits=globalThis.__benchmarkCursorCommits||[],gaps=commits.slice(1).map((entry,index)=>entry.timestamp-commits[index].timestamp);
|
||||
return{snapshot,diagnostic:{interval:DRAG_FRAME_INTERVAL,tolerance:INTERACTION_FRAME_TOLERANCE_MS,commitCount:commits.length,gaps:gaps.slice(0,20),lastDraw:customCursorLastDraw,inputRevision:customCursorInputRevision,committedRevision:customCursorCommittedRevision}};
|
||||
})()`),snapshot=measured.snapshot,gap=timing(snapshot,'cursorFrameGap'),age=timing(snapshot,'cursorInputAge');
|
||||
assert(gap.count>=30&&gap.p50<=20,`DOM cursor cadence missed acceptance: ${JSON.stringify({gap,diagnostic:measured.diagnostic})}`);
|
||||
assert(age.count>=25&&age.p95<30,`DOM cursor input age missed acceptance: ${JSON.stringify(age)}`);
|
||||
assert(gap.count>=6&&gap.p50<=40,`DOM cursor cadence missed the capped 30 FPS acceptance: ${JSON.stringify({gap,diagnostic:measured.diagnostic})}`);
|
||||
assert(age.count>=8&&age.p95<45,`DOM cursor input age missed acceptance: ${JSON.stringify(age)}`);
|
||||
return snapshot;
|
||||
}finally{await client.evaluate("if(globalThis.__benchmarkCommitCustomCursorFrame)commitCustomCursorFrame=globalThis.__benchmarkCommitCustomCursorFrame;delete globalThis.__benchmarkCommitCustomCursorFrame;delete globalThis.__benchmarkCursorCommits;localStorage.removeItem('bend-field-cursor-renderer');syncCursorAppearance('default');true")}
|
||||
}
|
||||
|
||||
async function measureEffectsAndCosmetics(client,{cpuRate=1,visual=true,inventory=true,aurora=true,memory=true,singles=true}={}){
|
||||
await client.send('Emulation.setCPUThrottlingRate',{rate:cpuRate});
|
||||
return client.evaluate(`(async()=>{
|
||||
const options=${JSON.stringify({cpuRate,visual,inventory,aurora,memory,singles,visualCases:effectVisualFixture.cases,visualWidth:effectVisualFixture.width,visualHeight:effectVisualFixture.height,perChannelTolerance:effectVisualFixture.perChannelTolerance,maximumChangedPixelRatio:effectVisualFixture.maximumChangedPixelRatio})},wait=milliseconds=>new Promise(resolve=>setTimeout(resolve,milliseconds)),
|
||||
styles=['classic','giant','laser','orbit','firework','comet'],emoji=REACTION_EMOJIS[0],
|
||||
runSet=async(runStyles,label,duration=900)=>{
|
||||
for(const style of new Set(runStyles))BEND_PERF.warmEffectCache(emoji,style);await new Promise(resolve=>requestAnimationFrame(resolve));await wait(80);
|
||||
BEND_PERF.reset();const rect=document.querySelector('#viewport').getBoundingClientRect(),center=worldUnitAtClient(rect.left+rect.width/2,rect.top+rect.height/2),now=trustedNow();
|
||||
runStyles.forEach((style,index)=>{const effectDuration=reactionDurationForStyle(style),angle=index*Math.PI*2/Math.max(1,runStyles.length),radius=runStyles.length>1?1.35:0;applyRealtimeReaction({id:'effect-benchmark-'+label+'-'+index,emoji,style,x:center[0]+Math.cos(angle)*radius,y:center[1]+Math.sin(angle)*radius,createdAt:now-effectDuration*.22,expiresAt:now+effectDuration*.78})});
|
||||
await wait(duration);const snapshot=BEND_PERF.snapshot();realtimeReactions.clear();reactionDirty=true;scheduleReactionRender(true);await wait(90);return snapshot;
|
||||
},
|
||||
loadPixels=dataUrl=>new Promise((resolve,reject)=>{const image=new Image();image.onload=()=>{const canvas=document.createElement('canvas');canvas.width=image.naturalWidth;canvas.height=image.naturalHeight;const context=canvas.getContext('2d',{willReadFrequently:true});context.drawImage(image,0,0);resolve({width:canvas.width,height:canvas.height,data:context.getImageData(0,0,canvas.width,canvas.height).data})};image.onerror=reject;image.src=dataUrl}),
|
||||
compareSamples=async(directUrl,cachedUrl)=>{const[a,b]=await Promise.all([loadPixels(directUrl),loadPixels(cachedUrl)]);let changed=0,active=0,alphaDelta=0,minX=a.width,minY=a.height,maxX=-1,maxY=-1;for(let offset=0;offset<a.data.length;offset+=4){const pixel=offset/4,x=pixel%a.width,y=Math.floor(pixel/a.width),visible=a.data[offset+3]>0||b.data[offset+3]>0;if(visible){active++;minX=Math.min(minX,x);minY=Math.min(minY,y);maxX=Math.max(maxX,x);maxY=Math.max(maxY,y)}if(Math.abs(a.data[offset]-b.data[offset])>options.perChannelTolerance||Math.abs(a.data[offset+1]-b.data[offset+1])>options.perChannelTolerance||Math.abs(a.data[offset+2]-b.data[offset+2])>options.perChannelTolerance||Math.abs(a.data[offset+3]-b.data[offset+3])>options.perChannelTolerance)changed++;alphaDelta+=Math.abs(a.data[offset+3]-b.data[offset+3])}return{changed,total:a.width*a.height,ratio:changed/(a.width*a.height),active,alphaDelta,bounds:[minX,minY,maxX,maxY]}},
|
||||
result={cpuRate:options.cpuRate,singles:{},visual:[],visualGate:{perChannelTolerance:options.perChannelTolerance,maximumChangedPixelRatio:options.maximumChangedPixelRatio,width:options.visualWidth,height:options.visualHeight},inventory:null,aurora:null,memory:null};
|
||||
BEND_PERF.clearEffectCaches();
|
||||
if(options.singles)for(const style of styles)result.singles[style]=await runSet([style],style,style==='classic'?620:1200);
|
||||
result.overlap4=await runSet(['giant','laser','orbit','firework'],'overlap4',1100);
|
||||
result.overlap8=await runSet(['giant','laser','orbit','firework','comet','laser','orbit','firework'],'overlap8',1100);
|
||||
if(options.cpuRate===1){
|
||||
const board=rendered.get('B0'),setup={};BEND_PERF.reset();for(let index=0;index<40;index++){const style=styles[index%styles.length],duration=reactionDurationForStyle(style),id='effect-publish-'+index,now=trustedNow();applyRealtimeReaction({id,emoji,style,x:0,y:0,createdAt:now,expiresAt:now+duration});realtimeReactions.delete(id)}cancelReactionRenderScheduler(true);setup.reaction=BEND_PERF.snapshot();
|
||||
if(board){cleanupGemEffects();BEND_PERF.reset();for(let index=0;index<30;index++){playGemCollectionAnimation(board,100000);cleanupGemEffects()}setup.gem=BEND_PERF.snapshot();skipCompletionVisuals();BEND_PERF.reset();for(let index=0;index<30;index++){completionEffect(board,1000);finishCompletionVisual(board.id,false)}setup.completion=BEND_PERF.snapshot()}
|
||||
result.setup=setup;
|
||||
}
|
||||
if(options.visual){
|
||||
for(const{style,life}of options.visualCases){
|
||||
const direct=BEND_PERF.renderReactionSample({style,emoji,life,width:options.visualWidth,height:options.visualHeight,glyphCache:false,pathCache:false}),cached=BEND_PERF.renderReactionSample({style,emoji,life,width:options.visualWidth,height:options.visualHeight,glyphCache:true,pathCache:true});
|
||||
result.visual.push({style,life,...await compareSamples(direct.dataUrl,cached.dataUrl)});
|
||||
}
|
||||
}
|
||||
if(options.inventory){
|
||||
const previousCursor=data.cursorStyle,previousLineColor=data.lineColorStyle,previousInventoryEntries=inventoryEntries,syntheticEntries=STORE_ITEMS.map((item,index)=>({meta:null,st:null,store:null,purchase:{id:item.id,itemId:item.id,boughtAt:index+1,paidCost:0},personal:true}));inventoryEntries=itemId=>itemId?syntheticEntries.filter(entry=>entry.purchase.id===itemId):[...syntheticEntries];invalidateEconomyCaches();await wait(100);BEND_PERF.reset();const resourcesBefore=performance.getEntriesByType('resource').filter(entry=>entry.name.includes('/assets/flags/')).length;
|
||||
openInventory();await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));const renderSnapshot=BEND_PERF.snapshot();inventoryPanel.scrollTop=Math.min(480,Math.max(0,inventoryPanel.scrollHeight-inventoryPanel.clientHeight));const scrollBefore=inventoryPanel.scrollTop,auroraItem=STORE_ITEMS.find(item=>item.aurora===true),focusTarget=auroraItem?inventoryItemViews.get(auroraItem.id)?.use:null,nodesBefore=inventoryList.querySelectorAll('*').length;focusTarget?.focus({preventScroll:true});BEND_PERF.reset();
|
||||
for(let index=0;index<40;index++){data.lineColorStyle=index%2?previousLineColor:auroraItem.id;patchInventoryItems([auroraItem.id,previousLineColor])}await new Promise(resolve=>requestAnimationFrame(resolve));
|
||||
const patchSnapshot=BEND_PERF.snapshot(),resourcesAfter=performance.getEntriesByType('resource').filter(entry=>entry.name.includes('/assets/flags/')).length;result.inventory={renderSnapshot,patchSnapshot,scrollBefore,scrollAfter:inventoryPanel.scrollTop,focusRetained:!focusTarget||document.activeElement===focusTarget,resourceDelta:resourcesAfter-resourcesBefore,mounted:inventoryList.querySelectorAll('[data-item-id]').length,nodesBefore,nodesAfter:inventoryList.querySelectorAll('*').length};
|
||||
closeInventory(false);inventoryEntries=previousInventoryEntries;invalidateEconomyCaches();data.cursorStyle=previousCursor;data.lineColorStyle=previousLineColor;syncCursorAppearance(previousCursor);syncCosmeticAppearance();renderInventoryPanel();
|
||||
}
|
||||
if(options.aurora){
|
||||
const svg=document.createElementNS('http://www.w3.org/2000/svg','svg'),fragment=document.createDocumentFragment();svg.setAttribute('aria-hidden','true');svg.style.cssText='position:absolute;width:1px;height:1px;overflow:visible;pointer-events:none';
|
||||
for(let index=0;index<500;index++){const path=document.createElementNS('http://www.w3.org/2000/svg','path');path.setAttribute('class','path line-effect-aurora');path.setAttribute('d',\`M0 \${index%25} L100 \${index%25}\`);fragment.append(path)}svg.append(fragment);world.append(svg);const previousCount=auroraVisiblePathCount;auroraVisiblePathCount+=500;BEND_PERF.reset();updateAuroraAnimationState();await wait(4300);const active=BEND_PERF.snapshot();auroraVisiblePathCount=0;updateAuroraAnimationState();BEND_PERF.reset();await wait(2300);const inactive=BEND_PERF.snapshot();
|
||||
Object.defineProperty(document,'visibilityState',{value:'hidden',configurable:true});auroraVisiblePathCount=500;document.dispatchEvent(new Event('visibilitychange'));BEND_PERF.reset();await wait(2300);const hidden=BEND_PERF.snapshot();delete document.visibilityState;auroraVisiblePathCount=previousCount;svg.remove();document.dispatchEvent(new Event('visibilitychange'));updateAuroraAnimationState();result.aurora={active,inactive,hidden};
|
||||
}
|
||||
if(options.memory&&globalThis.gc&&performance.memory){
|
||||
const board=rendered.get('B0');BEND_PERF.clearEffectCaches();cleanupGemEffects();skipCompletionVisuals();globalThis.gc();await wait(80);const before=performance.memory.usedJSHeapSize;
|
||||
for(let index=0;index<100;index++){const style=styles[index%styles.length],duration=reactionDurationForStyle(style),id='effect-memory-'+index,now=trustedNow();applyRealtimeReaction({id,emoji,style,x:0,y:0,createdAt:now-duration*.5,expiresAt:now+duration*.5});reactionDirty=true;drawReactionLayer();realtimeReactions.delete(id);cancelReactionRenderScheduler(true);if(board){playGemCollectionAnimation(board,100000);cleanupGemEffects();completionEffect(board,1000);finishCompletionVisual(board.id,false)}}
|
||||
reactionDirty=true;drawReactionLayer();cancelReactionRenderScheduler(true);cleanupGemEffects();skipCompletionVisuals();BEND_PERF.clearEffectCaches();globalThis.gc();await wait(120);globalThis.gc();result.memory={before,after:performance.memory.usedJSHeapSize,delta:performance.memory.usedJSHeapSize-before,active:realtimeReactions.size,gemBatches:activeGemBatches.size,completionVisuals:activeCompletionVisuals.size,effectNodes:document.querySelectorAll('.gem-particle,.completion-flash,.completion-burst').length,cacheEntries:(BEND_PERF.snapshot().gauges.reactionGlyphCacheEntries||0)+(BEND_PERF.snapshot().gauges.reactionStaticPathCacheEntries||0)};
|
||||
}
|
||||
return result;
|
||||
})()`);
|
||||
}
|
||||
|
||||
function validateEffectsAndCosmetics(result,{mobile=false}={}){
|
||||
const cpuRate=result.cpuRate,normalSpeed=cpuRate===1,singleLimit=normalSpeed?6:40,fourLimit=normalSpeed?12:80,longTaskLimit=normalSpeed?50:500,minFrames=normalSpeed?8:5,minGaps=normalSpeed?7:4;
|
||||
for(const[style,snapshot]of Object.entries(result.singles||{})){
|
||||
const draw=timing(snapshot,`reactionStyle.${style}`),gap=timing(snapshot,'reactionFrameGap');
|
||||
assert(draw.count>=minFrames&&(!normalSpeed||draw.p95<=singleLimit&&draw.p99<=8),`${mobile?'mobile ':''}${style}/${cpuRate}x reaction work missed acceptance: ${JSON.stringify({draw,singleLimit,minFrames})}`);
|
||||
if(normalSpeed)assert(gap.count>=minGaps&&gap.p50<=45,`${style}/${cpuRate}x single-effect median cadence missed acceptance: ${JSON.stringify({gap,draw:timing(snapshot,'reactionFrame'),rates:snapshot.rates,counters:snapshot.counters})}`);
|
||||
assert(timing(snapshot,'reactionFrame').max<longTaskLimit,`${style}/${cpuRate}x reaction callback exceeded its stress ceiling: ${JSON.stringify(timing(snapshot,'reactionFrame'))}`);
|
||||
}
|
||||
const four=timing(result.overlap4,'reactionFrame'),eight=timing(result.overlap8,'reactionFrame'),fourGap=timing(result.overlap4,'reactionFrameGap');
|
||||
assert(four.count>=minFrames&&(!normalSpeed||four.p95<=fourLimit&&four.p99<=20)&&four.max<longTaskLimit,`${mobile?'mobile ':''}four-effect/${cpuRate}x reaction missed its work budget: ${JSON.stringify({four,fourGap,fourLimit,minFrames,rates:result.overlap4.rates,counters:result.overlap4.counters})}`);
|
||||
assert(eight.count>=(normalSpeed?6:4)&&eight.max<longTaskLimit&&(result.overlap8.gauges.peakVisibleReactions||0)>=8,`Eight-effect overload did not render every valid reaction: ${JSON.stringify({eight,peakVisible:result.overlap8.gauges.peakVisibleReactions,visible:result.overlap8.gauges.visibleReactions})}`);
|
||||
for(const snapshot of[result.overlap4,result.overlap8]){
|
||||
assert((snapshot.rates.reactionDeadlineTimerCallbacksPerSecond||0)<=37.5,`Reaction deadline timer exceeded the 37.5 callbacks/s short-window envelope: ${snapshot.rates.reactionDeadlineTimerCallbacksPerSecond}`);
|
||||
assert((snapshot.rates.reactionDrawRafCallbacksPerSecond||0)<=37.5,`Reaction draw RAF exceeded the 37.5 callbacks/s short-window envelope: ${snapshot.rates.reactionDrawRafCallbacksPerSecond}`);
|
||||
}
|
||||
if(result.visual?.length){const worst=result.visual.reduce((a,b)=>a.ratio>b.ratio?a:b),limit=result.visualGate?.maximumChangedPixelRatio??.005;assert(worst.ratio<=limit,`Cached effect pixels changed beyond tolerance: ${JSON.stringify({worst,gate:result.visualGate})}`)}
|
||||
if(result.setup){assert(timing(result.setup.reaction,'reactionPublish').p95<=2,`Reaction publication exceeded 2 ms: ${JSON.stringify(timing(result.setup.reaction,'reactionPublish'))}`);assert(timing(result.setup.gem,'gemEffectSetup').p95<=3,`Gem setup exceeded 3 ms: ${JSON.stringify(timing(result.setup.gem,'gemEffectSetup'))}`);assert(timing(result.setup.completion,'completionEffectSetup').p95<=3,`Completion setup exceeded 3 ms: ${JSON.stringify(timing(result.setup.completion,'completionEffectSetup'))}`)}
|
||||
if(result.inventory){
|
||||
const render=timing(result.inventory.renderSnapshot,'inventoryRender'),initialPatch=timing(result.inventory.renderSnapshot,'inventoryPatch'),initial=render.count?render:initialPatch,patch=timing(result.inventory.patchSnapshot,'inventoryPatch');
|
||||
assert(initial.count&&initial.p95<=100,`Full cosmetic inventory render exceeded 100 ms: ${JSON.stringify({render,initialPatch})}`);assert(patch.count>=20&&patch.p95<=8,`Cosmetic equip patch exceeded 8 ms: ${JSON.stringify({patch})}`);
|
||||
assert(result.inventory.scrollAfter===result.inventory.scrollBefore,`Cosmetic equip moved inventory scroll from ${result.inventory.scrollBefore} to ${result.inventory.scrollAfter}`);assert(result.inventory.focusRetained,'Cosmetic equip moved keyboard focus');assert(result.inventory.nodesAfter===result.inventory.nodesBefore,'Cosmetic equip rebuilt inventory nodes');
|
||||
assert((result.inventory.renderSnapshot.counters.longTasks||0)===0&&(result.inventory.patchSnapshot.counters.longTasks||0)===0,`Full cosmetic inventory produced a long task: ${JSON.stringify({render,patch,mounted:result.inventory.mounted})}`);
|
||||
}
|
||||
if(result.aurora){const active=timing(result.aurora.active,'auroraTick');assert(active.p95<=.25,`Aurora tick exceeded 0.25 ms: ${JSON.stringify(active)}`);const writes=result.aurora.active.counters.auroraColorWrites||0;assert(writes>=2&&writes<=3,`Aurora did not select one curated color every two seconds: ${writes}`);for(const[name,snapshot]of Object.entries({inactive:result.aurora.inactive,hidden:result.aurora.hidden}))assert(!(snapshot.counters.auroraColorWrites||0)&&!timing(snapshot,'auroraTick').count,`Aurora performed work while ${name}: ${JSON.stringify(snapshot)}`)}
|
||||
if(result.memory)assert(result.memory.active===0&&result.memory.gemBatches===0&&result.memory.completionVisuals===0&&result.memory.effectNodes===0&&result.memory.cacheEntries===0&&result.memory.delta<=2*1024*1024,`Effect cleanup retained too much state: ${JSON.stringify(result.memory)}`);
|
||||
}
|
||||
|
||||
async function zoom(client,deltaY,repetitions){
|
||||
const center=await viewportCenter(client);
|
||||
for(let index=0;index<repetitions;index++){
|
||||
|
|
@ -413,7 +496,7 @@ async function zoom(client,deltaY,repetitions){
|
|||
await sleep(300);
|
||||
}
|
||||
|
||||
function timing(snapshot,name){return snapshot.timings?.[name]||{count:0,p50:0,p95:0,max:0}}
|
||||
function timing(snapshot,name){return snapshot.timings?.[name]||{count:0,p50:0,p95:0,p99:0,max:0}}
|
||||
async function measureGameplaySimplificationBudgets(client){
|
||||
const result=await client.evaluate(`(async()=>{
|
||||
const percentile=(rows,p)=>{const ordered=[...rows].sort((a,b)=>a-b);return ordered[Math.min(ordered.length-1,Math.floor(ordered.length*p))]||0};
|
||||
|
|
@ -470,14 +553,14 @@ function validateMeasurement(result){
|
|||
dragAge=timing(cadenceSnapshot,'pickupVisualInputAge'),cameraAge=timing(snapshot,'cameraInputAge'),
|
||||
minimap=timing(snapshot,'drawMinimap'),ensure=timing(snapshot,'ensureBoards'),save=timing(snapshot,'persistDirtyToDb'),
|
||||
overview=timing(snapshot,'drawWorldOverview'),mirrorChunk=timing(snapshot,'mirrorChunkWrite'),
|
||||
dragLimit=cpuRate===1?8:16,functionalDragLimit=cpuRate===1?16:24,minimapLimit=cpuRate===1?20:33,lodLimit=cpuRate===1?40:100;
|
||||
dragLimit=cpuRate===1?8:16,functionalDragLimit=cpuRate===1?18:24,minimapLimit=cpuRate===1?20:33,lodLimit=cpuRate===1?40:100;
|
||||
assert(drag.count+probeDrag.count>=5,`${profile}/${cpuRate}x captured only ${drag.count} functional and ${probeDrag.count} continuous-input drag frames: ${JSON.stringify(result.pickupProbe)}`);
|
||||
assert(camera.count>=5,`${profile}/${cpuRate}x did not capture frame-coalesced camera work`);
|
||||
assert(minimap.count>=2,`${profile}/${cpuRate}x did not capture minimap work`);
|
||||
assert(ensure.count>=2,`${profile}/${cpuRate}x did not capture LOD work`);
|
||||
assert(save.count>=1,`${profile}/${cpuRate}x did not capture an autosave`);
|
||||
assert(overview.count>=1,`${profile}/${cpuRate}x did not capture overview rendering`);
|
||||
assert(cameraGap.count>=5&&(cpuRate!==1||dragGap.count>=4),`${profile}/${cpuRate}x did not capture enough real interaction cadence samples (pickup ${dragGap.count}, camera ${cameraGap.count})`);
|
||||
assert(cameraGap.count>=4&&(cpuRate!==1||dragGap.count>=4),`${profile}/${cpuRate}x did not capture enough real interaction cadence samples (pickup ${dragGap.count}, camera ${cameraGap.count})`);
|
||||
if(cpuRate===1){
|
||||
const approved=result.claimApproved,denied=result.claimDenied;
|
||||
assert(approved?.previewWithinFrame&&approved.tracksLatest&&approved.modelUntouched&&approved.approved&&approved.noJump&&approved.fullBoardRenders===0,`${profile} claim approval preview/commit failed: ${JSON.stringify(approved)}`);
|
||||
|
|
@ -490,10 +573,10 @@ function validateMeasurement(result){
|
|||
assert((snapshot.counters.overviewBuildsDuringInteraction||0)>=1&&(snapshot.counters.overviewBuildsDuringInteraction||0)<=60,`${profile}/${cpuRate}x long overview pan did not use a bounded in-gesture cache refresh`);
|
||||
if(cpuRate===1){
|
||||
const cadenceHot=Object.entries(cadenceSnapshot.timings||{}).filter(([,value])=>value.max>1).sort((a,b)=>b[1].max-a[1].max).slice(0,12);
|
||||
assert(dragGap.p50<=18&&dragGap.p95<=28,`${profile} pickup visual median/p95 gap ${dragGap.p50.toFixed(2)}/${dragGap.p95.toFixed(2)} ms exceeded the capped 60 Hz cadence budget; active timings ${JSON.stringify(cadenceHot)}`);
|
||||
assert(cameraGap.p50<=20,`${profile} camera median gap ${cameraGap.p50.toFixed(2)} ms exceeded 20 ms`);
|
||||
assert(dragAge.p95<30,`${profile} pickup input age ${dragAge.p95.toFixed(2)} ms exceeded 30 ms`);
|
||||
assert(cameraAge.p95<25,`${profile} camera input age ${cameraAge.p95.toFixed(2)} ms exceeded 25 ms`);
|
||||
assert(dragGap.p50<=40&&dragGap.p95<=50,`${profile} pickup visual median/p95 gap ${dragGap.p50.toFixed(2)}/${dragGap.p95.toFixed(2)} ms exceeded the capped 30 FPS cadence budget; active timings ${JSON.stringify(cadenceHot)}`);
|
||||
assert(cameraGap.p50<=40,`${profile} camera median gap ${cameraGap.p50.toFixed(2)} ms exceeded the capped 30 FPS cadence budget`);
|
||||
assert(dragAge.p95<45,`${profile} pickup input age ${dragAge.p95.toFixed(2)} ms exceeded 45 ms`);
|
||||
assert(cameraAge.p95<45,`${profile} camera input age ${cameraAge.p95.toFixed(2)} ms exceeded 45 ms`);
|
||||
}
|
||||
assert(drag.p95<=functionalDragLimit&&probeDrag.p95<=dragLimit,`${profile}/${cpuRate}x drag work exceeded acceptance (functional ${drag.p95.toFixed(2)}/${functionalDragLimit} ms, cadence ${probeDrag.p95.toFixed(2)}/${dragLimit} ms, model ${timing(cadenceSnapshot,'pickupModelWork').p95.toFixed(2)}, visual ${timing(cadenceSnapshot,'pickupVisualWork').p95.toFixed(2)}, render ${timing(cadenceSnapshot,'renderDragFrame').p95.toFixed(2)})`);
|
||||
assert(camera.p95<=dragLimit,`${profile}/${cpuRate}x camera p95 ${camera.p95.toFixed(2)} ms exceeded acceptance`);
|
||||
|
|
@ -506,9 +589,8 @@ function validateMeasurement(result){
|
|||
}
|
||||
for(const name of ['minimapDrawsDuringInteraction','lodPassesDuringInteraction','persistenceDuringInteraction','worldRefreshesDuringInteraction'])
|
||||
assert((snapshot.counters[name]||0)===0&&(cadenceSnapshot.counters[name]||0)===0,`${profile}/${cpuRate}x ran ${name} during an active gesture`);
|
||||
assert((snapshot.counters.worldRefreshesDeferredDuringInteraction||0)>=1,`${profile}/${cpuRate}x did not defer the injected cross-tab refresh until gesture settlement`);
|
||||
assert(snapshot.gauges.renderedBoards>=snapshot.gauges.visibleUnsolvedBoards,`${profile}/${cpuRate}x omitted a visible unsolved board from detailed rendering`);
|
||||
assert((cadenceSnapshot.rates.dragFramesPerSecond||0)<=65,`${profile}/${cpuRate}x pickup presentation exceeded the 60 FPS ceiling (${(cadenceSnapshot.rates.dragFramesPerSecond||0).toFixed(1)} FPS)`);
|
||||
assert((cadenceSnapshot.rates.dragFramesPerSecond||0)<=35,`${profile}/${cpuRate}x pickup presentation exceeded the 30 FPS ceiling (${(cadenceSnapshot.rates.dragFramesPerSecond||0).toFixed(1)} FPS)`);
|
||||
assert(snapshot.gauges.domNodes<18000,`${profile}/${cpuRate}x DOM size is not viewport-bounded`);
|
||||
}
|
||||
|
||||
|
|
@ -545,6 +627,7 @@ async function measureScenario(client,starter,profile,cpuRate){
|
|||
leftProbe=await client.evaluate(`(()=>{const target=document.elementFromPoint(${leftStart.x},${leftStart.y});return{tag:target?.tagName||null,classes:target?.getAttribute?.('class')||null,board:target?.closest?.('.board-card')?.dataset?.id||null,allowed:leftFieldPanAllowed({button:0,target})}})()`);
|
||||
const beforeLeftPan=await client.evaluate('({x:cam.x,y:cam.y,solved:metaState("B0").solved})');
|
||||
assert(beforeLeftPan.solved&&leftProbe.allowed,`${profile.name}/${cpuRate}x solved-board left drag is not eligible for panning: ${JSON.stringify({leftStart,leftProbe,beforeLeftPan})}`);
|
||||
await client.evaluate("(()=>{const r=document.querySelector('#viewport').getBoundingClientRect(),p=worldUnitAtClient(r.left+r.width/2,r.top+r.height/2),now=trustedNow();applyRealtimeReaction({id:'benchmark-overview-effect',emoji:REACTION_EMOJIS[0],style:'orbit',x:p[0],y:p[1],createdAt:now,expiresAt:now+20000});return true})()");
|
||||
await zoom(client,180,15);
|
||||
const overviewPathsObserved=await client.evaluate('drawWorldOverview();BEND_PERF.snapshot().gauges.overviewPaths||0');
|
||||
const overviewBuildBaseline=await client.evaluate('BEND_PERF.snapshot().counters.overviewCacheBuilds||0');
|
||||
|
|
@ -554,7 +637,7 @@ async function measureScenario(client,starter,profile,cpuRate){
|
|||
await waitFor(()=>client.evaluate(`(()=>{if(!inWorldOverview()||!overviewCache)return false;const[centerX,centerY]=cameraCenterInChunks();return!overviewDirty&&Math.abs(centerX-overviewCache.anchorX)*overviewCache.unit<=overviewCache.overscan*.82&&Math.abs(centerY-overviewCache.anchorY)*overviewCache.unit<=overviewCache.overscan*.82})()`),{timeout:10000,label:'settled overview cache rebuild'});
|
||||
const pinch=await pinchZoomProbe(client);
|
||||
await sleep(80);
|
||||
const panLongTasks=(await client.evaluate('BEND_PERF.snapshot().counters.interactionLongTasks||0'))-panLongTaskBaseline;
|
||||
const panLongTasks=(await client.evaluate('BEND_PERF.snapshot().counters.interactionLongTasks||0'))-panLongTaskBaseline;await client.evaluate("realtimeReactions.delete('benchmark-overview-effect');true");
|
||||
await zoom(client,-180,15);
|
||||
await sleep(650);
|
||||
try{await waitFor(()=>client.evaluate("!hasPendingPersistence()"),{timeout:20000,label:'durable persistence drain'})}
|
||||
|
|
@ -564,9 +647,26 @@ async function measureScenario(client,starter,profile,cpuRate){
|
|||
}
|
||||
await sleep(500);
|
||||
const snapshot=await client.evaluate('BEND_PERF.snapshot()');
|
||||
const result={profile:profile.name,boards:profile.boards,cpuRate,snapshot,pickupCadence,pickupProbe,overviewPathsObserved,overviewBuildBaseline,pickupProbeLongTasks,pickupLongTasks,panLongTasks,claimApproved,claimDenied,edgePan,pinch};
|
||||
validateMeasurement(result);
|
||||
return result;
|
||||
return{profile:profile.name,boards:profile.boards,cpuRate,snapshot,pickupCadence,pickupProbe,overviewPathsObserved,overviewBuildBaseline,pickupProbeLongTasks,pickupLongTasks,panLongTasks,claimApproved,claimDenied,edgePan,pinch};
|
||||
}
|
||||
function profileSummaryRow(result){
|
||||
return{
|
||||
profile:result.profile,boards:result.boards,cpuRate:result.cpuRate,
|
||||
dragP95:timing(result.snapshot,'processBoardDragFrame').p95,
|
||||
cameraP95:timing(result.snapshot,'commitCameraInteraction').p95,
|
||||
dragGapP95:timing(result.pickupCadence||result.snapshot,'pickupVisualFrameGap').p95,
|
||||
dragInputAgeP95:timing(result.pickupCadence||result.snapshot,'pickupVisualInputAge').p95,
|
||||
cameraGapP50:timing(result.snapshot,'cameraFrameGap').p50,
|
||||
cameraInputAgeP95:timing(result.snapshot,'cameraInputAge').p95,
|
||||
minimapP95:timing(result.snapshot,'drawMinimap').p95,
|
||||
lodP95:timing(result.snapshot,'ensureBoards').p95,
|
||||
saveP95:timing(result.snapshot,'persistDirtyToDb').p95,
|
||||
overviewP95:timing(result.snapshot,'drawWorldOverview').p95,
|
||||
renderedBoards:result.snapshot.gauges.renderedBoards,
|
||||
staticBoards:result.snapshot.gauges.staticBoards,
|
||||
domNodes:result.snapshot.gauges.domNodes,
|
||||
longTasks:result.snapshot.counters.longTasks||0
|
||||
};
|
||||
}
|
||||
|
||||
async function main(runProfiles=profiles,cpuRates=[1,4]){
|
||||
|
|
@ -577,6 +677,7 @@ async function main(runProfiles=profiles,cpuRates=[1,4]){
|
|||
`--remote-debugging-port=${debuggingPort}`,'--remote-allow-origins=*',`--user-data-dir=${profilePath}`,
|
||||
`--host-resolver-rules=MAP ${benchmarkHost} 127.0.0.1`,'--window-size=1440,1000','about:blank'
|
||||
],{stdio:'ignore',windowsHide:true});
|
||||
const report={capturedAt:new Date().toISOString(),browserPath:edgePath,benchmarkHost,status:'running',effects:{},profiles:[]};
|
||||
let client=null;
|
||||
try{
|
||||
const target=await endpoint();client=new CdpClient(target.webSocketDebuggerUrl);await client.connect();
|
||||
|
|
@ -585,45 +686,41 @@ async function main(runProfiles=profiles,cpuRates=[1,4]){
|
|||
if(startupOnly){
|
||||
await sleep(12000);
|
||||
const state=await client.evaluate("({ready:document.body?.dataset?.ready||null,version:document.querySelector('.brand small')?.textContent||null,boards:typeof data!=='undefined'?Object.keys(data.metas).length:null,origin:typeof data!=='undefined'&&Boolean(data.metas?.B0?.puzzle),worldGeneration:typeof data!=='undefined'?data.worldGeneration:null,turnFont:getComputedStyle(document.querySelector('.board-svg text')||document.body).fontFamily,status:document.querySelector('#statusMessage')?.textContent||'',text:document.body?.innerText?.slice(0,300)||''})");
|
||||
assert(state.ready==='true'&&state.version==='v47.83'&&state.boards===1&&state.origin&&state.worldGeneration==='v47-field-reset-20260728-interaction-fix',`Real-browser startup state is incomplete: ${JSON.stringify(state)}`);
|
||||
assert(state.ready==='true'&&state.version==='v47.87'&&state.boards===1&&state.origin&&state.worldGeneration==='linkfield-single-world-20260801',`Real-browser startup state is incomplete: ${JSON.stringify(state)}`);
|
||||
assert(/DotGothic16|Press Start 2P|MS Gothic|monospace/i.test(state.turnFont),'Dot-styled game font is not active in the browser');
|
||||
console.log(`Real-browser startup passed: ${JSON.stringify(state)}`);return;
|
||||
}
|
||||
await ready(client);await sleep(1000);
|
||||
const displayCadence=await measureDisplayCadence(client);
|
||||
const displayCadence=await measureDisplayCadence(client);report.displayCadence=displayCadence;
|
||||
assert(displayCadence.count>=60&&displayCadence.p50<=20,`Headless display baseline is not 60 Hz: ${JSON.stringify(displayCadence)}`);
|
||||
const gameplayBudgets=await measureGameplaySimplificationBudgets(client);
|
||||
const cursorModes=await measureCursorModes(client);
|
||||
const gameplayBudgets=await measureGameplaySimplificationBudgets(client);report.gameplayBudgets=gameplayBudgets;
|
||||
const cursorModes=await measureCursorModes(client);report.cursorModes=cursorModes;
|
||||
assert(cursorModes.defaultMode==='default'&&cursorModes.emojiMode==='dom'&&cursorModes.flagMode==='dom'&&cursorModes.domMode==='dom'&&cursorModes.visibleBeforeDrag&&!cursorModes.visibleDuringDrag&&!cursorModes.movedDuringDrag,`Cursor mode runtime probe failed: ${JSON.stringify(cursorModes)}`);
|
||||
const cursorCadence=await measureCursorCadence(client);
|
||||
const cursorCadence=await measureCursorCadence(client);report.cursorCadence=cursorCadence;
|
||||
console.log(`Display cadence | median ${displayCadence.p50.toFixed(2)} ms | p95 ${displayCadence.p95.toFixed(2)} ms`);
|
||||
console.log(`Gameplay budgets | snap ${gameplayBudgets.snap.p95.toFixed(3)} ms | pointer samples ${gameplayBudgets.pointerSamples.p95.toFixed(3)} ms | minimap ${gameplayBudgets.minimap.p95.toFixed(3)} ms | noise max ${gameplayBudgets.noise.max.toFixed(3)} ms | worker ${gameplayBudgets.workerElapsed.toFixed(1)} ms`);
|
||||
console.log(`Cursor cadence | median gap ${timing(cursorCadence,'cursorFrameGap').p50.toFixed(2)} ms | input p95 ${timing(cursorCadence,'cursorInputAge').p95.toFixed(2)} ms`);
|
||||
const effects1x=await measureEffectsAndCosmetics(client,{cpuRate:1,visual:true,inventory:true,aurora:true,memory:true,singles:true});report.effects.desktop1x=effects1x;validateEffectsAndCosmetics(effects1x);
|
||||
const effects4x=await measureEffectsAndCosmetics(client,{cpuRate:4,visual:false,inventory:false,aurora:false,memory:false,singles:true});report.effects.desktop4x=effects4x;validateEffectsAndCosmetics(effects4x);
|
||||
await client.send('Emulation.setDeviceMetricsOverride',{width:390,height:844,deviceScaleFactor:1,mobile:true,screenWidth:390,screenHeight:844});await sleep(220);
|
||||
const effectsMobile=await measureEffectsAndCosmetics(client,{cpuRate:1,visual:false,inventory:false,aurora:false,memory:false,singles:false});report.effects.mobile=effectsMobile;validateEffectsAndCosmetics(effectsMobile,{mobile:true});
|
||||
await client.send('Emulation.clearDeviceMetricsOverride');await client.send('Emulation.setCPUThrottlingRate',{rate:1});await sleep(220);
|
||||
console.log(`Effects | single firework ${timing(effects1x.singles.firework,'reactionStyle.firework').p95.toFixed(2)} ms | overlap4 ${timing(effects1x.overlap4,'reactionFrame').p95.toFixed(2)} ms | overlap8 ${timing(effects1x.overlap8,'reactionFrame').p95.toFixed(2)} ms | visual max ${(Math.max(...effects1x.visual.map(row=>row.ratio))*100).toFixed(3)}%`);
|
||||
console.log(`EFFECT_BENCHMARK_JSON=${JSON.stringify({desktop1x:{single:Object.fromEntries(Object.entries(effects1x.singles).map(([style,snapshot])=>[style,timing(snapshot,`reactionStyle.${style}`)])),overlap4:timing(effects1x.overlap4,'reactionFrame'),overlap8:timing(effects1x.overlap8,'reactionFrame'),inventory:effects1x.inventory,memory:effects1x.memory,aurora:timing(effects1x.aurora.active,'auroraTick'),setup:effects1x.setup},desktop4x:{overlap4:timing(effects4x.overlap4,'reactionFrame'),overlap8:timing(effects4x.overlap8,'reactionFrame')},mobile:{overlap4:timing(effectsMobile.overlap4,'reactionFrame'),overlap8:timing(effectsMobile.overlap8,'reactionFrame')}})}`);
|
||||
const starter=await readStarterRows(client),results=[];
|
||||
for(const profile of runProfiles)for(const cpuRate of cpuRates){
|
||||
const result=await measureScenario(client,starter,profile,cpuRate);results.push(result);
|
||||
const result=await measureScenario(client,starter,profile,cpuRate);results.push(result);report.profiles=results.map(profileSummaryRow);validateMeasurement(result);
|
||||
const drag=timing(result.snapshot,'processBoardDragFrame'),camera=timing(result.snapshot,'commitCameraInteraction'),minimap=timing(result.snapshot,'drawMinimap'),
|
||||
ensure=timing(result.snapshot,'ensureBoards'),save=timing(result.snapshot,'persistDirtyToDb');
|
||||
console.log(`${profile.name.padEnd(6)} ${cpuRate}x CPU | drag p95 ${drag.p95.toFixed(2)} ms | camera ${camera.p95.toFixed(2)} ms | minimap ${minimap.p95.toFixed(2)} ms | LOD ${ensure.p95.toFixed(2)} ms | save ${save.p95.toFixed(2)} ms | DOM ${result.snapshot.gauges.domNodes}`);
|
||||
}
|
||||
console.log(`BROWSER_BENCHMARK_JSON=${JSON.stringify(results.map(result=>({
|
||||
profile:result.profile,boards:result.boards,cpuRate:result.cpuRate,
|
||||
dragP95:timing(result.snapshot,'processBoardDragFrame').p95,
|
||||
cameraP95:timing(result.snapshot,'commitCameraInteraction').p95,
|
||||
dragGapP95:timing(result.pickupCadence||result.snapshot,'pickupVisualFrameGap').p95,
|
||||
dragInputAgeP95:timing(result.pickupCadence||result.snapshot,'pickupVisualInputAge').p95,
|
||||
cameraGapP50:timing(result.snapshot,'cameraFrameGap').p50,
|
||||
cameraInputAgeP95:timing(result.snapshot,'cameraInputAge').p95,
|
||||
minimapP95:timing(result.snapshot,'drawMinimap').p95,
|
||||
lodP95:timing(result.snapshot,'ensureBoards').p95,
|
||||
saveP95:timing(result.snapshot,'persistDirtyToDb').p95,
|
||||
overviewP95:timing(result.snapshot,'drawWorldOverview').p95,
|
||||
renderedBoards:result.snapshot.gauges.renderedBoards,
|
||||
staticBoards:result.snapshot.gauges.staticBoards,
|
||||
domNodes:result.snapshot.gauges.domNodes,
|
||||
longTasks:result.snapshot.counters.longTasks||0
|
||||
})))}`);
|
||||
const profileSummary=results.map(profileSummaryRow);report.profiles=profileSummary;console.log(`BROWSER_BENCHMARK_JSON=${JSON.stringify(profileSummary)}`);
|
||||
report.status='passed';writeBenchmarkReport(report);console.log(`Browser benchmark report: ${benchmarkOutputPath}`);
|
||||
console.log('Real-browser performance benchmark passed');
|
||||
}catch(error){
|
||||
report.status='failed';report.failure={name:error?.name||'Error',message:error?.message||String(error),stack:error?.stack||''};
|
||||
try{writeBenchmarkReport(report);console.error(`Browser benchmark failure report: ${benchmarkOutputPath}`)}catch(reportError){console.error(`Browser benchmark report write failed: ${reportError.message}`)}
|
||||
throw error;
|
||||
}finally{
|
||||
client?.close();
|
||||
stopBrowserTree(edge);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue