122 lines
4.4 KiB
JavaScript
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));
|