From 822d08bb7cc4d14c75bc0882e326b2a07b10de75 Mon Sep 17 00:00:00 2001 From: Kerod Kibatu <88666160+kerodkibatu@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:25:54 -0500 Subject: [PATCH] Add performance stats (#2338) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Enhanced the performance overlay to display additional tick-related performance metrics. The overlay now shows: 1. **Tick Execution Duration** - Average and maximum time (in milliseconds) it takes to execute a game tick 2. **Tick Delay** - Average and maximum time (in milliseconds) between receiving tick updates from the server The server sends 10 updates per second (100ms interval), so these metrics help identify: - Client-side performance bottlenecks (tick execution duration) - Network latency issues (tick delay) **Additional improvements:** - Renamed `FPSDisplay` component to `PerformanceOverlay` to better reflect its expanded purpose - Updated method names (`updateFPS` → `updateFrameMetrics`) and CSS classes for consistency All metrics are tracked over the last 60 ticks, providing rolling averages and maximum values for performance analysis. ## Please complete the following: - [x] I have added screenshots for all UI updates: - image - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - Translation keys already exist in en.json for "performance_overlay_label" and "performance_overlay_desc" - [x] I have added relevant tests to the test directory - All existing tests pass (309/310 tests passed) - No new tests added as this is primarily a display enhancement - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - Tested locally with npm test - Verified performance overlay displays all metrics correctly - Confirmed tick metrics are calculated and displayed accurately ## Please put your Discord username so you can be contacted if a bug or regression is found: Discord: kerverse --------- Co-authored-by: Evan --- src/client/ClientGameRunner.ts | 19 +++ src/client/InputHandler.ts | 7 ++ src/client/graphics/GameRenderer.ts | 22 ++-- .../{FPSDisplay.ts => PerformanceOverlay.ts} | 112 ++++++++++++++---- src/client/index.html | 2 +- src/core/GameRunner.ts | 5 + src/core/game/GameUpdates.ts | 1 + 7 files changed, 136 insertions(+), 32 deletions(-) rename src/client/graphics/layers/{FPSDisplay.ts => PerformanceOverlay.ts} (65%) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index fbb1b83aa..320b8e3d8 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -33,6 +33,7 @@ import { InputHandler, MouseMoveEvent, MouseUpEvent, + TickMetricsEvent, } from "./InputHandler"; import { endGame, startGame, startTime } from "./LocalPersistantStats"; import { getPersistentID } from "./Main"; @@ -201,6 +202,8 @@ export class ClientGameRunner { private lastMessageTime: number = 0; private connectionCheckInterval: NodeJS.Timeout | null = null; + private lastTickReceiveTime: number = 0; + private currentTickDelay: number | undefined = undefined; constructor( private lobby: LobbyConfig, @@ -292,6 +295,14 @@ export class ClientGameRunner { this.gameView.update(gu); this.renderer.tick(); + // Emit tick metrics event for performance overlay + this.eventBus.emit( + new TickMetricsEvent(gu.tickExecutionDuration, this.currentTickDelay), + ); + + // Reset tick delay for next measurement + this.currentTickDelay = undefined; + if (gu.updates[GameUpdateType.Win].length > 0) { this.saveGame(gu.updates[GameUpdateType.Win][0]); } @@ -359,6 +370,14 @@ export class ClientGameRunner { this.transport.joinGame(0); return; } + // Track when we receive the turn to calculate delay + const now = Date.now(); + if (this.lastTickReceiveTime > 0) { + // Calculate delay between receiving turn messages + this.currentTickDelay = now - this.lastTickReceiveTime; + } + this.lastTickReceiveTime = now; + 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 686ea2e48..51212dd95 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -115,6 +115,13 @@ export class AutoUpgradeEvent implements GameEvent { ) {} } +export class TickMetricsEvent implements GameEvent { + constructor( + public readonly tickExecutionDuration?: number, + public readonly tickDelay?: number, + ) {} +} + export class InputHandler { private lastPointerX: number = 0; private lastPointerY: number = 0; diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 8b8080dcb..3a196bb2d 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -13,7 +13,6 @@ import { ChatModal } from "./layers/ChatModal"; import { ControlPanel } from "./layers/ControlPanel"; import { EmojiTable } from "./layers/EmojiTable"; import { EventsDisplay } from "./layers/EventsDisplay"; -import { FPSDisplay } from "./layers/FPSDisplay"; import { FxLayer } from "./layers/FxLayer"; import { GameLeftSidebar } from "./layers/GameLeftSidebar"; import { GameRightSidebar } from "./layers/GameRightSidebar"; @@ -23,6 +22,7 @@ import { Leaderboard } from "./layers/Leaderboard"; import { MainRadialMenu } from "./layers/MainRadialMenu"; import { MultiTabModal } from "./layers/MultiTabModal"; import { NameLayer } from "./layers/NameLayer"; +import { PerformanceOverlay } from "./layers/PerformanceOverlay"; import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay"; import { PlayerPanel } from "./layers/PlayerPanel"; import { RailroadLayer } from "./layers/RailroadLayer"; @@ -202,12 +202,14 @@ export function createRenderer( const structureLayer = new StructureLayer(game, eventBus, transformHandler); - const fpsDisplay = document.querySelector("fps-display") as FPSDisplay; - if (!(fpsDisplay instanceof FPSDisplay)) { - console.error("fps display not found"); + const performanceOverlay = document.querySelector( + "performance-overlay", + ) as PerformanceOverlay; + if (!(performanceOverlay instanceof PerformanceOverlay)) { + console.error("performance overlay not found"); } - fpsDisplay.eventBus = eventBus; - fpsDisplay.userSettings = userSettings; + performanceOverlay.eventBus = eventBus; + performanceOverlay.userSettings = userSettings; const alertFrame = document.querySelector("alert-frame") as AlertFrame; if (!(alertFrame instanceof AlertFrame)) { @@ -263,7 +265,7 @@ export function createRenderer( multiTabModal, new AdTimer(game), alertFrame, - fpsDisplay, + performanceOverlay, ]; return new GameRenderer( @@ -273,7 +275,7 @@ export function createRenderer( transformHandler, uiState, layers, - fpsDisplay, + performanceOverlay, ); } @@ -287,7 +289,7 @@ export class GameRenderer { public transformHandler: TransformHandler, public uiState: UIState, private layers: Layer[], - private fpsDisplay: FPSDisplay, + private performanceOverlay: PerformanceOverlay, ) { const context = canvas.getContext("2d"); if (context === null) throw new Error("2d context not supported"); @@ -371,7 +373,7 @@ export class GameRenderer { requestAnimationFrame(() => this.renderGame()); const duration = performance.now() - start; - this.fpsDisplay.updateFPS(duration); + this.performanceOverlay.updateFrameMetrics(duration); if (duration > 50) { console.warn( diff --git a/src/client/graphics/layers/FPSDisplay.ts b/src/client/graphics/layers/PerformanceOverlay.ts similarity index 65% rename from src/client/graphics/layers/FPSDisplay.ts rename to src/client/graphics/layers/PerformanceOverlay.ts index 37b3bb3cd..ab6e6cfef 100644 --- a/src/client/graphics/layers/FPSDisplay.ts +++ b/src/client/graphics/layers/PerformanceOverlay.ts @@ -2,11 +2,14 @@ import { LitElement, css, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { EventBus } from "../../../core/EventBus"; import { UserSettings } from "../../../core/game/UserSettings"; -import { TogglePerformanceOverlayEvent } from "../../InputHandler"; +import { + TickMetricsEvent, + TogglePerformanceOverlayEvent, +} from "../../InputHandler"; import { Layer } from "./Layer"; -@customElement("fps-display") -export class FPSDisplay extends LitElement implements Layer { +@customElement("performance-overlay") +export class PerformanceOverlay extends LitElement implements Layer { @property({ type: Object }) public eventBus!: EventBus; @@ -22,6 +25,18 @@ export class FPSDisplay extends LitElement implements Layer { @state() private frameTime: number = 0; + @state() + private tickExecutionAvg: number = 0; + + @state() + private tickExecutionMax: number = 0; + + @state() + private tickDelayAvg: number = 0; + + @state() + private tickDelayMax: number = 0; + @state() private isVisible: boolean = false; @@ -38,9 +53,11 @@ export class FPSDisplay extends LitElement implements Layer { private lastSecondTime: number = 0; private framesThisSecond: number = 0; private dragStart: { x: number; y: number } = { x: 0, y: 0 }; + private tickExecutionTimes: number[] = []; + private tickDelayTimes: number[] = []; static styles = css` - .fps-display { + .performance-overlay { position: fixed; top: 20px; left: 50%; @@ -57,25 +74,25 @@ export class FPSDisplay extends LitElement implements Layer { transition: none; } - .fps-display.dragging { + .performance-overlay.dragging { cursor: grabbing; transition: none; opacity: 0.5; } - .fps-line { + .performance-line { margin: 2px 0; } - .fps-good { + .performance-good { color: #4ade80; /* green-400 */ } - .fps-warning { + .performance-warning { color: #fbbf24; /* amber-400 */ } - .fps-bad { + .performance-bad { color: #f87171; /* red-400 */ } @@ -108,6 +125,9 @@ export class FPSDisplay extends LitElement implements Layer { this.eventBus.on(TogglePerformanceOverlayEvent, () => { this.userSettings.togglePerformanceOverlay(); }); + this.eventBus.on(TickMetricsEvent, (event: TickMetricsEvent) => { + this.updateTickMetrics(event.tickExecutionDuration, event.tickDelay); + }); } setVisible(visible: boolean) { @@ -159,7 +179,7 @@ export class FPSDisplay extends LitElement implements Layer { document.removeEventListener("mouseup", this.handleMouseUp); }; - updateFPS(frameDuration: number) { + updateFrameMetrics(frameDuration: number) { this.isVisible = this.userSettings.performanceOverlay(); if (!this.isVisible) return; @@ -216,14 +236,54 @@ export class FPSDisplay extends LitElement implements Layer { this.requestUpdate(); } + updateTickMetrics(tickExecutionDuration?: number, tickDelay?: number) { + if (!this.isVisible || !this.userSettings.performanceOverlay()) return; + + // Update tick execution duration stats + if (tickExecutionDuration !== undefined) { + this.tickExecutionTimes.push(tickExecutionDuration); + if (this.tickExecutionTimes.length > 60) { + this.tickExecutionTimes.shift(); + } + + if (this.tickExecutionTimes.length > 0) { + const avg = + this.tickExecutionTimes.reduce((a, b) => a + b, 0) / + this.tickExecutionTimes.length; + this.tickExecutionAvg = Math.round(avg * 100) / 100; + this.tickExecutionMax = Math.round( + Math.max(...this.tickExecutionTimes), + ); + } + } + + // Update tick delay stats + if (tickDelay !== undefined) { + this.tickDelayTimes.push(tickDelay); + if (this.tickDelayTimes.length > 60) { + this.tickDelayTimes.shift(); + } + + if (this.tickDelayTimes.length > 0) { + const avg = + this.tickDelayTimes.reduce((a, b) => a + b, 0) / + this.tickDelayTimes.length; + this.tickDelayAvg = Math.round(avg * 100) / 100; + this.tickDelayMax = Math.round(Math.max(...this.tickDelayTimes)); + } + } + + this.requestUpdate(); + } + shouldTransform(): boolean { return false; } - private getFPSColor(fps: number): string { - if (fps >= 55) return "fps-good"; - if (fps >= 30) return "fps-warning"; - return "fps-bad"; + private getPerformanceColor(fps: number): string { + if (fps >= 55) return "performance-good"; + if (fps >= 30) return "performance-warning"; + return "performance-bad"; } render() { @@ -239,29 +299,39 @@ export class FPSDisplay extends LitElement implements Layer { return html`
-
+
FPS: - ${this.currentFPS}
-
+
Avg (60s): - ${this.averageFPS}
-
+
Frame: - ${this.frameTime}ms
+
+ Tick Exec: + ${this.tickExecutionAvg.toFixed(2)}ms + (max: ${this.tickExecutionMax}ms) +
+
+ Tick Delay: + ${this.tickDelayAvg.toFixed(2)}ms + (max: ${this.tickDelayMax}ms) +
`; } diff --git a/src/client/index.html b/src/client/index.html index ae46d19e8..03f2efad8 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -405,7 +405,7 @@ - +