M1-03D
zoen-zone part D: socket load check (~200 WebSocket clients, 10 s) and tick p99 bench re-run
Evidence
From logs/evidence/M1-03D/. Click a thumbnail for the full image.
Logs and data (8)
bench-excerpt.txt · 791 B · open
# pnpm zone:bench re-run (M1-03D), release, one thread. Full JSON: tick-bench.json. Log: logs/test-output/M1-03D-04.log bench start: 5:37 up 2:44, 1 user, load averages: 66.75 92.23 73.80 bench end: 5:37 up 2:45, 1 user, load averages: 58.10 89.07 73.00 movers-1000 p50 1.126 ms p99 7.602 ms max 20.682 ms alloc 0 avg set 23.3 channel-1000p-4000m p50 3.543 ms p99 9.489 ms max 23.739 ms alloc 0 avg set 117.7 town-crowd-1000p-4000m p50 4.994 ms p99 11.982 ms max 27.532 ms alloc 0 avg set 268.0 M1-03 run (logs/evidence/M1-03/tick-bench.json): movers-1000 p99 0.796 ms; channel-1000p-4000m p99 2.508 ms. Machine: Apple M3, 8 cores (4P+4E); load average 58-92 during the run. Pass (p99 <= 15 ms, 0 allocations after warmup) despite the load.
load-average.txt · 439 B · open
loadcheck start: 5:35 up 2:43, 1 user, load averages: 95.18 105.82 75.65 loadcheck end: 5:36 up 2:43, 1 user, load averages: 85.95 103.21 75.24 snapbench start: 5:36 up 2:43, 1 user, load averages: 85.95 103.21 75.24 snapbench end: 5:37 up 2:44, 1 user, load averages: 66.75 92.23 73.80 bench start: 5:37 up 2:44, 1 user, load averages: 66.75 92.23 73.80 bench end: 5:37 up 2:45, 1 user, load averages: 58.10 89.07 73.00
server-log.txt · 1 kB · open
# Excerpt of logs/test-output/M1-03D-02.log (pnpm zone:loadcheck, 200 clients, 10 s). Full log: 400 connection lines; first and last shown. [loadcheck] server on ws://127.0.0.1:57853; 200 clients for 10 s (load average 89.96 104.56 75.38) [zone] 127.0.0.1:57854 joined (slot 0) [zone] 127.0.0.1:57855 joined (slot 1) [zone] 127.0.0.1:57856 joined (slot 2) ... (200 joined lines) [zone] t= 5.0s tick=100 clients=200 tick p50=12.82 ms p99=63.32 ms max=81.15 ms sent=18854 skipped=0 KB/s/client≈13.22 [zone] t= 10.0s tick=201 clients=200 tick p50=14.09 ms p99=84.72 ms max=127.90 ms sent=40061 skipped=0 KB/s/client≈12.27 [zone] 127.0.0.1:58043 left (slot 189): closed [zone] 127.0.0.1:58044 left (slot 190): closed [zone] 127.0.0.1:58045 left (slot 191): closed ... (200 left lines; reasons: 200 closed) loadcheck FAIL: 200/200 joined, 200 clean closes, loop p50 13.64 p99 84.72 max 127.90 mean 18.75 ms (target 15; tick-thread CPU 6.43 ms/tick), down mean 12797 p95 13759 max 13867 B/s/client (cap 32000), up mean 521 B/s, snapshot mean 604 B max 1452 B, rtt p50 41.6 p99 62.5 ms, skipped frames 0 of 44659, dropped ticks 0, load 89.96 104.56 75.38 -> 85.95 103.21 75.24 failed: loopP99WithinTarget
snapbench-excerpt.txt · 1 kB · open
# pnpm zone:snapbench re-run (M1-03D), release, one thread. Full JSON: snapshot-bench.json. Log: logs/test-output/M1-03D-03.log snapbench start: 5:36 up 2:43, 1 user, load averages: 85.95 103.21 75.24 snapbench end: 5:37 up 2:44, 1 user, load averages: 66.75 92.23 73.80 quiet-field-25 set 23.1 sent 23.1 mean 1.33 KB/s max 1.86 KB/s (doc 1.8) max packet 178 B tick+snap p99 47.62 ms busy-hunting-70 set 57.6 sent 57.6 mean 2.65 KB/s max 3.41 KB/s (doc 6) max packet 352 B tick+snap p99 64.92 ms crowded-town-300 set 245.0 sent 148.0 mean 9.40 KB/s max 10.30 KB/s (doc 14) max packet 647 B tick+snap p99 241.17 ms channel-1000p-4000m set 119.1 sent 119.0 mean 4.80 KB/s max 6.07 KB/s (doc 6) max packet 580 B tick+snap p99 162.59 ms town-crowd-1000p-4000m set 254.7 sent 149.8 mean 9.57 KB/s max 10.40 KB/s (doc 14) max packet 648 B tick+snap p99 153.43 ms M1-03B run (logs/evidence/M1-03B/snapshot-bench.json) tick+snap p99: 198.5 / 122.7 / 564.8 / 219.5 / 520.9 ms; bandwidth identical (deterministic). Machine: Apple M3, 8 cores (4P+4E); load average 86-103 (1/5 min) during the run, so timing is not an idle-machine number.
summary.txt · 2 kB · open
M1-03D - socket load check and bench re-runs (2026-10-11). Machine: Apple M3, 8 cores (4P+4E), heavily loaded by
other work: load average 86-106 during the load check, 58-103 during the benches (load-average.txt).
pnpm zone:loadcheck (new; services/zone/src/bin/loadcheck.rs) - 200 WebSocket clients, 10 s, in-process server on an
ephemeral loopback port (loadcheck.json, server-log.txt):
PASS 200/200 joined, 200 clean 1000 closes answered, server saw 200 leaves, 0 client errors, 0 threads left
PASS inputs 36,967 applied, 0 stale, 0 dropped; frames 44,659 sent, 0 skipped; 0 missed ticks seen by clients;
scheduler 0 dropped ticks, 4 catch-up ticks
PASS downstream per client mean 12.8 KB/s, p95 13.8, max 13.9 (hard cap 32 KB/s); snapshot frames mean 604 B,
p95 777 B, max 1,452 B (packet budget)
FAIL loop p99 84.7 ms (target 15 ms); p50 13.6, mean 18.8, max 127.9 ms; 88 ticks over target, 11 over 50 ms
info tick-thread CPU 6.43 ms per tick = 34 % of the loop's wall time: the tick spent about two thirds of each
iteration waiting for a core. The p99 miss is machine load; per-tick CPU p99 is not measured (mean only).
info upstream per client 521 B/s (doc estimate 0.3 KB/s counts inputs only; acks per snapshot and pings add the
rest); RTT p50 41.6 ms, p99 62.5 ms (pings are answered by the tick, so ~1 tick of server queueing).
Earlier exploratory runs at load 60-80: 50 clients p50 1.3 ms, 100 clients p50 12.6 ms; 20 clients at load 64:
tick CPU 0.12 ms/tick but loop mean 7.2 ms (CPU share 1.7 %) - the same load signature.
pnpm zone:snapbench re-run (snapshot-bench.json, snapbench-excerpt.txt): bandwidth identical to M1-03B and within
budget (crowded town max 10.4 KB/s); one-thread tick+snapshots p99 47.6-241.2 ms, still not an idle number.
pnpm zone:bench re-run (tick-bench.json, bench-excerpt.txt): PASS, p99 7.6 / 9.5 / 12.0 ms on one thread, 0 allocations.
Reading for M1-04: everyone-in-one-spot at 200 clients costs ~6.4 ms of tick CPU (mostly snapshot building, going by the tick bench). A 1,000-player
crowd on one thread would not fit 15 ms p99; NETCODE_1000.md already plans parallel per-client work over 4 cores.
An idle-machine re-run of `pnpm zone:loadcheck` is still owed (fold into the M1-04 soak run).
loadcheck.json · 3 kB · open
{
"task": "M1-03D",
"generatedBy": "pnpm zone:loadcheck (cargo run --release -p zoen-zone --bin loadcheck)",
"what": "200 headless WebSocket clients on loopback against the in-process zone server for 10 s: random walk (new heading every 0.5-2 s), one input per tick, an ack per snapshot, a ping per second; then a 1000 close each",
"machine": {"os": "macos", "arch": "aarch64", "availableParallelism": 8, "loadAverageBefore": "89.96 104.56 75.38", "loadAverageAfter": "85.95 103.21 75.24", "note": "clients, server tick and connection threads share this machine"},
"thresholds": {"loopP99Ms": 15.000, "loopP99Source": "netcode.json tick.targetP99Ms", "downstreamMaxBps": 32000, "downstreamSource": "netcode.json snapshot.hardCapKBps (KB = 1,000 B)", "skippedMaxPercent": 1},
"clients": {"requested": 200, "joined": 200, "cleanCloses": 200, "serverCloses": 0, "errors": 0, "activeSecondsMean": 10.034, "runSeconds": 10.064},
"loop": {"what": "server loop iteration: ingest + tick + all snapshots + hand-off (10 us histogram)", "seconds": 11.169, "ticks": 224, "p50Ms": 13.640, "p99Ms": 84.720, "maxMs": 127.896, "meanMs": 18.753, "tickThreadCpuMsPerTick": 6.433, "cpuShareOfLoopWall": 0.343, "overTarget": 88, "overBudget": 11, "catchUpTicks": 4, "droppedTicks": 0, "stalls": 0, "maxLatenessMs": 90.409},
"frames": {"sent": 44659, "skipped": 0, "snapshotErrors": 0, "clientSnapshots": 42463, "clientFullSnapshots": 2283, "clientMissedTicks": 0, "serverMaxSnapshotPayloadBytes": 1448},
"downstreamBpsPerClient": {"mean": 12796.5, "p50": 12809.3, "p95": 13758.7, "p99": 13797.0, "max": 13867.5, "samples": 200},
"downstreamServerCount": {"meanBps": 12089.9, "maxBps": 12737.7, "note": "server-side bytes handed to each writer, join to leave"},
"upstreamBpsPerClient": {"mean": 520.8, "p50": 520.8, "p95": 540.4, "p99": 543.0, "max": 544.4, "samples": 200},
"upstreamTarget": {"bps": 300, "source": "netcode.json snapshot.upstreamTargetKBps (NETCODE_1000.md: 20 inputs/s x ~14 B)", "within": false, "note": "recorded, not a check: the target counts inputs only; acks (one per snapshot) and pings are separate messages"},
"snapshotFrameBytes": {"mean": 604.2, "p50": 583.0, "p95": 777.0, "p99": 1452.0, "max": 1452.0, "samples": 42463},
"rttMs": {"mean": 38.150, "p50": 41.555, "p95": 51.085, "p99": 62.450, "max": 117.556, "samples": 1996},
"rttNote": "application RTT on loopback: the tick answers pings, and each client sends right after a snapshot arrives, so about one tick period (50 ms) is server queueing, not network",
"messagesPerClientPerSecond": {"snapshots": 21.16, "inputs": 18.42, "acks": 21.16, "pings": 0.99, "pongs": 0.99},
"inputs": {"applied": 36967, "stale": 0, "dropped": 0},
"server": {"accepted": 200, "joins": 200, "leaves": 200, "peakClients": 200, "refusedHandshakes": 0, "versionMismatches": 0, "full": 0, "idleTimeouts": 0, "badMessages": 0, "threadsLeft": 0},
"errors": [],
"checks": {"allJoined": true, "allClosedCleanly": true, "noClientErrors": true, "serverSawEveryLeave": true, "noStaleOrDroppedInputs": true, "framesSkippedUnder1Percent": true, "ticksDroppedUnder1Percent": true, "loopP99WithinTarget": false, "downstreamWithinHardCap": true, "noSnapshotErrors": true, "noThreadsLeft": true},
"pass": false
}
snapshot-bench.json · 3 kB · open
{
"task": "M1-03B",
"what": "snapshot bytes per client (movement share, WebSocket header included) vs NETCODE_1000.md Bandwidth math; tick + all snapshots on one thread",
"build": "release",
"ticks": 400,
"ackDelayTicks": 3,
"everyoneWalks": true,
"criterion": "pass = max client <= 32 KB/s and every packet <= the budget; tick + snapshot time is reported (withinP99Target) but only meaningful on an idle machine",
"pass": true,
"scenes": [
{"id": "quiet-field-25", "players": 400, "monsters": 0, "areaM": 640, "radiusM": 90, "interestSetAvg": 23.1, "sentEntitiesAvg": 23.1, "kBpsPerClient": {"mean": 1.33, "max": 1.86, "docMovement": 1.8, "hardCap": 32}, "maxPacketBytes": 178, "packetBudgetBytes": 1596, "deferredPerClientTick": 0.000, "tickPlusSnapshotsMs": {"p99": 47.616, "max": 146.533, "threads": 1, "withinP99Target": false}, "pass": true},
{"id": "busy-hunting-70", "players": 400, "monsters": 0, "areaM": 380, "radiusM": 90, "interestSetAvg": 57.6, "sentEntitiesAvg": 57.6, "kBpsPerClient": {"mean": 2.65, "max": 3.41, "docMovement": 6, "hardCap": 32}, "maxPacketBytes": 352, "packetBudgetBytes": 1596, "deferredPerClientTick": 0.000, "tickPlusSnapshotsMs": {"p99": 64.918, "max": 100.644, "threads": 1, "withinP99Target": false}, "pass": true},
{"id": "crowded-town-300", "players": 1000, "monsters": 0, "areaM": 226, "radiusM": 70, "interestSetAvg": 245.0, "sentEntitiesAvg": 148.0, "kBpsPerClient": {"mean": 9.40, "max": 10.30, "docMovement": 14, "hardCap": 32}, "maxPacketBytes": 647, "packetBudgetBytes": 1596, "deferredPerClientTick": 0.000, "tickPlusSnapshotsMs": {"p99": 241.172, "max": 733.987, "threads": 1, "withinP99Target": false}, "pass": true},
{"id": "channel-1000p-4000m", "players": 1000, "monsters": 4000, "areaM": 1024, "radiusM": 90, "interestSetAvg": 119.1, "sentEntitiesAvg": 119.0, "kBpsPerClient": {"mean": 4.80, "max": 6.07, "docMovement": 6, "hardCap": 32}, "maxPacketBytes": 580, "packetBudgetBytes": 1596, "deferredPerClientTick": 0.000, "tickPlusSnapshotsMs": {"p99": 162.590, "max": 282.240, "threads": 1, "withinP99Target": false}, "pass": true},
{"id": "town-crowd-1000p-4000m", "players": 1000, "monsters": 4000, "areaM": 256, "radiusM": 70, "interestSetAvg": 254.7, "sentEntitiesAvg": 149.8, "kBpsPerClient": {"mean": 9.57, "max": 10.40, "docMovement": 14, "hardCap": 32}, "maxPacketBytes": 648, "packetBudgetBytes": 1596, "deferredPerClientTick": 0.000, "tickPlusSnapshotsMs": {"p99": 153.429, "max": 295.655, "threads": 1, "withinP99Target": false}, "pass": true}
]
}
tick-bench.json · 2 kB · open
{
"task": "M1-03",
"generatedBy": "pnpm zone:bench (cargo run --release -p zoen-zone --bin bench-tick)",
"machine": {"os": "macos", "arch": "aarch64", "availableParallelism": 8, "threadsUsed": 1},
"config": {"tickHz": 20, "budgetMs": 50.000, "targetP99Ms": 15.000, "cellM": 16, "grid": [256, 256], "radiusM": 90, "townRadiusM": 70, "leaveMarginM": 5, "maxInterestSet": 512},
"covers": "zoen-sim step (one input per entity per tick + movement), spatial hash update, interest sets with enter/leave diffs, state hash. Not yet: AI, combat, snapshot encoding, network (M1-03B, M1-04, M2).",
"criterion": "tick p99 <= targetP99Ms and 0 allocations after warmup on one thread",
"pass": true,
"scenarios": [
{"id": "movers-1000", "what": "1,000 moving players with interest sets in a 1,024 m field", "players": 1000, "monsters": 0, "viewers": 1000, "radiusM": 90, "warmupTicks": 100, "ticks": 1200, "tickMs": {"p50": 1.126, "p99": 7.602, "max": 20.682, "mean": 1.583}, "allocationsAfterWarmup": 0, "interestSetAvg": 23.3, "interestSetMax": 37, "fullSets": 0, "entersPerTick": 49.7, "leavesPerTick": 49.8, "cellMovesPerTick": 25.2, "droppedChanges": 0, "pass": true},
{"id": "channel-1000p-4000m", "what": "full channel: 1,000 players with interest sets + 4,000 moving monsters, all in a 1,024 m field", "players": 1000, "monsters": 4000, "viewers": 1000, "radiusM": 90, "warmupTicks": 100, "ticks": 1200, "tickMs": {"p50": 3.543, "p99": 9.489, "max": 23.739, "mean": 4.062}, "allocationsAfterWarmup": 0, "interestSetAvg": 117.7, "interestSetMax": 158, "fullSets": 0, "entersPerTick": 249.1, "leavesPerTick": 248.7, "cellMovesPerTick": 126.3, "droppedChanges": 0, "pass": true},
{"id": "town-crowd-1000p-4000m", "what": "worst case: 1,000 players crowding a 256 m town (70 m radius) + 4,000 monsters in the 1,024 m field", "players": 1000, "monsters": 4000, "viewers": 1000, "radiusM": 70, "warmupTicks": 100, "ticks": 1200, "tickMs": {"p50": 4.994, "p99": 11.982, "max": 27.532, "mean": 5.510}, "allocationsAfterWarmup": 0, "interestSetAvg": 268.0, "interestSetMax": 353, "fullSets": 0, "entersPerTick": 663.7, "leavesPerTick": 658.5, "cellMovesPerTick": 127.6, "droppedChanges": 0, "pass": true}
]
}
Tests
$ pnpm zone:loadcheck --out logs/evidence/M1-03D/loadcheck.json$ pnpm zone:loadcheck --out logs/evidence/M1-03D/loadcheck.json$ pnpm zone:snapbench --out logs/evidence/M1-03D/snapshot-bench.json$ pnpm zone:bench --out logs/evidence/M1-03D/tick-bench.json$ pnpm --filter @zoen/wiki exec playwright test --only-changedNotes
2026-10-10 21:40 UTC · Built pnpm zone:loadcheck (services/zone/src/bin/loadcheck.rs, no crates): in-process ZoneServer on an ephemeral loopback port, tick on a thread owned by the harness so its CPU time (clock_gettime THREAD_CPUTIME, macOS/Linux) is read next to wall time; 200 Client threads (5 ms connect ramp, barrier, 10 s window): random walk (heading every 0.5-2 s, 1 in 8 stands), one input per tick period (sim.json tickHz; NETCODE_1000.md 20 inputs/s) sent from the receive loop, ack per snapshot, ping per second; then close 1000; shutdown drains leaves. Checks (exit 1 on breach): all joined + clean closes + server leaves, 0 stale/dropped inputs, skipped frames and dropped ticks <1% (card), loop p99 <= netcode.json tick.targetP99Ms, every client downstream <= snapshot.hardCapKBps, 0 snapshot errors, threads_left 0. Run M1-03D-02 at load avg 90-105 (8 cores): FAIL only on loop p99 84.7 ms (p50 13.6, mean 18.8, max 127.9); tick CPU 6.43 ms/tick = 34% of wall, so the miss is machine load (per-tick CPU p99 not measured). Everything else passed: 200/200 joined + clean closes, 36,967 inputs 0 stale/0 dropped, 0/44,659 frames skipped, 0 dropped ticks, down mean 12.8 / p95 13.8 / max 13.9 KB/s (cap 32), snapshot mean 604 B max 1,452 B, RTT p50 41.6 / p99 62.5 ms (tick answers pings), up 521 B/s (doc 0.3 KB/s counts inputs only; acks + pings recorded, not checked). Earlier M1-03D-01 (before CPU metric) at load ~40-60: p99 2.2 s. zone:bench re-run PASS (p99 7.6/9.5/12.0 ms one thread, load 58-92); zone:snapbench re-run PASS (bandwidth identical to M1-03B; one-thread tick+snap p99 47.6-241 ms, load 67-103). Small lib changes: Client.bytes_out, Client::close() -> bool (clean handshake), SchedulerMetrics.total_tick_ns, ServerReport tick_mean_ns/catch_up_ticks/stalls. netcode.json unchanged (no measured slots); NETCODE_1000.md transport gains the load-check line. Evidence logs/evidence/M1-03D/: loadcheck.json, server-log.txt, snapbench-excerpt.txt, bench-excerpt.txt, load-average.txt, summary.txt, snapshot-bench.json, tick-bench.json. Owed: idle-machine re-run of zone:loadcheck (fold into M1-04); M1-04 likely needs parallel snapshot building (6.4 ms CPU for 200 clients in one spot).
