mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 10:53:31 +00:00
perf(core): speed up packedTileUpdates (Uint32 pairs, no tile wrappers) (#3255)
## Description Reduces CPU + GC pressure from tile update serialization. **What changed** - Switched `packedTileUpdates` from `BigUint64Array` (BigInt packing) to `Uint32Array` `[tileRef, state]` pairs, updating `GameView` ingestion. - Updated tile state to use `GameMap.tileState(tile)` and `GameMap.updateTile(tile, state)`. - Removed per-tile `GameUpdateType.Tile` wrapper allocations by recording raw `(tile, state)` pairs in `GameImpl` and draining them via `drainPackedTileUpdates()` in `GameRunner`. **Why it’s faster** - Avoids BigInt and pack/unpack. - Avoids per-tile object allocations. **Compatibility** - Wire format change: `packedTileUpdates` is now `Uint32Array` pairs instead of `BigUint64Array`. ## 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:
+2
-12
@@ -22,11 +22,7 @@ import {
|
||||
import { createGame } from "./game/GameImpl";
|
||||
import { TileRef } from "./game/GameMap";
|
||||
import { GameMapLoader } from "./game/GameMapLoader";
|
||||
import {
|
||||
ErrorUpdate,
|
||||
GameUpdateType,
|
||||
GameUpdateViewData,
|
||||
} from "./game/GameUpdates";
|
||||
import { ErrorUpdate, GameUpdateViewData } from "./game/GameUpdates";
|
||||
import { createNationsForGame } from "./game/NationCreation";
|
||||
import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader";
|
||||
import { PseudoRandom } from "./PseudoRandom";
|
||||
@@ -172,13 +168,7 @@ export class GameRunner {
|
||||
});
|
||||
}
|
||||
|
||||
// Many tiles are updated to pack it into an array
|
||||
const tileUpdates = updates[GameUpdateType.Tile];
|
||||
const packedTileUpdates = new BigUint64Array(tileUpdates.length);
|
||||
for (let i = 0; i < tileUpdates.length; i++) {
|
||||
packedTileUpdates[i] = tileUpdates[i].update;
|
||||
}
|
||||
updates[GameUpdateType.Tile] = [];
|
||||
const packedTileUpdates = this.game.drainPackedTileUpdates();
|
||||
|
||||
this.callBack({
|
||||
tick: this.game.ticks(),
|
||||
|
||||
@@ -766,6 +766,7 @@ export interface Game extends GameMap {
|
||||
ticks(): Tick;
|
||||
inSpawnPhase(): boolean;
|
||||
executeNextTick(): GameUpdates;
|
||||
drainPackedTileUpdates(): Uint32Array;
|
||||
setWinner(winner: Player | Team, allPlayersStats: AllPlayersStats): void;
|
||||
getWinner(): Player | Team | null;
|
||||
config(): Config;
|
||||
|
||||
+24
-17
@@ -39,7 +39,7 @@ import {
|
||||
UnitInfo,
|
||||
UnitType,
|
||||
} from "./Game";
|
||||
import { GameMap, TileRef, TileUpdate } from "./GameMap";
|
||||
import { GameMap, TileRef } from "./GameMap";
|
||||
import { GameUpdate, GameUpdateType } from "./GameUpdates";
|
||||
import { PlayerImpl } from "./PlayerImpl";
|
||||
import { RailNetwork } from "./RailNetwork";
|
||||
@@ -83,6 +83,7 @@ export class GameImpl implements Game {
|
||||
private _nextUnitID = 1;
|
||||
|
||||
private updates: GameUpdates = createGameUpdatesMap();
|
||||
private tileUpdatePairs: number[] = [];
|
||||
private unitGrid: UnitGrid;
|
||||
|
||||
private playerTeams: Team[];
|
||||
@@ -248,10 +249,7 @@ export class GameImpl implements Game {
|
||||
return;
|
||||
}
|
||||
this._map.setFallout(tile, value);
|
||||
this.addUpdate({
|
||||
type: GameUpdateType.Tile,
|
||||
update: this.toTileUpdate(tile),
|
||||
});
|
||||
this.recordTileUpdate(tile);
|
||||
}
|
||||
|
||||
units(...types: UnitType[]): Unit[] {
|
||||
@@ -379,6 +377,7 @@ export class GameImpl implements Game {
|
||||
|
||||
executeNextTick(): GameUpdates {
|
||||
this.updates = createGameUpdatesMap();
|
||||
this.tileUpdatePairs.length = 0;
|
||||
this.execs.forEach((e) => {
|
||||
if (
|
||||
(!this.inSpawnPhase() || e.activeDuringSpawnPhase()) &&
|
||||
@@ -417,6 +416,20 @@ export class GameImpl implements Game {
|
||||
return this.updates;
|
||||
}
|
||||
|
||||
private recordTileUpdate(tile: TileRef): void {
|
||||
this.tileUpdatePairs.push(tile, this._map.tileState(tile));
|
||||
}
|
||||
|
||||
drainPackedTileUpdates(): Uint32Array {
|
||||
const pairs = this.tileUpdatePairs;
|
||||
const packed = new Uint32Array(pairs.length);
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
packed[i] = pairs[i];
|
||||
}
|
||||
pairs.length = 0;
|
||||
return packed;
|
||||
}
|
||||
|
||||
private hash(): number {
|
||||
let hash = 1;
|
||||
this._players.forEach((p) => {
|
||||
@@ -588,10 +601,7 @@ export class GameImpl implements Game {
|
||||
owner._lastTileChange = this._ticks;
|
||||
this.updateBorders(tile);
|
||||
this._map.setFallout(tile, false);
|
||||
this.addUpdate({
|
||||
type: GameUpdateType.Tile,
|
||||
update: this.toTileUpdate(tile),
|
||||
});
|
||||
this.recordTileUpdate(tile);
|
||||
}
|
||||
|
||||
relinquish(tile: TileRef) {
|
||||
@@ -609,10 +619,7 @@ export class GameImpl implements Game {
|
||||
|
||||
this._map.setOwnerID(tile, 0);
|
||||
this.updateBorders(tile);
|
||||
this.addUpdate({
|
||||
type: GameUpdateType.Tile,
|
||||
update: this.toTileUpdate(tile),
|
||||
});
|
||||
this.recordTileUpdate(tile);
|
||||
}
|
||||
|
||||
private updateBorders(tile: TileRef) {
|
||||
@@ -1017,11 +1024,11 @@ export class GameImpl implements Game {
|
||||
): Set<TileRef> {
|
||||
return this._map.bfs(tile, filter);
|
||||
}
|
||||
toTileUpdate(tile: TileRef): bigint {
|
||||
return this._map.toTileUpdate(tile);
|
||||
tileState(tile: TileRef): number {
|
||||
return this._map.tileState(tile);
|
||||
}
|
||||
updateTile(tu: TileUpdate): TileRef {
|
||||
return this._map.updateTile(tu);
|
||||
updateTile(tile: TileRef, state: number): void {
|
||||
this._map.updateTile(tile, state);
|
||||
}
|
||||
numTilesWithFallout(): number {
|
||||
return this._map.numTilesWithFallout();
|
||||
|
||||
+20
-18
@@ -1,7 +1,6 @@
|
||||
import { Cell, TerrainType } from "./Game";
|
||||
|
||||
export type TileRef = number;
|
||||
export type TileUpdate = bigint;
|
||||
|
||||
export interface GameMap {
|
||||
ref(x: number, y: number): TileRef;
|
||||
@@ -49,8 +48,20 @@ export interface GameMap {
|
||||
filter: (gm: GameMap, tile: TileRef) => boolean,
|
||||
): Set<TileRef>;
|
||||
|
||||
toTileUpdate(tile: TileRef): bigint;
|
||||
updateTile(tu: TileUpdate): TileRef;
|
||||
/**
|
||||
* Returns the packed per-tile state as an unsigned 16-bit value (`0..65535`).
|
||||
*
|
||||
* Backed by a `Uint16Array` in `GameMapImpl`, so callers must treat this as `uint16`.
|
||||
*/
|
||||
tileState(tile: TileRef): number;
|
||||
|
||||
/**
|
||||
* Applies a packed per-tile state value.
|
||||
*
|
||||
* `state` must be an unsigned 16-bit value (`0..65535`). Implementations may
|
||||
* store this in a `Uint16Array` and will truncate higher bits if provided.
|
||||
*/
|
||||
updateTile(tile: TileRef, state: number): void;
|
||||
|
||||
numTilesWithFallout(): number;
|
||||
}
|
||||
@@ -342,29 +353,20 @@ export class GameMapImpl implements GameMap {
|
||||
return seen;
|
||||
}
|
||||
|
||||
toTileUpdate(tile: TileRef): bigint {
|
||||
// Pack the tile reference and state into a bigint
|
||||
// Format: [32 bits for tile reference][16 bits for state]
|
||||
return (BigInt(tile) << 16n) | BigInt(this.state[tile]);
|
||||
tileState(tile: TileRef): number {
|
||||
return this.state[tile];
|
||||
}
|
||||
|
||||
updateTile(tu: TileUpdate): TileRef {
|
||||
// Extract tile reference and state from the TileUpdate
|
||||
// Last 16 bits are state, rest is tile reference
|
||||
const tileRef = Number(tu >> 16n);
|
||||
const state = Number(tu & 0xffffn);
|
||||
|
||||
const existingFallout = this.hasFallout(tileRef);
|
||||
this.state[tileRef] = state;
|
||||
const newFallout = this.hasFallout(tileRef);
|
||||
updateTile(tile: TileRef, state: number): void {
|
||||
const existingFallout = this.hasFallout(tile);
|
||||
this.state[tile] = state;
|
||||
const newFallout = this.hasFallout(tile);
|
||||
if (existingFallout && !newFallout) {
|
||||
this._numTilesWithFallout--;
|
||||
}
|
||||
if (!existingFallout && newFallout) {
|
||||
this._numTilesWithFallout++;
|
||||
}
|
||||
|
||||
return tileRef;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,18 @@ import {
|
||||
TrainType,
|
||||
UnitType,
|
||||
} from "./Game";
|
||||
import { TileRef, TileUpdate } from "./GameMap";
|
||||
import { TileRef } from "./GameMap";
|
||||
|
||||
export interface GameUpdateViewData {
|
||||
tick: number;
|
||||
updates: GameUpdates;
|
||||
packedTileUpdates: BigUint64Array;
|
||||
/**
|
||||
* Packed tile updates as `[tileRef, state]` uint32 pairs.
|
||||
*
|
||||
* `tileRef` is a `TileRef` (fits in uint32), and `state` is the packed per-tile
|
||||
* state (`uint16`) stored in a `uint32` lane.
|
||||
*/
|
||||
packedTileUpdates: Uint32Array;
|
||||
playerNameViewData: Record<string, NameViewData>;
|
||||
tickExecutionDuration?: number;
|
||||
pendingTurns?: number;
|
||||
@@ -29,6 +35,7 @@ export interface ErrorUpdate {
|
||||
}
|
||||
|
||||
export enum GameUpdateType {
|
||||
// Tile updates are delivered via `packedTileUpdates` on the outer GameUpdateViewData.
|
||||
Tile,
|
||||
Unit,
|
||||
Player,
|
||||
@@ -54,7 +61,6 @@ export enum GameUpdateType {
|
||||
}
|
||||
|
||||
export type GameUpdate =
|
||||
| TileUpdateWrapper
|
||||
| UnitUpdate
|
||||
| PlayerUpdate
|
||||
| AllianceRequestUpdate
|
||||
@@ -112,11 +118,6 @@ export interface ConquestUpdate {
|
||||
gold: Gold;
|
||||
}
|
||||
|
||||
export interface TileUpdateWrapper {
|
||||
type: GameUpdateType.Tile;
|
||||
update: TileUpdate;
|
||||
}
|
||||
|
||||
export interface UnitUpdate {
|
||||
type: GameUpdateType.Unit;
|
||||
unitType: UnitType;
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
UnitInfo,
|
||||
UnitType,
|
||||
} from "./Game";
|
||||
import { GameMap, TileRef, TileUpdate } from "./GameMap";
|
||||
import { GameMap, TileRef } from "./GameMap";
|
||||
import {
|
||||
AllianceView,
|
||||
AttackUpdate,
|
||||
@@ -648,9 +648,13 @@ export class GameView implements GameMap {
|
||||
this.lastUpdate = gu;
|
||||
|
||||
this.updatedTiles = [];
|
||||
this.lastUpdate.packedTileUpdates.forEach((tu) => {
|
||||
this.updatedTiles.push(this.updateTile(tu));
|
||||
});
|
||||
const packed = this.lastUpdate.packedTileUpdates;
|
||||
for (let i = 0; i + 1 < packed.length; i += 2) {
|
||||
const tile = packed[i];
|
||||
const state = packed[i + 1];
|
||||
this.updateTile(tile, state);
|
||||
this.updatedTiles.push(tile);
|
||||
}
|
||||
|
||||
if (gu.updates === null) {
|
||||
throw new Error("lastUpdate.updates not initialized");
|
||||
@@ -949,11 +953,11 @@ export class GameView implements GameMap {
|
||||
): Set<TileRef> {
|
||||
return this._map.bfs(tile, filter);
|
||||
}
|
||||
toTileUpdate(tile: TileRef): bigint {
|
||||
return this._map.toTileUpdate(tile);
|
||||
tileState(tile: TileRef): number {
|
||||
return this._map.tileState(tile);
|
||||
}
|
||||
updateTile(tu: TileUpdate): TileRef {
|
||||
return this._map.updateTile(tu);
|
||||
updateTile(tile: TileRef, state: number): void {
|
||||
this._map.updateTile(tile, state);
|
||||
}
|
||||
numTilesWithFallout(): number {
|
||||
return this._map.numTilesWithFallout();
|
||||
|
||||
Reference in New Issue
Block a user