diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 575977f9a..c7c95a24b 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -208,6 +208,16 @@ export class ClientGameRunner { private lastTickReceiveTime: number = 0; private currentTickDelay: number | undefined = undefined; + // Track how far behind the client simulation is compared to the server. + private serverTurnHighWater: number = 0; + private lastProcessedTick: number = 0; + private backlogTurns: number = 0; + + private catchUpMode: boolean = false; + private readonly CATCH_UP_ENTER_BACKLOG = 120; // turns behind to enter catch-up + private readonly CATCH_UP_EXIT_BACKLOG = 20; // turns behind to exit catch-up + private readonly CATCH_UP_HEARTBEATS_PER_FRAME = 5; + constructor( private lobby: LobbyConfig, private eventBus: EventBus, @@ -299,9 +309,43 @@ export class ClientGameRunner { 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), + new TickMetricsEvent( + gu.tickExecutionDuration, + this.currentTickDelay, + this.backlogTurns, + this.catchUpMode, + ), ); // Reset tick delay for next measurement @@ -314,7 +358,12 @@ export class ClientGameRunner { const worker = this.worker; const keepWorkerAlive = () => { if (this.isActive) { - worker.sendHeartbeat(); + const beatsPerFrame = this.catchUpMode + ? this.CATCH_UP_HEARTBEATS_PER_FRAME + : 1; + for (let i = 0; i < beatsPerFrame; i++) { + worker.sendHeartbeat(); + } requestAnimationFrame(keepWorkerAlive); } }; @@ -363,6 +412,10 @@ export class ClientGameRunner { } for (const turn of message.turns) { + this.serverTurnHighWater = Math.max( + this.serverTurnHighWater, + turn.turnNumber, + ); if (turn.turnNumber < this.turnsSeen) { continue; } @@ -415,6 +468,11 @@ export class ClientGameRunner { } this.lastTickReceiveTime = now; + this.serverTurnHighWater = Math.max( + this.serverTurnHighWater, + message.turn.turnNumber, + ); + if (this.turnsSeen !== message.turn.turnNumber) { console.error( `got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`, diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 26d8f6c27..5ecc23915 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -129,6 +129,10 @@ export class TickMetricsEvent implements GameEvent { constructor( public readonly tickExecutionDuration?: number, public readonly tickDelay?: number, + // Number of turns the client is behind the server (if known) + public readonly backlogTurns?: number, + // Whether the client is currently in catch-up mode + public readonly inCatchUpMode?: boolean, ) {} } diff --git a/src/client/graphics/layers/PerformanceOverlay.ts b/src/client/graphics/layers/PerformanceOverlay.ts index fb744d4a0..0fc50ffdf 100644 --- a/src/client/graphics/layers/PerformanceOverlay.ts +++ b/src/client/graphics/layers/PerformanceOverlay.ts @@ -229,7 +229,12 @@ export class PerformanceOverlay extends LitElement implements Layer { this.setVisible(this.userSettings.performanceOverlay()); }); this.eventBus.on(TickMetricsEvent, (event: TickMetricsEvent) => { - this.updateTickMetrics(event.tickExecutionDuration, event.tickDelay); + this.updateTickMetrics( + event.tickExecutionDuration, + event.tickDelay, + event.backlogTurns, + event.inCatchUpMode, + ); }); } @@ -418,7 +423,18 @@ export class PerformanceOverlay extends LitElement implements Layer { this.layerBreakdown = breakdown; } - updateTickMetrics(tickExecutionDuration?: number, tickDelay?: number) { + @state() + private backlogTurns: number = 0; + + @state() + private inCatchUpMode: boolean = false; + + updateTickMetrics( + tickExecutionDuration?: number, + tickDelay?: number, + backlogTurns?: number, + inCatchUpMode?: boolean, + ) { if (!this.isVisible || !this.userSettings.performanceOverlay()) return; // Update tick execution duration stats @@ -455,6 +471,13 @@ export class PerformanceOverlay extends LitElement implements Layer { } } + if (backlogTurns !== undefined) { + this.backlogTurns = backlogTurns; + } + if (inCatchUpMode !== undefined) { + this.inCatchUpMode = inCatchUpMode; + } + this.requestUpdate(); } @@ -600,6 +623,11 @@ export class PerformanceOverlay extends LitElement implements Layer { ${this.tickDelayAvg.toFixed(2)}ms (max: ${this.tickDelayMax}ms) +