Files
shacraft-core/client/dynamics-diagnostics.js
Emil c7e86663d8
MVP checks / mvp (push) Waiting to run
Expand voxel gameplay, lighting, full-height streaming and world imports
Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
2026-09-17 02:10:53 +03:00

61 lines
4.7 KiB
JavaScript

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;i<f.length;i++)distance+=Math.hypot(...f[i].position.map((v,a)=>v-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();
}
}