mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-28 17:17:58 +00:00
Batch worker updates in client catch-up mode to reduce render cost
- Refactor worker update handling into processPendingUpdates so multiple GameUpdateViewData objects are batched per frame. - Combine all tick updates in a batch into a single GameUpdateViewData before applying it to GameView, while still running per-tick side effects (turnComplete, hashes, backlog metrics, win saving). - Ensure layers using updatesSinceLastTick and recentlyUpdatedTiles see all events in a batch, fixing visual artifacts during fast-forward resync.
This commit is contained in:
+131
-52
@@ -12,7 +12,7 @@ import {
|
|||||||
import { createPartialGameRecord, replacer } from "../core/Util";
|
import { createPartialGameRecord, replacer } from "../core/Util";
|
||||||
import { ServerConfig } from "../core/configuration/Config";
|
import { ServerConfig } from "../core/configuration/Config";
|
||||||
import { getConfig } from "../core/configuration/ConfigLoader";
|
import { getConfig } from "../core/configuration/ConfigLoader";
|
||||||
import { PlayerActions, UnitType } from "../core/game/Game";
|
import { GameUpdates, PlayerActions, UnitType } from "../core/game/Game";
|
||||||
import { TileRef } from "../core/game/GameMap";
|
import { TileRef } from "../core/game/GameMap";
|
||||||
import { GameMapLoader } from "../core/game/GameMapLoader";
|
import { GameMapLoader } from "../core/game/GameMapLoader";
|
||||||
import {
|
import {
|
||||||
@@ -218,6 +218,9 @@ export class ClientGameRunner {
|
|||||||
private readonly CATCH_UP_EXIT_BACKLOG = 20; // turns behind to exit catch-up
|
private readonly CATCH_UP_EXIT_BACKLOG = 20; // turns behind to exit catch-up
|
||||||
private readonly CATCH_UP_HEARTBEATS_PER_FRAME = 5;
|
private readonly CATCH_UP_HEARTBEATS_PER_FRAME = 5;
|
||||||
|
|
||||||
|
private pendingUpdates: GameUpdateViewData[] = [];
|
||||||
|
private isProcessingUpdates = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private lobby: LobbyConfig,
|
private lobby: LobbyConfig,
|
||||||
private eventBus: EventBus,
|
private eventBus: EventBus,
|
||||||
@@ -302,57 +305,9 @@ export class ClientGameRunner {
|
|||||||
this.stop();
|
this.stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.transport.turnComplete();
|
this.pendingUpdates.push(gu);
|
||||||
gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => {
|
if (!this.catchUpMode) {
|
||||||
this.eventBus.emit(new SendHashEvent(hu.tick, hu.hash));
|
this.processPendingUpdates();
|
||||||
});
|
|
||||||
this.gameView.update(gu);
|
|
||||||
this.renderer.tick();
|
|
||||||
|
|
||||||
// Update tick / backlog metrics
|
|
||||||
if (!("errMsg" in gu)) {
|
|
||||||
this.lastProcessedTick = gu.tick;
|
|
||||||
this.backlogTurns = Math.max(
|
|
||||||
0,
|
|
||||||
this.serverTurnHighWater - this.lastProcessedTick,
|
|
||||||
);
|
|
||||||
|
|
||||||
const wasCatchUp = this.catchUpMode;
|
|
||||||
if (
|
|
||||||
!this.catchUpMode &&
|
|
||||||
this.backlogTurns >= this.CATCH_UP_ENTER_BACKLOG
|
|
||||||
) {
|
|
||||||
this.catchUpMode = true;
|
|
||||||
} else if (
|
|
||||||
this.catchUpMode &&
|
|
||||||
this.backlogTurns <= this.CATCH_UP_EXIT_BACKLOG
|
|
||||||
) {
|
|
||||||
this.catchUpMode = false;
|
|
||||||
}
|
|
||||||
if (wasCatchUp !== this.catchUpMode) {
|
|
||||||
console.log(
|
|
||||||
`Catch-up mode ${this.catchUpMode ? "enabled" : "disabled"} (backlog: ${
|
|
||||||
this.backlogTurns
|
|
||||||
} turns)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit tick metrics event for performance overlay
|
|
||||||
this.eventBus.emit(
|
|
||||||
new TickMetricsEvent(
|
|
||||||
gu.tickExecutionDuration,
|
|
||||||
this.currentTickDelay,
|
|
||||||
this.backlogTurns,
|
|
||||||
this.catchUpMode,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset tick delay for next measurement
|
|
||||||
this.currentTickDelay = undefined;
|
|
||||||
|
|
||||||
if (gu.updates[GameUpdateType.Win].length > 0) {
|
|
||||||
this.saveGame(gu.updates[GameUpdateType.Win][0]);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const worker = this.worker;
|
const worker = this.worker;
|
||||||
@@ -364,6 +319,7 @@ export class ClientGameRunner {
|
|||||||
for (let i = 0; i < beatsPerFrame; i++) {
|
for (let i = 0; i < beatsPerFrame; i++) {
|
||||||
worker.sendHeartbeat();
|
worker.sendHeartbeat();
|
||||||
}
|
}
|
||||||
|
this.processPendingUpdates();
|
||||||
requestAnimationFrame(keepWorkerAlive);
|
requestAnimationFrame(keepWorkerAlive);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -503,6 +459,129 @@ export class ClientGameRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private processPendingUpdates() {
|
||||||
|
if (this.isProcessingUpdates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.pendingUpdates.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isProcessingUpdates = true;
|
||||||
|
const batch = this.pendingUpdates.splice(0);
|
||||||
|
|
||||||
|
let processedCount = 0;
|
||||||
|
let lastTickDuration: number | undefined;
|
||||||
|
let lastTick: number | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const gu of batch) {
|
||||||
|
processedCount++;
|
||||||
|
|
||||||
|
this.transport.turnComplete();
|
||||||
|
gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => {
|
||||||
|
this.eventBus.emit(new SendHashEvent(hu.tick, hu.hash));
|
||||||
|
});
|
||||||
|
this.updateBacklogMetrics(gu.tick);
|
||||||
|
|
||||||
|
if (gu.updates[GameUpdateType.Win].length > 0) {
|
||||||
|
this.saveGame(gu.updates[GameUpdateType.Win][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gu.tickExecutionDuration !== undefined) {
|
||||||
|
lastTickDuration = gu.tickExecutionDuration;
|
||||||
|
}
|
||||||
|
lastTick = gu.tick;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isProcessingUpdates = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedCount > 0 && lastTick !== undefined) {
|
||||||
|
const combinedGu = this.mergeGameUpdates(batch);
|
||||||
|
if (combinedGu) {
|
||||||
|
this.gameView.update(combinedGu);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderer.tick();
|
||||||
|
this.eventBus.emit(
|
||||||
|
new TickMetricsEvent(
|
||||||
|
lastTickDuration,
|
||||||
|
this.currentTickDelay,
|
||||||
|
this.backlogTurns,
|
||||||
|
this.catchUpMode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// Reset tick delay for next measurement
|
||||||
|
this.currentTickDelay = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeGameUpdates(
|
||||||
|
batch: GameUpdateViewData[],
|
||||||
|
): GameUpdateViewData | null {
|
||||||
|
if (batch.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = batch[batch.length - 1];
|
||||||
|
const combinedUpdates: GameUpdates = {} as GameUpdates;
|
||||||
|
|
||||||
|
// Initialize combinedUpdates with empty arrays for each existing key
|
||||||
|
for (const key in last.updates) {
|
||||||
|
const type = Number(key) as GameUpdateType;
|
||||||
|
combinedUpdates[type] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedPackedTileUpdates: bigint[] = [];
|
||||||
|
|
||||||
|
for (const gu of batch) {
|
||||||
|
for (const key in gu.updates) {
|
||||||
|
const type = Number(key) as GameUpdateType;
|
||||||
|
// We don't care about the specific update subtype here; just treat it
|
||||||
|
// as an array we can concatenate.
|
||||||
|
const updatesForType = gu.updates[type] as unknown as any[];
|
||||||
|
(combinedUpdates[type] as unknown as any[]).push(...updatesForType);
|
||||||
|
}
|
||||||
|
gu.packedTileUpdates.forEach((tu) => {
|
||||||
|
combinedPackedTileUpdates.push(tu);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tick: last.tick,
|
||||||
|
updates: combinedUpdates,
|
||||||
|
packedTileUpdates: new BigUint64Array(combinedPackedTileUpdates),
|
||||||
|
playerNameViewData: last.playerNameViewData,
|
||||||
|
tickExecutionDuration: last.tickExecutionDuration,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateBacklogMetrics(processedTick: number) {
|
||||||
|
this.lastProcessedTick = processedTick;
|
||||||
|
this.backlogTurns = Math.max(
|
||||||
|
0,
|
||||||
|
this.serverTurnHighWater - this.lastProcessedTick,
|
||||||
|
);
|
||||||
|
|
||||||
|
const wasCatchUp = this.catchUpMode;
|
||||||
|
if (!this.catchUpMode && this.backlogTurns >= this.CATCH_UP_ENTER_BACKLOG) {
|
||||||
|
this.catchUpMode = true;
|
||||||
|
} else if (
|
||||||
|
this.catchUpMode &&
|
||||||
|
this.backlogTurns <= this.CATCH_UP_EXIT_BACKLOG
|
||||||
|
) {
|
||||||
|
this.catchUpMode = false;
|
||||||
|
}
|
||||||
|
if (wasCatchUp !== this.catchUpMode) {
|
||||||
|
console.log(
|
||||||
|
`Catch-up mode ${this.catchUpMode ? "enabled" : "disabled"} (backlog: ${
|
||||||
|
this.backlogTurns
|
||||||
|
} turns)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private inputEvent(event: MouseUpEvent) {
|
private inputEvent(event: MouseUpEvent) {
|
||||||
if (!this.isActive || this.renderer.uiState.ghostStructure !== null) {
|
if (!this.isActive || this.renderer.uiState.ghostStructure !== null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user