feat: F5.6c measured and accepted on test-filius; tickets up to 5 s ahead of F's clock

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>
This commit is contained in:
2026-10-04 19:22:22 +00:00
co-authored by Claude Opus 5.5
parent 087721cba3
commit 416d5fdea3
15 changed files with 630 additions and 7 deletions
+93
View File
@@ -0,0 +1,93 @@
// 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);
}