const percentile=(values,p)=>{if(!values.length)return 0;const sorted=[...values].sort((a,b)=>a-b);return Math.round(sorted[Math.min(sorted.length-1,Math.floor((sorted.length-1)*p))]*100)/100;}; const statistics=values=>({p50:percentile(values,.5),p95:percentile(values,.95),p99:percentile(values,.99),max:percentile(values,1)}); /** Bounded local trace. Durations use monotonic browser time, not wall clocks. */ export class DynamicsDiagnostics { constructor(){this.status='idle';this.recent=[];this.frames=[];this.events=[];this.server=[];this.result=null;this.scenario='record';this.duration=30;this.toggleSent=false;} arm(scenario='record',duration=30){ this.status='warming';this.scenario=scenario;this.duration=Math.min(120,Math.max(5,duration));this.frames=[];this.events=[];this.server=[];this.result=null;this.toggleSent=false;this.climbUntil=0;this.armed=performance.now();this.stableAt=null; } ready(sample,ready,now=performance.now()){ if(this.status!=='warming')return; if(ready)this.stableAt??=now;else this.stableAt=null; if(this.stableAt!==null&&now-this.stableAt>=500){this.status='recording';this.started=now;this.initial=sample;this.yaw=sample.yaw;} if(now-this.armed>120000){this.status='error';this.result={error:'Terrain did not settle within 120 seconds',sample};} } frame(sample,now=performance.now()){ this.recent.push(sample);if(this.recent.length>300)this.recent.shift(); if(this.status!=='recording')return; if(this.frames.length<18000)this.frames.push({...sample,t:now-this.started}); if(now-this.started>=this.duration*1000)this.stop(now); } event(type,milliseconds,detail={}){if(this.status==='recording'&&this.events.length<3000)this.events.push({t:performance.now()-this.started,type,milliseconds,...detail});} serverSample(sample){if(this.status==='recording'&&this.server.length<150)this.server.push({t:performance.now()-this.started,...sample});} controls(consume,flying,now=performance.now(),collision=false){ if(this.status!=='recording'||this.scenario==='record')return null; const t=(now-this.started)/1000,fly=this.scenario!=='run'; let fly_toggle=false; if(fly&&!flying&&!this.toggleSent){fly_toggle=true;if(consume)this.toggleSent=true;} if(fly&&collision&&consume)this.climbUntil=now+900; return {forward:fly&&t<2.5?0:1,strafe:0,jump:fly&&(t<2.5||now<(this.climbUntil||0)),sprint:true,sneak:false,fly_toggle, yaw:this.yaw+(this.scenario==='turns'?Math.sin(t*.8)*1.3:Math.sin(Math.max(0,t-3)*.11)*.35),pitch:fly?-.16:-.08}; } stop(now=performance.now(),reason=null){ if(this.status==='warming'){this.status='idle';return;} if(this.status!=='recording')return; const f=this.frames,last=f.at(-1)||this.initial,values=k=>f.map(x=>x[k]||0); let distance=0;for(let i=1;iv-f[i-1].position[a])); this.result={schema:1,scenario:this.scenario,interrupted:reason,durationMs:Math.round(now-this.started),frames:f.length, frameMs:statistics(values('frameMs')),physicsMs:statistics(values('physicsMs')),drawMs:statistics(values('drawMs')), over25ms:f.filter(x=>x.frameMs>25).length,over50ms:f.filter(x=>x.frameMs>50).length, streamApplyMs:statistics(this.events.filter(e=>e.type==='sections').map(e=>e.milliseconds)), networkHandlerMs:statistics(this.events.filter(e=>e.type==='network').map(e=>e.milliseconds)), longTasks:this.events.filter(e=>e.type==='longtask').length, maxCorrection:percentile(values('correction'),1),waitingFrames:f.filter(x=>x.waiting).length, maxDirty:percentile(values('dirty'),1),distance:Math.round(distance*100)/100, viewChanges:last.viewChanges-this.initial.viewChanges,sectionBatches:last.batches-this.initial.batches, meshResets:last.meshResets-this.initial.meshResets,loadedSections:last.loadedSections,totalSections:last.totalSections, voxelBytes:last.voxelBytes,server:this.server.at(-1)||null,start:this.initial.position,end:last.position}; this.status='done'; } trace(){return {summary:this.result,frames:this.frames,events:this.events,server:this.server};} } export function drawDynamicsChart(canvas,samples){ const ctx=canvas.getContext('2d');if(!ctx)return; const w=canvas.width,h=canvas.height;ctx.fillStyle='#10231b';ctx.fillRect(0,0,w,h); ctx.font='11px monospace';ctx.strokeStyle='#405448';ctx.fillStyle='#a5b8ac'; for(const ms of [16.7,33.3,50]){const y=h-ms/70*h;ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(w,y);ctx.stroke();ctx.fillText(`${ms} ms`,4,y-3);} for(const [key,color] of [['frameMs','#efc46a'],['drawMs','#8bd0b4'],['physicsMs','#8bb6ed']]){ ctx.strokeStyle=color;ctx.beginPath();samples.forEach((s,i)=>{const x=i/300*w,y=h-Math.min(70,s[key]||0)/70*h;i?ctx.lineTo(x,y):ctx.moveTo(x,y);});ctx.stroke(); } }