mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-25 02:27:14 +00:00
perf: cut core-sim GC churn another 36% (75% cumulative) (#4498)
## Summary Round 3 of GC-churn reduction (follow-up to #4494 and #4496). All changes are **behavior-preserving** — final game-state hash unchanged on three seeded runs. ### Changes | Site | Change | Churn target | |---|---|---| | `MiniMapTransformer` | Path upscaling works in pure numeric coordinates and emits main-map `TileRef`s directly, replacing three intermediate `Cell`-object arrays per path (cell path → scaled path → smoothed path → final map). Identical arithmetic, so identical rounding and identical tiles. | ~7.1 GB | | `ShoreCoercingTransformer` | Reused neighbor buffers instead of `neighbors()` arrays; no per-call `{water, original}` objects. Tie-breaking preserved (helpers share the unified N,S,W,E order since #4495). | ~1.5 GB | | `diffPlayerUpdate` | Allocation-free all-equal fast path. Runs per player per tick and usually returns `null` (gold/troops/tiles travel via packed arrays), but previously allocated the diff object + a closure first. Field list matches the diff exactly. | ~2.6 GB | | Large-`Set` iteration | `for..of` over a `Set` allocates an iterator-result object per element — significant on 100k-tile border sets. `calculateClusters`, `calculateBoundingBox` (indexed fast path for arrays too) and `getAttackFrontTiles` (also dropped its `neighbors()` arrays) now use `Set.forEach`. | ~3.6 GB | ### Results (Giant World Map, 400 bots, 12,000 ticks, seed `perf-default`) | Metric | Before | After | vs. original (pre-#4494) | |---|---|---|---| | Sampled allocations (incl. collected) | 37.8 GB | **24.1 GB (−36%)** | 97.7 GB (**−75%**) | | Ticks/sec | 82 | **88** | 66 (+33%) | | Mean / p99 tick | 12.2 / 36.0 ms | 11.3 / 34.8 ms | 15.2 / 49.9 ms | | Peak heap | 762 MB | 529 MB | 758 MB | ## Determinism Final hash unchanged on all three reference runs: - Giant World Map 12,000 ticks: `57830793797434300` ✓ - Giant World Map 2,000 ticks: `55125379638382860` ✓ - World 1,800 ticks: `32337437717390864` ✓ ## Test plan - [x] Full suite green (1,906 tests; the `getAttackFrontTiles` test stub gained a `neighbors4` implementation to match the real interface) - [x] Hash equality on 3 seeded headless runs (2 maps) - [x] Before/after 20-min GC benchmarks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -339,18 +339,23 @@ export class PlayerExecution implements Execution {
|
||||
|
||||
const clusters: TileRef[][] = [];
|
||||
|
||||
for (const startTile of borderTiles) {
|
||||
if (visited[startTile] === currentGen) continue;
|
||||
// Set.forEach instead of for..of: iterating a large Set allocates an
|
||||
// iterator-result object per element, and border sets can be huge.
|
||||
const neighborFn = (tile: TileRef, cb: (neighbor: TileRef) => void) =>
|
||||
this.mg.forEachNeighborWithDiag(tile, cb);
|
||||
const includeFn = (tile: TileRef) => borderTiles.has(tile);
|
||||
borderTiles.forEach((startTile) => {
|
||||
if (visited[startTile] === currentGen) return;
|
||||
|
||||
const cluster = this.floodFillWithGen(
|
||||
currentGen,
|
||||
visited,
|
||||
[startTile],
|
||||
(tile, cb) => this.mg.forEachNeighborWithDiag(tile, cb),
|
||||
(tile) => borderTiles.has(tile),
|
||||
neighborFn,
|
||||
includeFn,
|
||||
);
|
||||
clusters.push(cluster);
|
||||
}
|
||||
});
|
||||
return clusters;
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,9 @@ const UNDER_ATTACK_THREAT_RATIO = 0.35;
|
||||
*/
|
||||
const DEFENSE_POST_RATIO_PER_POST = 0.4;
|
||||
|
||||
// Reusable neighbor buffer for hot loops; the simulation is single-threaded.
|
||||
const NEIGHBOR_SCRATCH: TileRef[] = [0, 0, 0, 0];
|
||||
|
||||
export class NationStructureBehavior {
|
||||
private reachableStationsCache: Array<{
|
||||
tile: TileRef;
|
||||
@@ -256,16 +259,21 @@ export class NationStructureBehavior {
|
||||
const attackerSet = new Set(landAttacks.map((a) => a.attacker()));
|
||||
if (attackerSet.size === 0) return [];
|
||||
|
||||
// Set.forEach + a reused neighbor buffer: border sets are huge, and
|
||||
// for..of over a Set allocates an iterator-result object per element.
|
||||
// "Any neighbor is an attacker" is order-insensitive.
|
||||
const frontTiles: TileRef[] = [];
|
||||
outer: for (const borderTile of player.borderTiles()) {
|
||||
for (const neighbor of game.neighbors(borderTile)) {
|
||||
const owner = game.owner(neighbor);
|
||||
const nbuf = NEIGHBOR_SCRATCH;
|
||||
player.borderTiles().forEach((borderTile) => {
|
||||
const n = game.neighbors4(borderTile, nbuf);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const owner = game.owner(nbuf[i]);
|
||||
if (attackerSet.has(owner as Player)) {
|
||||
frontTiles.push(borderTile);
|
||||
continue outer;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return frontTiles;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user