// Terrain budget benchmark under plain Node (readme ยง5: an op + flood fill + colliders in 2 ms). // Usage: npm run bench:node // The vitest benchmarks run through the test runner's module transform, which slows hot loops // that read imported bindings; this script measures what the server actually runs. import { World } from 'planck'; import { loadCharacterStats, loadMap, loadMaterials, resolveAssetsDir } from '../src/server/assets'; import { Game } from '../src/server/game'; import { TerrainColliders } from '../src/server/physics/terrainColliders'; import { Rng } from '../src/shared/rng'; import { DetachFinder } from '../src/shared/terrain/detach'; import { applyCells } from '../src/shared/terrain/events'; import { applyOp } from '../src/shared/terrain/ops'; const assets = resolveAssetsDir(); const materials = loadMaterials(assets); const WARMUP = 3; function report(label: string, samples: number[]): void { const warm = samples.slice(WARMUP).sort((a, b) => a - b); const pick = (q: number): string => warm[Math.min(warm.length - 1, Math.floor(warm.length * q))].toFixed(2); const mean = warm.reduce((a, b) => a + b, 0) / warm.length; console.log(`${label.padEnd(44)} mean ${mean.toFixed(2)} median ${pick(0.5)} p99 ${pick(0.99)} max ${pick(1)} ms`); } function newGame(): Game { const { definition, terrain } = loadMap(assets, 'gateau-etages', materials); return new Game({ map: definition, terrain, materials, seed: 1, loadCharacter: (id) => loadCharacterStats(assets, id) }); } // 1. Ordinary ops on a cake turning into Swiss cheese: op + detach search + colliders + step. { const { terrain } = loadMap(assets, 'gateau-etages', materials); const world = new World(); const colliders = new TerrainColliders(world, terrain); colliders.rebuildDirty(); const finder = new DetachFinder(terrain); const rng = new Rng(7); const samples: number[] = []; for (let i = 0; i < 1500; i++) { const t = performance.now(); const { changed } = applyOp(terrain, { kind: 'carve', x: rng.int(300, 1700), y: rng.int(300, 900), r: 40, power: 99 }, materials); if (changed) { for (const c of finder.find(changed)) { const { x0, y0, x1, y1 } = c.bounds; applyCells(terrain, 'detach', x0, y0, x1 - x0, y1 - y0, c.mask); } } colliders.rebuildDirty(); world.step(1 / 60); samples.push(performance.now() - t); } report('carve r=40 (op + detach + colliders + step)', samples.slice(200 - WARMUP)); } // 2. One op severing the last link of the top tier (~90k px come loose): the worst single op. { const samples: number[] = []; for (let round = 0; round < WARMUP + 10; round++) { const game = newGame(); for (let x = 760; x <= 1250; x += 12) { game.queueOp({ kind: 'carve', x, y: 462, r: 14, power: 99 }); game.step(); } game.queueOp({ kind: 'carve', x: 1268, y: 462, r: 16, power: 99 }); const t = performance.now(); game.step(); samples.push(performance.now() - t); } report('single op severing a whole tier (full tick)', samples); } // 3. A 44-op cut in a single tick (e.g. several players at once). { const samples: number[] = []; for (let round = 0; round < WARMUP + 10; round++) { const game = newGame(); for (let x = 760; x <= 1278; x += 12) game.queueOp({ kind: 'carve', x, y: 462, r: 14, power: 99 }); const t = performance.now(); game.step(); samples.push(performance.now() - t); } report('44-op cut in one tick (full tick)', samples); }