54 lines
3.9 KiB
TypeScript
54 lines
3.9 KiB
TypeScript
import { create,fromBinary,toBinary } from '@bufbuild/protobuf';
|
||
import { ClientMessageSchema,ServerMessageSchema } from '../client/generated/city_pb';
|
||
const [base='http://127.0.0.1:3000',cityId='balzers-demo',duration='20']=process.argv.slice(2);
|
||
const seconds=Number(duration);
|
||
if (!Number.isFinite(seconds) || seconds<1 || seconds>600) throw new Error('Duration must be 1–600 seconds');
|
||
const response=await fetch(`${base}/api/rooms`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cityId,seed:1})});
|
||
const room=await response.json();if (!response.ok) throw new Error(room.error);
|
||
const manifest=await (await fetch(`${base}/cities/${encodeURIComponent(cityId)}/manifest.json`)).json();
|
||
const bounds=manifest.city.bounds;
|
||
const sockets:WebSocket[]=[];const bytes=Array<number>(30).fill(0);let snapshots=0,deltas=0,throttles=0;const errors:string[]=[];
|
||
let started=0;
|
||
let expectedClose=false;
|
||
try {
|
||
await Promise.all(Array.from({length:30},(_,i)=>new Promise<void>((resolve,reject)=>{
|
||
const url=new URL(`/api/rooms/${encodeURIComponent(room.id)}/ws`,base);url.protocol=url.protocol==='https:'?'wss:':'ws:';
|
||
const ws=new WebSocket(url);ws.binaryType='arraybuffer';sockets.push(ws);
|
||
let lod=0;
|
||
const send=(body:any)=>ws.send(toBinary(ClientMessageSchema,create(ClientMessageSchema,{body})));
|
||
const x=bounds.minX+(bounds.maxX-bounds.minX)*(i%6+0.5)/6;
|
||
const y=bounds.minY+(bounds.maxY-bounds.minY)*(Math.floor(i/6)+0.5)/5;
|
||
const subscribe=()=>send({case:'subscribe',value:{lod,rect:{minX:Math.round((x-50)*100),maxX:Math.round((x+50)*100),minY:Math.round((y-50)*100),maxY:Math.round((y+50)*100)}}});
|
||
const timeout=setTimeout(()=>reject(new Error(`Client ${i} did not receive a snapshot`)),30000);
|
||
ws.onopen=()=>send({case:'join',value:{room:room.id,clientVersion:1,nickname:`Load-${i}`}});
|
||
ws.onerror=()=>{errors.push(`Client ${i} connection error`);clearTimeout(timeout);reject(new Error(errors.at(-1)));};
|
||
ws.onclose=()=>{clearTimeout(timeout);if(!expectedClose){errors.push(`Client ${i} disconnected`);reject(new Error(errors.at(-1)));}};
|
||
ws.onmessage=ev=>{
|
||
bytes[i]+=(ev.data as ArrayBuffer).byteLength;
|
||
const b=fromBinary(ServerMessageSchema,new Uint8Array(ev.data as ArrayBuffer)).body;
|
||
if (b.case==='welcome') subscribe();
|
||
else if (b.case==='snapshot') {snapshots++;clearTimeout(timeout);resolve();}
|
||
else if (b.case==='delta') deltas++;
|
||
else if (b.case==='error') {
|
||
if (b.value.code==='ENTITY_LIMIT' && lod<10) {lod++;subscribe();}
|
||
else if (b.value.code==='TRAFFIC_LIMIT') {throttles++;setTimeout(()=>{if(ws.readyState===WebSocket.OPEN)subscribe();},1000);}
|
||
else {errors.push(`${i}:${b.value.code}:${b.value.message}`);clearTimeout(timeout);reject(new Error(errors.at(-1)));}
|
||
}
|
||
};
|
||
})));
|
||
started=performance.now();
|
||
const samples:number[]=[];let sampling=true;
|
||
const sampler=(async()=>{while(sampling){
|
||
const text=await (await fetch(`${base}/metrics`)).text();
|
||
const line=text.split('\n').find(line=>line.startsWith(`citygen_room_tick_seconds{room="${room.id}"}`));
|
||
if(line)samples.push(Number(line.split(' ').at(-1))*1000);
|
||
await new Promise(resolve=>setTimeout(resolve,200));
|
||
}})();
|
||
await new Promise(resolve=>setTimeout(resolve,seconds*1000));
|
||
sampling=false;await sampler;samples.sort((a,b)=>a-b);
|
||
const metrics=await (await fetch(`${base}/metrics`)).text();
|
||
const elapsed=(performance.now()-started)/1000;
|
||
console.log(JSON.stringify({room:room.id,clients:30,seconds:elapsed,snapshots,deltas,throttles,sampledTickP95Ms:samples[Math.floor(samples.length*.95)],sampledTickMaxMs:samples.at(-1),meanBytesPerClientSecond:Math.round(bytes.reduce((a,b)=>a+b,0)/30/elapsed),maxBytesPerClientSecond:Math.round(Math.max(...bytes)/elapsed),errors,metrics},null,2));
|
||
if (errors.length) process.exitCode=1;
|
||
} finally {expectedClose=true;for(const socket of sockets)socket.close();}
|