mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 16:56:36 +00:00
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>
This commit is contained in:
@@ -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}`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
+91
-21
@@ -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`
|
||||
<div
|
||||
class="fps-display ${this.isDragging ? "dragging" : ""}"
|
||||
class="performance-overlay ${this.isDragging ? "dragging" : ""}"
|
||||
style="${style}"
|
||||
@mousedown="${this.handleMouseDown}"
|
||||
>
|
||||
<button class="close-button" @click="${this.handleClose}">×</button>
|
||||
<div class="fps-line">
|
||||
<div class="performance-line">
|
||||
FPS:
|
||||
<span class="${this.getFPSColor(this.currentFPS)}"
|
||||
<span class="${this.getPerformanceColor(this.currentFPS)}"
|
||||
>${this.currentFPS}</span
|
||||
>
|
||||
</div>
|
||||
<div class="fps-line">
|
||||
<div class="performance-line">
|
||||
Avg (60s):
|
||||
<span class="${this.getFPSColor(this.averageFPS)}"
|
||||
<span class="${this.getPerformanceColor(this.averageFPS)}"
|
||||
>${this.averageFPS}</span
|
||||
>
|
||||
</div>
|
||||
<div class="fps-line">
|
||||
<div class="performance-line">
|
||||
Frame:
|
||||
<span class="${this.getFPSColor(1000 / this.frameTime)}"
|
||||
<span class="${this.getPerformanceColor(1000 / this.frameTime)}"
|
||||
>${this.frameTime}ms</span
|
||||
>
|
||||
</div>
|
||||
<div class="performance-line">
|
||||
Tick Exec:
|
||||
<span>${this.tickExecutionAvg.toFixed(2)}ms</span>
|
||||
(max: <span>${this.tickExecutionMax}ms</span>)
|
||||
</div>
|
||||
<div class="performance-line">
|
||||
Tick Delay:
|
||||
<span>${this.tickDelayAvg.toFixed(2)}ms</span>
|
||||
(max: <span>${this.tickDelayMax}ms</span>)
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -405,7 +405,7 @@
|
||||
<news-modal></news-modal>
|
||||
<game-left-sidebar></game-left-sidebar>
|
||||
<flag-input-modal></flag-input-modal>
|
||||
<fps-display></fps-display>
|
||||
<performance-overlay></performance-overlay>
|
||||
<div
|
||||
id="language-modal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex justify-center items-center"
|
||||
|
||||
@@ -130,9 +130,13 @@ export class GameRunner {
|
||||
this.currTurn++;
|
||||
|
||||
let updates: GameUpdates;
|
||||
let tickExecutionDuration: number = 0;
|
||||
|
||||
try {
|
||||
const startTime = performance.now();
|
||||
updates = this.game.executeNextTick();
|
||||
const endTime = performance.now();
|
||||
tickExecutionDuration = endTime - startTime;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error("Game tick error:", error.message);
|
||||
@@ -173,6 +177,7 @@ export class GameRunner {
|
||||
packedTileUpdates: new BigUint64Array(packedTileUpdates),
|
||||
updates: updates,
|
||||
playerNameViewData: this.playerViewData,
|
||||
tickExecutionDuration: tickExecutionDuration,
|
||||
});
|
||||
this.isExecuting = false;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface GameUpdateViewData {
|
||||
updates: GameUpdates;
|
||||
packedTileUpdates: BigUint64Array;
|
||||
playerNameViewData: Record<string, NameViewData>;
|
||||
tickExecutionDuration?: number;
|
||||
}
|
||||
|
||||
export interface ErrorUpdate {
|
||||
|
||||
Reference in New Issue
Block a user