Skip to content

Commit 822d08b

Browse files
Add performance stats (#2338)
## 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: - <img width="495" height="227" alt="image" src="https://github.com/user-attachments/assets/142b0313-61bf-46cc-b595-61fe73f6b54c" /> - [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 <evanpelle@gmail.com>
1 parent 4553962 commit 822d08b

File tree

7 files changed

+136
-32
lines changed

7 files changed

+136
-32
lines changed

src/client/ClientGameRunner.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
InputHandler,
3434
MouseMoveEvent,
3535
MouseUpEvent,
36+
TickMetricsEvent,
3637
} from "./InputHandler";
3738
import { endGame, startGame, startTime } from "./LocalPersistantStats";
3839
import { getPersistentID } from "./Main";
@@ -201,6 +202,8 @@ export class ClientGameRunner {
201202

202203
private lastMessageTime: number = 0;
203204
private connectionCheckInterval: NodeJS.Timeout | null = null;
205+
private lastTickReceiveTime: number = 0;
206+
private currentTickDelay: number | undefined = undefined;
204207

205208
constructor(
206209
private lobby: LobbyConfig,
@@ -292,6 +295,14 @@ export class ClientGameRunner {
292295
this.gameView.update(gu);
293296
this.renderer.tick();
294297

298+
// Emit tick metrics event for performance overlay
299+
this.eventBus.emit(
300+
new TickMetricsEvent(gu.tickExecutionDuration, this.currentTickDelay),
301+
);
302+
303+
// Reset tick delay for next measurement
304+
this.currentTickDelay = undefined;
305+
295306
if (gu.updates[GameUpdateType.Win].length > 0) {
296307
this.saveGame(gu.updates[GameUpdateType.Win][0]);
297308
}
@@ -359,6 +370,14 @@ export class ClientGameRunner {
359370
this.transport.joinGame(0);
360371
return;
361372
}
373+
// Track when we receive the turn to calculate delay
374+
const now = Date.now();
375+
if (this.lastTickReceiveTime > 0) {
376+
// Calculate delay between receiving turn messages
377+
this.currentTickDelay = now - this.lastTickReceiveTime;
378+
}
379+
this.lastTickReceiveTime = now;
380+
362381
if (this.turnsSeen !== message.turn.turnNumber) {
363382
console.error(
364383
`got wrong turn have turns ${this.turnsSeen}, received turn ${message.turn.turnNumber}`,

src/client/InputHandler.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ export class AutoUpgradeEvent implements GameEvent {
115115
) {}
116116
}
117117

118+
export class TickMetricsEvent implements GameEvent {
119+
constructor(
120+
public readonly tickExecutionDuration?: number,
121+
public readonly tickDelay?: number,
122+
) {}
123+
}
124+
118125
export class InputHandler {
119126
private lastPointerX: number = 0;
120127
private lastPointerY: number = 0;

src/client/graphics/GameRenderer.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { ChatModal } from "./layers/ChatModal";
1313
import { ControlPanel } from "./layers/ControlPanel";
1414
import { EmojiTable } from "./layers/EmojiTable";
1515
import { EventsDisplay } from "./layers/EventsDisplay";
16-
import { FPSDisplay } from "./layers/FPSDisplay";
1716
import { FxLayer } from "./layers/FxLayer";
1817
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
1918
import { GameRightSidebar } from "./layers/GameRightSidebar";
@@ -23,6 +22,7 @@ import { Leaderboard } from "./layers/Leaderboard";
2322
import { MainRadialMenu } from "./layers/MainRadialMenu";
2423
import { MultiTabModal } from "./layers/MultiTabModal";
2524
import { NameLayer } from "./layers/NameLayer";
25+
import { PerformanceOverlay } from "./layers/PerformanceOverlay";
2626
import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay";
2727
import { PlayerPanel } from "./layers/PlayerPanel";
2828
import { RailroadLayer } from "./layers/RailroadLayer";
@@ -202,12 +202,14 @@ export function createRenderer(
202202

203203
const structureLayer = new StructureLayer(game, eventBus, transformHandler);
204204

205-
const fpsDisplay = document.querySelector("fps-display") as FPSDisplay;
206-
if (!(fpsDisplay instanceof FPSDisplay)) {
207-
console.error("fps display not found");
205+
const performanceOverlay = document.querySelector(
206+
"performance-overlay",
207+
) as PerformanceOverlay;
208+
if (!(performanceOverlay instanceof PerformanceOverlay)) {
209+
console.error("performance overlay not found");
208210
}
209-
fpsDisplay.eventBus = eventBus;
210-
fpsDisplay.userSettings = userSettings;
211+
performanceOverlay.eventBus = eventBus;
212+
performanceOverlay.userSettings = userSettings;
211213

212214
const alertFrame = document.querySelector("alert-frame") as AlertFrame;
213215
if (!(alertFrame instanceof AlertFrame)) {
@@ -263,7 +265,7 @@ export function createRenderer(
263265
multiTabModal,
264266
new AdTimer(game),
265267
alertFrame,
266-
fpsDisplay,
268+
performanceOverlay,
267269
];
268270

269271
return new GameRenderer(
@@ -273,7 +275,7 @@ export function createRenderer(
273275
transformHandler,
274276
uiState,
275277
layers,
276-
fpsDisplay,
278+
performanceOverlay,
277279
);
278280
}
279281

@@ -287,7 +289,7 @@ export class GameRenderer {
287289
public transformHandler: TransformHandler,
288290
public uiState: UIState,
289291
private layers: Layer[],
290-
private fpsDisplay: FPSDisplay,
292+
private performanceOverlay: PerformanceOverlay,
291293
) {
292294
const context = canvas.getContext("2d");
293295
if (context === null) throw new Error("2d context not supported");
@@ -371,7 +373,7 @@ export class GameRenderer {
371373
requestAnimationFrame(() => this.renderGame());
372374
const duration = performance.now() - start;
373375

374-
this.fpsDisplay.updateFPS(duration);
376+
this.performanceOverlay.updateFrameMetrics(duration);
375377

376378
if (duration > 50) {
377379
console.warn(

src/client/graphics/layers/FPSDisplay.ts renamed to src/client/graphics/layers/PerformanceOverlay.ts

Lines changed: 91 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ import { LitElement, css, html } from "lit";
22
import { customElement, property, state } from "lit/decorators.js";
33
import { EventBus } from "../../../core/EventBus";
44
import { UserSettings } from "../../../core/game/UserSettings";
5-
import { TogglePerformanceOverlayEvent } from "../../InputHandler";
5+
import {
6+
TickMetricsEvent,
7+
TogglePerformanceOverlayEvent,
8+
} from "../../InputHandler";
69
import { Layer } from "./Layer";
710

8-
@customElement("fps-display")
9-
export class FPSDisplay extends LitElement implements Layer {
11+
@customElement("performance-overlay")
12+
export class PerformanceOverlay extends LitElement implements Layer {
1013
@property({ type: Object })
1114
public eventBus!: EventBus;
1215

@@ -22,6 +25,18 @@ export class FPSDisplay extends LitElement implements Layer {
2225
@state()
2326
private frameTime: number = 0;
2427

28+
@state()
29+
private tickExecutionAvg: number = 0;
30+
31+
@state()
32+
private tickExecutionMax: number = 0;
33+
34+
@state()
35+
private tickDelayAvg: number = 0;
36+
37+
@state()
38+
private tickDelayMax: number = 0;
39+
2540
@state()
2641
private isVisible: boolean = false;
2742

@@ -38,9 +53,11 @@ export class FPSDisplay extends LitElement implements Layer {
3853
private lastSecondTime: number = 0;
3954
private framesThisSecond: number = 0;
4055
private dragStart: { x: number; y: number } = { x: 0, y: 0 };
56+
private tickExecutionTimes: number[] = [];
57+
private tickDelayTimes: number[] = [];
4158

4259
static styles = css`
43-
.fps-display {
60+
.performance-overlay {
4461
position: fixed;
4562
top: 20px;
4663
left: 50%;
@@ -57,25 +74,25 @@ export class FPSDisplay extends LitElement implements Layer {
5774
transition: none;
5875
}
5976
60-
.fps-display.dragging {
77+
.performance-overlay.dragging {
6178
cursor: grabbing;
6279
transition: none;
6380
opacity: 0.5;
6481
}
6582
66-
.fps-line {
83+
.performance-line {
6784
margin: 2px 0;
6885
}
6986
70-
.fps-good {
87+
.performance-good {
7188
color: #4ade80; /* green-400 */
7289
}
7390
74-
.fps-warning {
91+
.performance-warning {
7592
color: #fbbf24; /* amber-400 */
7693
}
7794
78-
.fps-bad {
95+
.performance-bad {
7996
color: #f87171; /* red-400 */
8097
}
8198
@@ -108,6 +125,9 @@ export class FPSDisplay extends LitElement implements Layer {
108125
this.eventBus.on(TogglePerformanceOverlayEvent, () => {
109126
this.userSettings.togglePerformanceOverlay();
110127
});
128+
this.eventBus.on(TickMetricsEvent, (event: TickMetricsEvent) => {
129+
this.updateTickMetrics(event.tickExecutionDuration, event.tickDelay);
130+
});
111131
}
112132

113133
setVisible(visible: boolean) {
@@ -159,7 +179,7 @@ export class FPSDisplay extends LitElement implements Layer {
159179
document.removeEventListener("mouseup", this.handleMouseUp);
160180
};
161181

162-
updateFPS(frameDuration: number) {
182+
updateFrameMetrics(frameDuration: number) {
163183
this.isVisible = this.userSettings.performanceOverlay();
164184

165185
if (!this.isVisible) return;
@@ -216,14 +236,54 @@ export class FPSDisplay extends LitElement implements Layer {
216236
this.requestUpdate();
217237
}
218238

239+
updateTickMetrics(tickExecutionDuration?: number, tickDelay?: number) {
240+
if (!this.isVisible || !this.userSettings.performanceOverlay()) return;
241+
242+
// Update tick execution duration stats
243+
if (tickExecutionDuration !== undefined) {
244+
this.tickExecutionTimes.push(tickExecutionDuration);
245+
if (this.tickExecutionTimes.length > 60) {
246+
this.tickExecutionTimes.shift();
247+
}
248+
249+
if (this.tickExecutionTimes.length > 0) {
250+
const avg =
251+
this.tickExecutionTimes.reduce((a, b) => a + b, 0) /
252+
this.tickExecutionTimes.length;
253+
this.tickExecutionAvg = Math.round(avg * 100) / 100;
254+
this.tickExecutionMax = Math.round(
255+
Math.max(...this.tickExecutionTimes),
256+
);
257+
}
258+
}
259+
260+
// Update tick delay stats
261+
if (tickDelay !== undefined) {
262+
this.tickDelayTimes.push(tickDelay);
263+
if (this.tickDelayTimes.length > 60) {
264+
this.tickDelayTimes.shift();
265+
}
266+
267+
if (this.tickDelayTimes.length > 0) {
268+
const avg =
269+
this.tickDelayTimes.reduce((a, b) => a + b, 0) /
270+
this.tickDelayTimes.length;
271+
this.tickDelayAvg = Math.round(avg * 100) / 100;
272+
this.tickDelayMax = Math.round(Math.max(...this.tickDelayTimes));
273+
}
274+
}
275+
276+
this.requestUpdate();
277+
}
278+
219279
shouldTransform(): boolean {
220280
return false;
221281
}
222282

223-
private getFPSColor(fps: number): string {
224-
if (fps >= 55) return "fps-good";
225-
if (fps >= 30) return "fps-warning";
226-
return "fps-bad";
283+
private getPerformanceColor(fps: number): string {
284+
if (fps >= 55) return "performance-good";
285+
if (fps >= 30) return "performance-warning";
286+
return "performance-bad";
227287
}
228288

229289
render() {
@@ -239,29 +299,39 @@ export class FPSDisplay extends LitElement implements Layer {
239299

240300
return html`
241301
<div
242-
class="fps-display ${this.isDragging ? "dragging" : ""}"
302+
class="performance-overlay ${this.isDragging ? "dragging" : ""}"
243303
style="${style}"
244304
@mousedown="${this.handleMouseDown}"
245305
>
246306
<button class="close-button" @click="${this.handleClose}">×</button>
247-
<div class="fps-line">
307+
<div class="performance-line">
248308
FPS:
249-
<span class="${this.getFPSColor(this.currentFPS)}"
309+
<span class="${this.getPerformanceColor(this.currentFPS)}"
250310
>${this.currentFPS}</span
251311
>
252312
</div>
253-
<div class="fps-line">
313+
<div class="performance-line">
254314
Avg (60s):
255-
<span class="${this.getFPSColor(this.averageFPS)}"
315+
<span class="${this.getPerformanceColor(this.averageFPS)}"
256316
>${this.averageFPS}</span
257317
>
258318
</div>
259-
<div class="fps-line">
319+
<div class="performance-line">
260320
Frame:
261-
<span class="${this.getFPSColor(1000 / this.frameTime)}"
321+
<span class="${this.getPerformanceColor(1000 / this.frameTime)}"
262322
>${this.frameTime}ms</span
263323
>
264324
</div>
325+
<div class="performance-line">
326+
Tick Exec:
327+
<span>${this.tickExecutionAvg.toFixed(2)}ms</span>
328+
(max: <span>${this.tickExecutionMax}ms</span>)
329+
</div>
330+
<div class="performance-line">
331+
Tick Delay:
332+
<span>${this.tickDelayAvg.toFixed(2)}ms</span>
333+
(max: <span>${this.tickDelayMax}ms</span>)
334+
</div>
265335
</div>
266336
`;
267337
}

src/client/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@
405405
<news-modal></news-modal>
406406
<game-left-sidebar></game-left-sidebar>
407407
<flag-input-modal></flag-input-modal>
408-
<fps-display></fps-display>
408+
<performance-overlay></performance-overlay>
409409
<div
410410
id="language-modal"
411411
class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex justify-center items-center"

src/core/GameRunner.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,13 @@ export class GameRunner {
130130
this.currTurn++;
131131

132132
let updates: GameUpdates;
133+
let tickExecutionDuration: number = 0;
133134

134135
try {
136+
const startTime = performance.now();
135137
updates = this.game.executeNextTick();
138+
const endTime = performance.now();
139+
tickExecutionDuration = endTime - startTime;
136140
} catch (error: unknown) {
137141
if (error instanceof Error) {
138142
console.error("Game tick error:", error.message);
@@ -173,6 +177,7 @@ export class GameRunner {
173177
packedTileUpdates: new BigUint64Array(packedTileUpdates),
174178
updates: updates,
175179
playerNameViewData: this.playerViewData,
180+
tickExecutionDuration: tickExecutionDuration,
176181
});
177182
this.isExecuting = false;
178183
}

src/core/game/GameUpdates.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface GameUpdateViewData {
1919
updates: GameUpdates;
2020
packedTileUpdates: BigUint64Array;
2121
playerNameViewData: Record<string, NameViewData>;
22+
tickExecutionDuration?: number;
2223
}
2324

2425
export interface ErrorUpdate {

0 commit comments

Comments
 (0)