Files
citygen/tools/load-bots.ts
T

54 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();}