mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 13:50:43 +00:00
Feature/frame profiler (#2467)
## Description:
Adds a reusable FrameProfiler utility, and a way to export profiling
data for offline analysis.
### What this PR changes in the existing performance monitor
This PR enhances the performance monitor by:
- **Introducing a reusable `FrameProfiler` utility**
- New `FrameProfiler` singleton in
`src/client/graphics/FrameProfiler.ts`.
- Profiling is only active when the performance overlay is visible
(toggled via user settings), to avoid unnecessary overhead.
- **Per-layer and span-level timing integration**
- `GameRenderer.renderGame` now:
- Clears the profiler at the start of each frame.
- Wraps each `layer.renderLayer?.(this.context)` call with
`FrameProfiler.start()/end()`, keyed by the layer’s constructor name.
- Consumes the recorded timings at the end of the frame and passes them
into `PerformanceOverlay.updateFrameMetrics(frameDuration,
layerDurations)`.
- `TerritoryLayer` instruments key operations:
- `renderTerritory`
- `putImageData`
- Drawing the main canvas
- Drawing the highlight canvas during spawn
- These show up in the performance overlay as additional entries (e.g.
`TerritoryLayer:renderTerritory`).
- **JSON export of performance snapshots**
- `PerformanceOverlay` can now build a full performance snapshot
(`buildPerformanceSnapshot`) containing:
- FPS and frame time stats (current, 60s average, 60s history).
- Tick metrics (avg/max execution and delay, plus raw samples).
- Layer breakdown (EMA-smoothed avg, max, total time per layer/span).
- A new “Copy JSON” button:
- Uses `navigator.clipboard.writeText` when available and falls back to
a hidden `<textarea>` + `document.execCommand("copy")`.
- Provides user feedback via a transient status ("Copy JSON" → "Copied!"
or "Failed to copy").
- **Enable/disable functionality hooked into the UI**
- `FrameProfiler.setEnabled(visible)` is invoked:
- When the overlay visibility is toggled (`init` → `setVisible`).
- When the overlay re-checks visibility in `updateFrameMetrics`, so the
profiler state stays in sync with user settings.
- When disabled, `FrameProfiler` becomes a no-op (returns `0` from
`start`, ignores `record`/`end`, and `consume` returns an empty object),
ensuring minimal overhead when performance monitoring is off.
- **Performance overlay UX and i18n improvements**
- New controls:
- **Reset** button to clear all FPS/tick/layer stats.
- **Copy JSON** button with a tooltip and transient status text.
- Visual enhancements:
- Wider overlay (`min-width: 420px`) and extra padding for readability.
- Layer breakdown section with:
- A list that is now sorted by total accumulated time.
- A horizontal bar per entry, scaled by average cost.
- Avg / max time display per layer/span.
- All new text is routed through `translateText` and backed by
`en.json`:
- `performance_overlay.reset`
- `performance_overlay.copy_json_title`
- `performance_overlay.copy_clipboard`
- `performance_overlay.copied`
- `performance_overlay.failed_copy`
- `performance_overlay.fps`
- `performance_overlay.avg_60s`
- `performance_overlay.frame`
- `performance_overlay.tick_exec`
- `performance_overlay.tick_delay`
- `performance_overlay.layers_header`
---
### How to set up profiling for new functions / code paths
For any function or code block you want to profile during a frame:
```ts
import { FrameProfiler } from "../FrameProfiler";
function heavyOperation() {
const spanStart = FrameProfiler.start();
// ... your existing work ...
FrameProfiler.end("MyFeature:heavyOperation", spanStart);
}
```
Guidelines:
- Use descriptive, stable names:
- Prefix with the component or layer name, e.g.:
- `"TerritoryLayer:prepareTiles"`
- `"GameRenderer:resolveVisibility"`
- `"FooFeature:fetchData"`
- The same name can be called multiple times per frame; the profiler
accumulates the durations in that frame.
- The accumulated values will appear:
- In `layerDurations` consumed at the end of the frame.
- In the overlay “Layers (avg / max, sorted by total time)” section.
- In the exported JSON under `layers` with `avg`, `max`, and `total`.
**3. Record pre-computed durations (optional)**
If you already have a measured duration and just want to attach it:
```ts
FrameProfiler.record("MyFeature:step1", someDurationInMs);
```
- This is equivalent to calling `start`/`end` but with your own timing
logic.
- Again, multiple calls with the same name in one frame will be summed.
---
<img width="466" height="823" alt="image"
src="https://github.com/user-attachments/assets/354b249a-25eb-4c3f-bd2e-9906372f761b"
/>
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
---------
Co-authored-by: Evan <evanpelle@gmail.com>
This commit is contained in:
@@ -671,6 +671,19 @@
|
||||
},
|
||||
"desync_notice": "You are desynced from other players. What you see might differ from other players."
|
||||
},
|
||||
"performance_overlay": {
|
||||
"reset": "Reset",
|
||||
"copy_json_title": "Copy current performance metrics as JSON",
|
||||
"copy_clipboard": "Copy JSON",
|
||||
"copied": "Copied!",
|
||||
"failed_copy": "Failed to copy",
|
||||
"fps": "FPS:",
|
||||
"avg_60s": "Avg (60s):",
|
||||
"frame": "Frame:",
|
||||
"tick_exec": "Tick Exec:",
|
||||
"tick_delay": "Tick Delay:",
|
||||
"layers_header": "Layers (avg / max, sorted by total time):"
|
||||
},
|
||||
"heads_up_message": {
|
||||
"choose_spawn": "Choose a starting location",
|
||||
"random_spawn": "Random spawn is enabled. Selecting starting location for you..."
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
export class FrameProfiler {
|
||||
private static timings: Record<string, number> = {};
|
||||
private static enabled: boolean = false;
|
||||
|
||||
/**
|
||||
* Enable or disable profiling.
|
||||
*/
|
||||
static setEnabled(enabled: boolean): void {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if profiling is enabled.
|
||||
*/
|
||||
static isEnabled(): boolean {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all accumulated timings for the current frame.
|
||||
*/
|
||||
static clear(): void {
|
||||
if (!this.enabled) return;
|
||||
this.timings = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a duration (in ms) for a named span.
|
||||
*/
|
||||
static record(name: string, duration: number): void {
|
||||
if (!this.enabled || !Number.isFinite(duration)) return;
|
||||
this.timings[name] = (this.timings[name] ?? 0) + duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience helper to start a span.
|
||||
* Returns a high-resolution timestamp to be passed into end().
|
||||
*/
|
||||
static start(): number {
|
||||
if (!this.enabled) return 0;
|
||||
return performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience helper to end a span started with start().
|
||||
*/
|
||||
static end(name: string, startTime: number): void {
|
||||
if (!this.enabled || startTime === 0) return;
|
||||
const duration = performance.now() - startTime;
|
||||
this.record(name, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume and reset all timings collected so far.
|
||||
*/
|
||||
static consume(): Record<string, number> {
|
||||
if (!this.enabled) return {};
|
||||
const copy = { ...this.timings };
|
||||
this.timings = {};
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { GameView } from "../../core/game/GameView";
|
||||
import { UserSettings } from "../../core/game/UserSettings";
|
||||
import { GameStartingModal } from "../GameStartingModal";
|
||||
import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler";
|
||||
import { FrameProfiler } from "./FrameProfiler";
|
||||
import { TransformHandler } from "./TransformHandler";
|
||||
import { UIState } from "./UIState";
|
||||
import { AdTimer } from "./layers/AdTimer";
|
||||
@@ -343,6 +344,7 @@ export class GameRenderer {
|
||||
}
|
||||
|
||||
renderGame() {
|
||||
FrameProfiler.clear();
|
||||
const start = performance.now();
|
||||
// Set background
|
||||
this.context.fillStyle = this.game
|
||||
@@ -375,7 +377,10 @@ export class GameRenderer {
|
||||
needsTransform,
|
||||
isTransformActive,
|
||||
);
|
||||
|
||||
const layerStart = FrameProfiler.start();
|
||||
layer.renderLayer?.(this.context);
|
||||
FrameProfiler.end(layer.constructor?.name ?? "UnknownLayer", layerStart);
|
||||
}
|
||||
handleTransformState(false, isTransformActive); // Ensure context is clean after rendering
|
||||
this.transformHandler.resetChanged();
|
||||
@@ -383,7 +388,8 @@ export class GameRenderer {
|
||||
requestAnimationFrame(() => this.renderGame());
|
||||
const duration = performance.now() - start;
|
||||
|
||||
this.performanceOverlay.updateFrameMetrics(duration);
|
||||
const layerDurations = FrameProfiler.consume();
|
||||
this.performanceOverlay.updateFrameMetrics(duration, layerDurations);
|
||||
|
||||
if (duration > 50) {
|
||||
console.warn(
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
TickMetricsEvent,
|
||||
TogglePerformanceOverlayEvent,
|
||||
} from "../../InputHandler";
|
||||
import { translateText } from "../../Utils";
|
||||
import { FrameProfiler } from "../FrameProfiler";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("performance-overlay")
|
||||
@@ -46,6 +48,9 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
@state()
|
||||
private position: { x: number; y: number } = { x: 50, y: 20 }; // Percentage values
|
||||
|
||||
@state()
|
||||
private copyStatus: "idle" | "success" | "error" = "idle";
|
||||
|
||||
private frameCount: number = 0;
|
||||
private lastTime: number = 0;
|
||||
private frameTimes: number[] = [];
|
||||
@@ -56,6 +61,22 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
private tickExecutionTimes: number[] = [];
|
||||
private tickDelayTimes: number[] = [];
|
||||
|
||||
private copyStatusTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Smoothed per-layer render timings (EMA over recent frames)
|
||||
private layerStats: Map<
|
||||
string,
|
||||
{ avg: number; max: number; last: number; total: number }
|
||||
> = new Map();
|
||||
|
||||
@state()
|
||||
private layerBreakdown: {
|
||||
name: string;
|
||||
avg: number;
|
||||
max: number;
|
||||
total: number;
|
||||
}[] = [];
|
||||
|
||||
static styles = css`
|
||||
.performance-overlay {
|
||||
position: fixed;
|
||||
@@ -64,7 +85,7 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
@@ -72,6 +93,7 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
user-select: none;
|
||||
cursor: move;
|
||||
transition: none;
|
||||
min-width: 420px;
|
||||
}
|
||||
|
||||
.performance-overlay.dragging {
|
||||
@@ -115,6 +137,86 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.copy-json-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 70px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.layers-section {
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.layer-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.layer-name {
|
||||
flex: 0 0 280px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.layer-bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: rgba(148, 163, 184, 0.25);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.layer-bar-fill {
|
||||
height: 100%;
|
||||
background: #38bdf8;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.layer-metrics {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
@@ -124,6 +226,7 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
init() {
|
||||
this.eventBus.on(TogglePerformanceOverlayEvent, () => {
|
||||
this.userSettings.togglePerformanceOverlay();
|
||||
this.setVisible(this.userSettings.performanceOverlay());
|
||||
});
|
||||
this.eventBus.on(TickMetricsEvent, (event: TickMetricsEvent) => {
|
||||
this.updateTickMetrics(event.tickExecutionDuration, event.tickDelay);
|
||||
@@ -132,6 +235,7 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
|
||||
setVisible(visible: boolean) {
|
||||
this.isVisible = visible;
|
||||
FrameProfiler.setEnabled(visible);
|
||||
}
|
||||
|
||||
private handleClose() {
|
||||
@@ -140,7 +244,12 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
|
||||
private handleMouseDown = (e: MouseEvent) => {
|
||||
// Don't start dragging if clicking on close button
|
||||
if ((e.target as HTMLElement).classList.contains("close-button")) {
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.classList.contains("close-button") ||
|
||||
target.classList.contains("reset-button") ||
|
||||
target.classList.contains("copy-json-button")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -179,9 +288,45 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
document.removeEventListener("mouseup", this.handleMouseUp);
|
||||
};
|
||||
|
||||
updateFrameMetrics(frameDuration: number) {
|
||||
private handleReset = () => {
|
||||
// reset FPS / frame stats
|
||||
this.frameCount = 0;
|
||||
this.lastTime = 0;
|
||||
this.frameTimes = [];
|
||||
this.fpsHistory = [];
|
||||
this.lastSecondTime = 0;
|
||||
this.framesThisSecond = 0;
|
||||
this.currentFPS = 0;
|
||||
this.averageFPS = 0;
|
||||
this.frameTime = 0;
|
||||
|
||||
// reset tick metrics
|
||||
this.tickExecutionTimes = [];
|
||||
this.tickDelayTimes = [];
|
||||
this.tickExecutionAvg = 0;
|
||||
this.tickExecutionMax = 0;
|
||||
this.tickDelayAvg = 0;
|
||||
this.tickDelayMax = 0;
|
||||
|
||||
// reset layer breakdown
|
||||
this.layerStats.clear();
|
||||
this.layerBreakdown = [];
|
||||
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
updateFrameMetrics(
|
||||
frameDuration: number,
|
||||
layerDurations?: Record<string, number>,
|
||||
) {
|
||||
const wasVisible = this.isVisible;
|
||||
this.isVisible = this.userSettings.performanceOverlay();
|
||||
|
||||
// Update FrameProfiler enabled state when visibility changes
|
||||
if (wasVisible !== this.isVisible) {
|
||||
FrameProfiler.setEnabled(this.isVisible);
|
||||
}
|
||||
|
||||
if (!this.isVisible) return;
|
||||
|
||||
const now = performance.now();
|
||||
@@ -233,9 +378,46 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
this.lastTime = now;
|
||||
this.frameCount++;
|
||||
|
||||
if (layerDurations) {
|
||||
this.updateLayerStats(layerDurations);
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private updateLayerStats(layerDurations: Record<string, number>) {
|
||||
const alpha = 0.2; // smoothing factor for EMA
|
||||
|
||||
Object.entries(layerDurations).forEach(([name, duration]) => {
|
||||
const existing = this.layerStats.get(name);
|
||||
if (!existing) {
|
||||
this.layerStats.set(name, {
|
||||
avg: duration,
|
||||
max: duration,
|
||||
last: duration,
|
||||
total: duration,
|
||||
});
|
||||
} else {
|
||||
const avg = existing.avg + alpha * (duration - existing.avg);
|
||||
const max = Math.max(existing.max, duration);
|
||||
const total = existing.total + duration;
|
||||
this.layerStats.set(name, { avg, max, last: duration, total });
|
||||
}
|
||||
});
|
||||
|
||||
// Derive contributors sorted by total accumulated time spent
|
||||
const breakdown = Array.from(this.layerStats.entries())
|
||||
.map(([name, stats]) => ({
|
||||
name,
|
||||
avg: stats.avg,
|
||||
max: stats.max,
|
||||
total: stats.total,
|
||||
}))
|
||||
.sort((a, b) => b.total - a.total);
|
||||
|
||||
this.layerBreakdown = breakdown;
|
||||
}
|
||||
|
||||
updateTickMetrics(tickExecutionDuration?: number, tickDelay?: number) {
|
||||
if (!this.isVisible || !this.userSettings.performanceOverlay()) return;
|
||||
|
||||
@@ -286,6 +468,70 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
return "performance-bad";
|
||||
}
|
||||
|
||||
private buildPerformanceSnapshot() {
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
fps: {
|
||||
current: this.currentFPS,
|
||||
average60s: this.averageFPS,
|
||||
frameTimeMs: this.frameTime,
|
||||
history: [...this.fpsHistory],
|
||||
},
|
||||
ticks: {
|
||||
executionAvgMs: this.tickExecutionAvg,
|
||||
executionMaxMs: this.tickExecutionMax,
|
||||
delayAvgMs: this.tickDelayAvg,
|
||||
delayMaxMs: this.tickDelayMax,
|
||||
executionSamples: [...this.tickExecutionTimes],
|
||||
delaySamples: [...this.tickDelayTimes],
|
||||
},
|
||||
layers: this.layerBreakdown.map((layer) => ({ ...layer })),
|
||||
};
|
||||
}
|
||||
|
||||
private clearCopyStatusTimeout() {
|
||||
if (this.copyStatusTimeoutId !== null) {
|
||||
clearTimeout(this.copyStatusTimeoutId);
|
||||
this.copyStatusTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleCopyStatusReset() {
|
||||
this.clearCopyStatusTimeout();
|
||||
this.copyStatusTimeoutId = setTimeout(() => {
|
||||
this.copyStatus = "idle";
|
||||
this.copyStatusTimeoutId = null;
|
||||
this.requestUpdate();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
private async handleCopyJson() {
|
||||
const snapshot = this.buildPerformanceSnapshot();
|
||||
const json = JSON.stringify(snapshot, null, 2);
|
||||
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(json);
|
||||
} else {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = json;
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.left = "-9999px";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
this.copyStatus = "success";
|
||||
} catch (err) {
|
||||
console.warn("Failed to copy performance snapshot", err);
|
||||
this.copyStatus = "error";
|
||||
}
|
||||
|
||||
this.scheduleCopyStatusReset();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
@@ -297,41 +543,87 @@ export class PerformanceOverlay extends LitElement implements Layer {
|
||||
transform: none;
|
||||
`;
|
||||
|
||||
const copyLabel =
|
||||
this.copyStatus === "success"
|
||||
? translateText("performance_overlay.copied")
|
||||
: this.copyStatus === "error"
|
||||
? translateText("performance_overlay.failed_copy")
|
||||
: translateText("performance_overlay.copy_clipboard");
|
||||
|
||||
const maxLayerAvg =
|
||||
this.layerBreakdown.length > 0
|
||||
? Math.max(...this.layerBreakdown.map((l) => l.avg))
|
||||
: 1;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="performance-overlay ${this.isDragging ? "dragging" : ""}"
|
||||
style="${style}"
|
||||
@mousedown="${this.handleMouseDown}"
|
||||
>
|
||||
<button class="reset-button" @click="${this.handleReset}">
|
||||
${translateText("performance_overlay.reset")}
|
||||
</button>
|
||||
<button
|
||||
class="copy-json-button"
|
||||
@click="${this.handleCopyJson}"
|
||||
title="${translateText("performance_overlay.copy_json_title")}"
|
||||
>
|
||||
${copyLabel}
|
||||
</button>
|
||||
<button class="close-button" @click="${this.handleClose}">×</button>
|
||||
<div class="performance-line">
|
||||
FPS:
|
||||
${translateText("performance_overlay.fps")}
|
||||
<span class="${this.getPerformanceColor(this.currentFPS)}"
|
||||
>${this.currentFPS}</span
|
||||
>
|
||||
</div>
|
||||
<div class="performance-line">
|
||||
Avg (60s):
|
||||
${translateText("performance_overlay.avg_60s")}
|
||||
<span class="${this.getPerformanceColor(this.averageFPS)}"
|
||||
>${this.averageFPS}</span
|
||||
>
|
||||
</div>
|
||||
<div class="performance-line">
|
||||
Frame:
|
||||
${translateText("performance_overlay.frame")}
|
||||
<span class="${this.getPerformanceColor(1000 / this.frameTime)}"
|
||||
>${this.frameTime}ms</span
|
||||
>
|
||||
</div>
|
||||
<div class="performance-line">
|
||||
Tick Exec:
|
||||
${translateText("performance_overlay.tick_exec")}
|
||||
<span>${this.tickExecutionAvg.toFixed(2)}ms</span>
|
||||
(max: <span>${this.tickExecutionMax}ms</span>)
|
||||
</div>
|
||||
<div class="performance-line">
|
||||
Tick Delay:
|
||||
${translateText("performance_overlay.tick_delay")}
|
||||
<span>${this.tickDelayAvg.toFixed(2)}ms</span>
|
||||
(max: <span>${this.tickDelayMax}ms</span>)
|
||||
</div>
|
||||
${this.layerBreakdown.length
|
||||
? html`<div class="layers-section">
|
||||
<div class="performance-line">
|
||||
${translateText("performance_overlay.layers_header")}
|
||||
</div>
|
||||
${this.layerBreakdown.map((layer) => {
|
||||
const width = Math.min(
|
||||
100,
|
||||
(layer.avg / maxLayerAvg) * 100 || 0,
|
||||
);
|
||||
return html`<div class="layer-row">
|
||||
<span class="layer-name" title=${layer.name}
|
||||
>${layer.name}</span
|
||||
>
|
||||
<div class="layer-bar">
|
||||
<div class="layer-bar-fill" style="width: ${width}%;"></div>
|
||||
</div>
|
||||
<span class="layer-metrics">
|
||||
${layer.avg.toFixed(2)} / ${layer.max.toFixed(2)}ms
|
||||
</span>
|
||||
</div>`;
|
||||
})}
|
||||
</div>`
|
||||
: html``}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
DragEvent,
|
||||
MouseOverEvent,
|
||||
} from "../../InputHandler";
|
||||
import { FrameProfiler } from "../FrameProfiler";
|
||||
import { TransformHandler } from "../TransformHandler";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@@ -399,7 +400,9 @@ export class TerritoryLayer implements Layer {
|
||||
now > this.lastRefresh + this.refreshRate
|
||||
) {
|
||||
this.lastRefresh = now;
|
||||
const renderTerritoryStart = FrameProfiler.start();
|
||||
this.renderTerritory();
|
||||
FrameProfiler.end("TerritoryLayer:renderTerritory", renderTerritoryStart);
|
||||
|
||||
const [topLeft, bottomRight] = this.transformHandler.screenBoundingRect();
|
||||
const vx0 = Math.max(0, topLeft.x);
|
||||
@@ -411,6 +414,7 @@ export class TerritoryLayer implements Layer {
|
||||
const h = vy1 - vy0 + 1;
|
||||
|
||||
if (w > 0 && h > 0) {
|
||||
const putImageStart = FrameProfiler.start();
|
||||
this.context.putImageData(
|
||||
this.alternativeView ? this.alternativeImageData : this.imageData,
|
||||
0,
|
||||
@@ -420,9 +424,11 @@ export class TerritoryLayer implements Layer {
|
||||
w,
|
||||
h,
|
||||
);
|
||||
FrameProfiler.end("TerritoryLayer:putImageData", putImageStart);
|
||||
}
|
||||
}
|
||||
|
||||
const drawCanvasStart = FrameProfiler.start();
|
||||
context.drawImage(
|
||||
this.canvas,
|
||||
-this.game.width() / 2,
|
||||
@@ -430,7 +436,9 @@ export class TerritoryLayer implements Layer {
|
||||
this.game.width(),
|
||||
this.game.height(),
|
||||
);
|
||||
FrameProfiler.end("TerritoryLayer:drawCanvas", drawCanvasStart);
|
||||
if (this.game.inSpawnPhase()) {
|
||||
const highlightDrawStart = FrameProfiler.start();
|
||||
context.drawImage(
|
||||
this.highlightCanvas,
|
||||
-this.game.width() / 2,
|
||||
@@ -438,6 +446,10 @@ export class TerritoryLayer implements Layer {
|
||||
this.game.width(),
|
||||
this.game.height(),
|
||||
);
|
||||
FrameProfiler.end(
|
||||
"TerritoryLayer:drawHighlightCanvas",
|
||||
highlightDrawStart,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user