mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-07 11:09:34 +00:00
Perf clusters (#3127)
## Description: This PR reduces server/client tick CPU spent on territory cluster maintenance by: - Cutting redundant work in `PlayerExecution.removeClusters()` (largest-cluster bounding box reuse, fewer allocations in `isSurrounded()` and `removeCluster()`). - Making `calculateBoundingBox()` allocation-free per tile by switching from `gm.cell(tile)` to `gm.x(tile)`/`gm.y(tile)` (it now only allocates the two result `Cell`s, instead of 1 per tile). ## Commits - `51de0a1b` core: reduce PlayerExecution cluster overhead - `6d9d85c5` core: avoid Cell allocations in PlayerExecution isSurrounded - `346f6a8c` core(util): speed up calculateBoundingBox by avoiding Cell allocations ## Notes - This PR is intended to be behavior-preserving; changes are limited to hot-path micro-optimizations. - Follow-up opportunity: `calculateClusters`/flood-fill is now the top hotspot; further wins likely come from reducing traversal work or caching. ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file - [ ] I have added relevant tests to the test directory - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: DISCORD_USERNAME
This commit is contained in:
+8
-7
@@ -80,13 +80,14 @@ export function calculateBoundingBox(
|
|||||||
maxX = -Infinity,
|
maxX = -Infinity,
|
||||||
maxY = -Infinity;
|
maxY = -Infinity;
|
||||||
|
|
||||||
borderTiles.forEach((tile: TileRef) => {
|
for (const tile of borderTiles) {
|
||||||
const cell = gm.cell(tile);
|
const x = gm.x(tile);
|
||||||
minX = Math.min(minX, cell.x);
|
const y = gm.y(tile);
|
||||||
minY = Math.min(minY, cell.y);
|
minX = Math.min(minX, x);
|
||||||
maxX = Math.max(maxX, cell.x);
|
minY = Math.min(minY, y);
|
||||||
maxY = Math.max(maxY, cell.y);
|
maxX = Math.max(maxX, x);
|
||||||
});
|
maxY = Math.max(maxY, y);
|
||||||
|
}
|
||||||
|
|
||||||
return { min: new Cell(minX, minY), max: new Cell(maxX, maxY) };
|
return { min: new Cell(minX, minY), max: new Cell(maxX, maxY) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Config } from "../configuration/Config";
|
import { Config } from "../configuration/Config";
|
||||||
import { Execution, Game, Player, UnitType } from "../game/Game";
|
import { Cell, Execution, Game, Player, UnitType } from "../game/Game";
|
||||||
import { TileRef } from "../game/GameMap";
|
import { TileRef } from "../game/GameMap";
|
||||||
import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";
|
import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util";
|
||||||
|
|
||||||
@@ -139,11 +139,12 @@ export class PlayerExecution implements Execution {
|
|||||||
const largestCluster = clusters[largestIndex];
|
const largestCluster = clusters[largestIndex];
|
||||||
if (largestCluster === undefined) throw new Error("No clusters");
|
if (largestCluster === undefined) throw new Error("No clusters");
|
||||||
|
|
||||||
this.player.largestClusterBoundingBox = calculateBoundingBox(
|
const largestClusterBox = calculateBoundingBox(this.mg, largestCluster);
|
||||||
this.mg,
|
this.player.largestClusterBoundingBox = largestClusterBox;
|
||||||
|
const surroundedBy = this.surroundedBySamePlayer(
|
||||||
largestCluster,
|
largestCluster,
|
||||||
|
largestClusterBox,
|
||||||
);
|
);
|
||||||
const surroundedBy = this.surroundedBySamePlayer(largestCluster);
|
|
||||||
if (surroundedBy && !surroundedBy.isFriendly(this.player)) {
|
if (surroundedBy && !surroundedBy.isFriendly(this.player)) {
|
||||||
this.removeCluster(largestCluster);
|
this.removeCluster(largestCluster);
|
||||||
}
|
}
|
||||||
@@ -158,7 +159,10 @@ export class PlayerExecution implements Execution {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private surroundedBySamePlayer(cluster: Set<TileRef>): false | Player {
|
private surroundedBySamePlayer(
|
||||||
|
cluster: Set<TileRef>,
|
||||||
|
clusterBox: { min: Cell; max: Cell },
|
||||||
|
): false | Player {
|
||||||
const enemies = new Set<number>();
|
const enemies = new Set<number>();
|
||||||
for (const tile of cluster) {
|
for (const tile of cluster) {
|
||||||
let hasUnownedNeighbor = false;
|
let hasUnownedNeighbor = false;
|
||||||
@@ -187,7 +191,6 @@ export class PlayerExecution implements Execution {
|
|||||||
}
|
}
|
||||||
const enemy = this.mg.playerBySmallID(Array.from(enemies)[0]) as Player;
|
const enemy = this.mg.playerBySmallID(Array.from(enemies)[0]) as Player;
|
||||||
const enemyBox = calculateBoundingBox(this.mg, enemy.borderTiles());
|
const enemyBox = calculateBoundingBox(this.mg, enemy.borderTiles());
|
||||||
const clusterBox = calculateBoundingBox(this.mg, cluster);
|
|
||||||
if (inscribed(enemyBox, clusterBox)) {
|
if (inscribed(enemyBox, clusterBox)) {
|
||||||
return enemy;
|
return enemy;
|
||||||
}
|
}
|
||||||
@@ -195,7 +198,11 @@ export class PlayerExecution implements Execution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isSurrounded(cluster: Set<TileRef>): boolean {
|
private isSurrounded(cluster: Set<TileRef>): boolean {
|
||||||
const enemyTiles = new Set<TileRef>();
|
let hasEnemy = false;
|
||||||
|
let minX = Infinity,
|
||||||
|
minY = Infinity,
|
||||||
|
maxX = -Infinity,
|
||||||
|
maxY = -Infinity;
|
||||||
for (const tr of cluster) {
|
for (const tr of cluster) {
|
||||||
if (this.mg.isShore(tr) || this.mg.isOnEdgeOfMap(tr)) {
|
if (this.mg.isShore(tr) || this.mg.isOnEdgeOfMap(tr)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -203,27 +210,31 @@ export class PlayerExecution implements Execution {
|
|||||||
this.mg.forEachNeighbor(tr, (n) => {
|
this.mg.forEachNeighbor(tr, (n) => {
|
||||||
const owner = this.mg.owner(n);
|
const owner = this.mg.owner(n);
|
||||||
if (owner.isPlayer() && this.mg.ownerID(n) !== this.player.smallID()) {
|
if (owner.isPlayer() && this.mg.ownerID(n) !== this.player.smallID()) {
|
||||||
enemyTiles.add(n);
|
hasEnemy = true;
|
||||||
|
const x = this.mg.x(n);
|
||||||
|
const y = this.mg.y(n);
|
||||||
|
minX = Math.min(minX, x);
|
||||||
|
minY = Math.min(minY, y);
|
||||||
|
maxX = Math.max(maxX, x);
|
||||||
|
maxY = Math.max(maxY, y);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (enemyTiles.size === 0) {
|
if (!hasEnemy) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const enemyBox = calculateBoundingBox(this.mg, enemyTiles);
|
|
||||||
const clusterBox = calculateBoundingBox(this.mg, cluster);
|
const clusterBox = calculateBoundingBox(this.mg, cluster);
|
||||||
|
const enemyBox = { min: new Cell(minX, minY), max: new Cell(maxX, maxY) };
|
||||||
return inscribed(enemyBox, clusterBox);
|
return inscribed(enemyBox, clusterBox);
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeCluster(cluster: Set<TileRef>) {
|
private removeCluster(cluster: Set<TileRef>) {
|
||||||
if (
|
for (const t of cluster) {
|
||||||
Array.from(cluster).some(
|
if (this.mg?.ownerID(t) !== this.player?.smallID()) {
|
||||||
(t) => this.mg?.ownerID(t) !== this.player?.smallID(),
|
// Other removeCluster operations could change tile owners,
|
||||||
)
|
// so double check.
|
||||||
) {
|
return;
|
||||||
// Other removeCluster operations could change tile owners,
|
}
|
||||||
// so double check.
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const capturing = this.getCapturingPlayer(cluster);
|
const capturing = this.getCapturingPlayer(cluster);
|
||||||
|
|||||||
Reference in New Issue
Block a user