Files
mdg-filius/tools/live/m3.ts
T
julian.kiedaischandClaude Opus 5 26ed1495fc chore(tools): the live tools take their target, they do not assume it
The dashboard's fifth guard learned to see a host name and found three in
tools/live: a default host in lib.ts and two health checks written out.
A measuring tool that aims at somebody else's server by default is a trap,
so FILIUS_HOST has no fallback and the run says what to set; the two checks
use BASE, which is derived from it.

BASE had to be imported in m3.ts — without that this would have been a
NameError instead of the intended refusal.

README.md keeps the real names: the project is this school's and the prose
describes its deployment; what has to stand on its own is the code.

The allow file also excuses the simulator's own data. Filius simulates a
network, so its scenarios, tests and documentation carry 65 693 addresses
(measured) — all of invented machines in invented networks, which is the
point of the program. Deliberately broad: a list of scenario files would be
one nobody keeps correct.

typecheck green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-11-13 19:09:03 +00:00

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 { BASE, 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(`${BASE}/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(`${BASE}/healthz`)).json();
console.log('after clean-up:', JSON.stringify(health), (await svc('GET', '/svc/sessions')).status);
}