M1-03
zoen-zone: 20 Hz loop, spatial hash, AOI interest sets
Evidence
From logs/evidence/M1-03/. Click a thumbnail for the full image.
Logs and data (1)
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": 0.457, "p99": 0.796, "max": 1.348, "mean": 0.471}, "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": 1.661, "p99": 2.508, "max": 3.728, "mean": 1.690}, "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": 2.523, "p99": 4.051, "max": 27.672, "mean": 2.614}, "allocationsAfterWarmup": 0, "interestSetAvg": 268.0, "interestSetMax": 353, "fullSets": 0, "entersPerTick": 663.7, "leavesPerTick": 658.5, "cellMovesPerTick": 127.6, "droppedChanges": 0, "pass": true}
]
}
Tests
$ cargo run --quiet --release -p zoen-zone --bin bench-tick -- --out logs/evidence/M1-03/tick-bench.json$ pnpm --filter @zoen/wiki exec playwright test --only-changedNotes
2026-10-10 09:09 UTC · Part A built. Layout: services/zone = crate zoen-zone (workspace members crates/* + services/*): zone.rs pure tick(state, inputs, out) = zoen_sim::step (reused, parity test vs plain step) -> grid.rs SpatialHash (16 m uniform grid 256x256, intrusive per-cell lists, O(1) insert/move/remove, overflow-safe radius query) -> interest.rs InterestSets (bitset + member list per viewer, enter/leave spans, nearest-kept cap) -> sim state hash; scheduler.rs FixedRateScheduler separate from the tick (Clock trait, absolute deadlines start + n*1s/hz in u128 = no drift; catch-up policy: run all due ticks back to back up to maxTicksPerFrame=4, older backlog dropped + schedule re-anchored to now + 1 period, stall counted; metrics: over-budget, over-p99-target, lateness, catch-up, dropped, 10 us histogram percentiles); rng.rs SplitMix64 for tests/bench/bots. New crates/zoen-data (JSON reader + load_data moved out of zoen-sim-tools, which re-exports them; servers must not link sim-tools). Data moved to packages/game-data/data/netcode.json (documented: tick budget 50 ms, p50 11, p99 15, per-system budgets, cell 16 m, grid 256x256, radius 90 m, town 70 m, maxSentPerSnapshot 150, channel soft 900 / hard 1000 / 85 percent / 4000 monsters; proposed with note: scheduler.maxTicksPerFrame 4, interestSet.leaveMarginM 5, interestSet.maxSize 512). Tests: grid 20k random ops vs brute force; interest sets vs brute-force reference with hysteresis, despawns, attach/detach, radius switches and the cap; scheduler on a test clock (90k ticks at 30 Hz no drift, catch-up, stall drop, overruns, percentiles, real-clock 10 ticks); zone determinism + sim parity; no_alloc at full channel size (0 allocations in 100 ticks). Bench (release, 1 thread, this Mac): movers-1000 p99 0.796 ms, channel-1000p-4000m p99 2.508 ms, town-crowd-1000p-4000m p99 4.051 ms (max 27.7 ms, machine noise), 0 allocations after warmup -> logs/evidence/M1-03/tick-bench.json; pnpm zone:bench. Docs: NETCODE_1000.md points to netcode.json, ADR-003 build status, ARCHITECTURE layout; data test netcode.test.mjs. Remaining = M1-03B (roadmap card added): delta snapshots (baselines/acks, bit-packing, priority accumulators + near/mid/far rates), packages/protocol schema entries + Rust generator, WebSocket endpoint for the M1-04 bots. Multi-core (rayon/4 cores) not needed yet: single thread already under target.
