clay-borg/research/CB-RES-0001-harness/boardgame-io/bench.js

122 lines
4.4 KiB
JavaScript

// Baseline harness for CB-RES-0001 — boardgame.io v0.50.2, headless client.
// Measures: (1) throughput of applying moves to authoritative state,
// (2) determinism across two seeded runs, (3) commit/reveal-style stage flow.
// Fidelity note: synthetic GROUND-shaped workload (3 players, relationship
// map, per-player hands, commit list), not a real GROUND implementation.
// Run: node bench.js [numMoves]
const { Client } = require('boardgame.io/client');
const { Stage } = require('boardgame.io/core');
const crypto = require('crypto');
const NUM_MOVES = parseInt(process.argv[2] || '100000', 10);
const DISABLE_UNDO = process.argv.includes('--disable-undo');
const SINGLE_RUN = process.argv.includes('--single-run'); // isolated memory measurement
const NUM_PLAYERS = 3;
const SyntheticGround = {
name: 'synthetic-ground',
setup: ({ ctx }) => ({
round: 0,
relationships: {}, // "a->b" -> {kind, strength}
hands: Object.fromEntries(
Array.from({ length: ctx.numPlayers }, (_, i) => [String(i), Array.from({ length: 7 }, (_, c) => c)])
),
committed: {},
revealed: [],
score: Object.fromEntries(Array.from({ length: ctx.numPlayers }, (_, i) => [String(i), 0])),
}),
moves: {
commitAction: ({ G, playerID, random }, targetOffset) => {
const target = String((Number(playerID) + targetOffset) % NUM_PLAYERS);
const card = G.hands[playerID].length > 0 ? G.hands[playerID][0] : random.D6();
G.committed[playerID] = { target, card };
},
resolveAll: ({ G, random }) => {
const order = Object.keys(G.committed).sort();
for (const p of order) {
const { target, card } = G.committed[p];
const key = `${p}->${target}`;
const rel = G.relationships[key] || { kind: 'rivalry', strength: 0 };
rel.strength = (rel.strength + card + random.D6()) % 10;
G.relationships[key] = rel;
G.score[p] += rel.strength;
G.revealed.push({ p, target, card });
if (G.revealed.length > 50) G.revealed.shift();
}
G.committed = {};
G.round += 1;
},
},
turn: { activePlayers: { all: Stage.NULL } },
};
function makeClient(seed) {
return Client({
game: { ...SyntheticGround, seed, disableUndo: DISABLE_UNDO },
numPlayers: NUM_PLAYERS,
playerID: '0',
});
}
function stateHash(client) {
const { G } = client.getState();
return crypto.createHash('sha256').update(JSON.stringify(G)).digest('hex');
}
function run(seed, numMoves, collectHash) {
const client = makeClient(seed);
// Drive all players through commit windows via events/moves.
// Each "round" = NUM_PLAYERS commitAction moves + 1 resolveAll.
const rounds = Math.floor(numMoves / (NUM_PLAYERS + 1));
const t0 = process.hrtime.bigint();
for (let r = 0; r < rounds; r++) {
for (let p = 0; p < NUM_PLAYERS; p++) {
client.updatePlayerID(String(p));
client.moves.commitAction(1 + (r % (NUM_PLAYERS - 1)));
}
client.updatePlayerID('0');
client.moves.resolveAll();
}
const t1 = process.hrtime.bigint();
const elapsedMs = Number(t1 - t0) / 1e6;
const applied = rounds * (NUM_PLAYERS + 1);
return {
applied,
elapsedMs,
movesPerSec: Math.round(applied / (elapsedMs / 1000)),
hash: collectHash ? stateHash(client) : null,
finalRound: client.getState().G.round,
};
}
// --- throughput ---
if (!SINGLE_RUN) run(42, Math.min(NUM_MOVES, 5000), false); // warmup/JIT
const main = run(42, NUM_MOVES, true);
const memAfterMain = process.memoryUsage();
// --- determinism: same seed twice, different seed once ---
const detA = SINGLE_RUN ? {hash: 'skipped'} : run(7, 4000, true);
const detB = SINGLE_RUN ? {hash: 'skipped'} : run(7, 4000, true);
const detC = SINGLE_RUN ? {hash: 'x'} : run(8, 4000, true);
// --- memory ---
const mem = process.memoryUsage();
const result = {
library: 'boardgame.io@0.50.2',
node: process.version,
workload: `synthetic-ground ${NUM_PLAYERS}p commit/reveal`,
applied_moves: main.applied,
elapsed_ms: Math.round(main.elapsedMs),
moves_per_sec: main.movesPerSec,
final_round: main.finalRound,
determinism_same_seed: detA.hash === detB.hash,
determinism_diff_seed_differs: detA.hash !== detC.hash,
rss_mb: Math.round(mem.rss / 1048576),
heap_mb: Math.round(mem.heapUsed / 1048576),
rss_after_main_mb: Math.round(memAfterMain.rss / 1048576),
disable_undo: DISABLE_UNDO,
single_run: SINGLE_RUN,
};
console.log(JSON.stringify(result, null, 2));