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`