mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 22:05:21 +00:00
perf: Optimize cluster calculation with DFS and zero-allocation patterns
Replace BFS with DFS and eliminate GC pressure in calculateClusters() hot path: - Switch from O(N) queue.shift() to O(1) stack.pop() operations - Replace Set.has()/Set.add() with Uint8Array bitfield - Add reusable buffer management to avoid repeated allocations - Implement callback-based neighbor iteration to eliminate array allocations - Add forEachNeighborWithDiag() method to Game interface and GameImpl - Remove now unused GameImpl import from PlayerExecution
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { Config } from "../configuration/Config";
|
||||
import { Execution, Game, Player, UnitType } from "../game/Game";
|
||||
import { GameImpl } from "../game/GameImpl";
|
||||
import { GameMap, TileRef } from "../game/GameMap";
|
||||
import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";
|
||||
|
||||
@@ -11,8 +10,11 @@ export class PlayerExecution implements Execution {
|
||||
private lastCalc = 0;
|
||||
private mg: Game;
|
||||
private active = true;
|
||||
private _visitedBuffer: Uint8Array;
|
||||
|
||||
constructor(private player: Player) {}
|
||||
constructor(private player: Player) {
|
||||
this._visitedBuffer = new Uint8Array(0); // Initialize empty buffer
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
@@ -259,31 +261,45 @@ export class PlayerExecution implements Execution {
|
||||
}
|
||||
|
||||
private calculateClusters(): Set<TileRef>[] {
|
||||
const seen = new Set<TileRef>();
|
||||
const border = this.player.borderTiles();
|
||||
const borderTiles = this.player.borderTiles();
|
||||
if (borderTiles.size === 0) return [];
|
||||
|
||||
// Ensure buffer is large enough
|
||||
const mapSize = this.mg.width() * this.mg.height();
|
||||
if (!this._visitedBuffer || this._visitedBuffer.length < mapSize) {
|
||||
this._visitedBuffer = new Uint8Array(mapSize);
|
||||
} else {
|
||||
// Fast clear (much faster than creating a new Set)
|
||||
this._visitedBuffer.fill(0);
|
||||
}
|
||||
|
||||
const clusters: Set<TileRef>[] = [];
|
||||
for (const tile of border) {
|
||||
if (seen.has(tile)) {
|
||||
continue;
|
||||
}
|
||||
const stack: TileRef[] = []; // Reusable stack
|
||||
|
||||
const cluster = new Set<TileRef>();
|
||||
const queue: TileRef[] = [tile];
|
||||
seen.add(tile);
|
||||
while (queue.length > 0) {
|
||||
const curr = queue.shift();
|
||||
if (curr === undefined) throw new Error("curr is undefined");
|
||||
cluster.add(curr);
|
||||
for (const startTile of borderTiles) {
|
||||
// FAST: Array access instead of Set.has()
|
||||
if (this._visitedBuffer[startTile] === 1) continue;
|
||||
|
||||
const neighbors = (this.mg as GameImpl).neighborsWithDiag(curr);
|
||||
for (const neighbor of neighbors) {
|
||||
if (border.has(neighbor) && !seen.has(neighbor)) {
|
||||
queue.push(neighbor);
|
||||
seen.add(neighbor);
|
||||
const currentCluster = new Set<TileRef>();
|
||||
stack.push(startTile);
|
||||
this._visitedBuffer[startTile] = 1;
|
||||
|
||||
while (stack.length > 0) {
|
||||
const tile = stack.pop()!;
|
||||
currentCluster.add(tile);
|
||||
|
||||
//Use callback to avoid creating a 'neighbors' Array
|
||||
this.mg.forEachNeighborWithDiag(tile, (neighbor) => {
|
||||
if (
|
||||
borderTiles.has(neighbor) &&
|
||||
this._visitedBuffer[neighbor] === 0
|
||||
) {
|
||||
stack.push(neighbor);
|
||||
this._visitedBuffer[neighbor] = 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
clusters.push(cluster);
|
||||
clusters.push(currentCluster);
|
||||
}
|
||||
return clusters;
|
||||
}
|
||||
|
||||
@@ -668,6 +668,13 @@ export interface Game extends GameMap {
|
||||
map(): GameMap;
|
||||
miniMap(): GameMap;
|
||||
forEachTile(fn: (tile: TileRef) => void): void;
|
||||
// Zero-allocation neighbor iteration for performance-critical cluster calculation
|
||||
// Alternative to neighborsWithDiag() that returns arrays
|
||||
// Avoids creating intermediate arrays and uses a callback for better performance
|
||||
forEachNeighborWithDiag(
|
||||
tile: TileRef,
|
||||
callback: (neighbor: TileRef) => void,
|
||||
): void;
|
||||
|
||||
// Player Management
|
||||
player(id: PlayerID): Player;
|
||||
|
||||
@@ -517,6 +517,30 @@ export class GameImpl implements Game {
|
||||
return ns;
|
||||
}
|
||||
|
||||
// Zero-allocation neighbor iteration for performance-critical code
|
||||
forEachNeighborWithDiag(
|
||||
tile: TileRef,
|
||||
callback: (neighbor: TileRef) => void,
|
||||
): void {
|
||||
const x = this.x(tile);
|
||||
const y = this.y(tile);
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
if (dx === 0 && dy === 0) continue; // Skip the center tile
|
||||
const newX = x + dx;
|
||||
const newY = y + dy;
|
||||
if (
|
||||
newX >= 0 &&
|
||||
newX < this._width &&
|
||||
newY >= 0 &&
|
||||
newY < this._height
|
||||
) {
|
||||
callback(this._map.ref(newX, newY));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conquer(owner: PlayerImpl, tile: TileRef): void {
|
||||
if (!this.isLand(tile)) {
|
||||
throw Error(`cannot conquer water`);
|
||||
|
||||
Reference in New Issue
Block a user