test-filius refused every ticket as from the future: its clock was 886 ms behind the dashboard's machine, with NTP still 8 ms (one ticket in 125, as a ticket carries whole seconds). F now takes a ticket signed up to 5 s ahead (TICKET_AHEAD_SECONDS); itsdangerous refuses one second, and the oracle keeps that on record. tools/live measures and accepts on a live F, with fresh projects each run: m3 (200 rooms of the largest example with pings: probe p95 37 ms), m4 (63 KB document; dragging makes an update per pointer move, 59/s), m5 (an hour through the proxy), acceptance (two browsers, 14 checks, passed three times) and clock (F's clock against ours). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
94 lines
4.5 KiB
TypeScript
94 lines
4.5 KiB
TypeScript
// M3 on a live server F: how many simulation rooms it carries. Each room is a
|
|
// fresh project with the largest example, one person, the simulation at
|
|
// 100 %, the cables' activity watched (as an editor does) and a ping across
|
|
// the network every 6 s. The probe: once a second per room, an action for a
|
|
// device that does not exist — the room answers it at once, between two of
|
|
// its thread's ticks, so the time to the answer is how far behind the
|
|
// room's thread is (plus the way there and back). Rooms are added in steps;
|
|
// each step is held, then measured. Every project made is deleted at the end.
|
|
//
|
|
// npx vite-node tools/live/m3.ts [steps, e.g. 1,10,25,50] [seconds per step]
|
|
|
|
import { cleanUp, openRoom, seededProject, signIn, svc, type Room } from './lib.js';
|
|
|
|
const EXAMPLE = 'java-examples/Internet_Komplett_mit_eMail_Webserver_Intranet_Portforwarding_Firewall_DHCP_DE.fls';
|
|
const steps = (process.argv[2] ?? '1,10,25,50,100,150,200').split(',').map(Number);
|
|
const seconds = Number(process.argv[3] ?? 45);
|
|
const SETTLE_SECONDS = 10;
|
|
|
|
interface Running {
|
|
readonly room: Room;
|
|
readonly timers: NodeJS.Timeout[];
|
|
}
|
|
|
|
const rooms: Running[] = [];
|
|
let samples: number[] = [];
|
|
let lost = 0;
|
|
let errors = 0;
|
|
const pending = new Map<string, number>();
|
|
let counter = 0;
|
|
|
|
async function addRoom(): Promise<void> {
|
|
const { project, example } = await seededProject(EXAMPLE);
|
|
const room = await openRoom(project, await signIn(project, { name: `Raum ${rooms.length + 1}` }));
|
|
const ipad = example.graph.nodes.find((node) => node.displayName === 'iPad 1')!;
|
|
room.onMessage = (message) => {
|
|
if (message.kind === 'error' && typeof message.reqId === 'string' && pending.has(message.reqId)) {
|
|
samples.push(performance.now() - pending.get(message.reqId)!);
|
|
pending.delete(message.reqId);
|
|
} else if (message.kind === 'error' && message.code !== 'unknownNode') errors += 1;
|
|
};
|
|
room.send({ kind: 'speed', value: 100 });
|
|
room.send({ kind: 'start' });
|
|
await room.waitFor((message) => message.kind === 'state' && message.mode === 'simulation', 20_000);
|
|
room.send({ kind: 'sub', topics: ['activity', `app:${ipad.id}/terminal`] });
|
|
const probe = () => {
|
|
const reqId = `p${counter++}`;
|
|
pending.set(reqId, performance.now());
|
|
room.send({ kind: 'input', node: 'probe', app: 'terminal', action: 'enter', args: { line: '' }, reqId });
|
|
};
|
|
const ping = () => room.send({ kind: 'input', node: ipad.id, app: 'terminal', action: 'enter', args: { line: 'ping 120.130.140.80' }, reqId: `ping${counter++}` });
|
|
const timers: NodeJS.Timeout[] = [setInterval(ping, 6000)];
|
|
// Spread over the second, so that the probes do not all arrive at one tick.
|
|
timers.push(setTimeout(() => timers.push(setInterval(probe, 1000)), Math.random() * 1000));
|
|
rooms.push({ room, timers });
|
|
ping();
|
|
}
|
|
|
|
const quantile = (sorted: number[], q: number) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))] ?? NaN;
|
|
|
|
try {
|
|
console.log('rooms probes p50 ms p95 ms p99 ms max ms lost errors F rooms seeding s');
|
|
for (const target of steps) {
|
|
const seeding = performance.now();
|
|
while (rooms.length < target) {
|
|
await addRoom();
|
|
await new Promise((resolve) => setTimeout(resolve, Math.random() * 50));
|
|
}
|
|
const seeded = (performance.now() - seeding) / 1000;
|
|
await new Promise((resolve) => setTimeout(resolve, SETTLE_SECONDS * 1000));
|
|
samples = [];
|
|
lost = 0;
|
|
errors = 0;
|
|
for (const sent of pending.values()) if (performance.now() - sent > 10_000) lost += 1;
|
|
pending.clear();
|
|
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
|
lost += [...pending.values()].filter((sent) => performance.now() - sent > 5_000).length;
|
|
const sorted = [...samples].sort((a, b) => a - b);
|
|
const health = await (await fetch('https://test-filius.hub.mdg-hamburg.de/healthz')).json();
|
|
console.log(
|
|
[rooms.length, sorted.length, quantile(sorted, 0.5), quantile(sorted, 0.95), quantile(sorted, 0.99), sorted.at(-1) ?? NaN]
|
|
.map((value, index) => (index < 2 ? String(value) : value.toFixed(0)).padStart(index === 0 ? 5 : 7))
|
|
.join(' ') + ` ${String(lost).padStart(4)} ${String(errors).padStart(6)} ${String(health.rooms).padStart(7)} ${seeded.toFixed(0).padStart(9)}`,
|
|
);
|
|
}
|
|
} finally {
|
|
for (const { room, timers } of rooms) {
|
|
timers.forEach((timer) => clearInterval(timer));
|
|
room.close();
|
|
}
|
|
await cleanUp();
|
|
const health = await (await fetch('https://test-filius.hub.mdg-hamburg.de/healthz')).json();
|
|
console.log('after clean-up:', JSON.stringify(health), (await svc('GET', '/svc/sessions')).status);
|
|
}
|