mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 03:43:48 +00:00
766 lines
20 KiB
TypeScript
766 lines
20 KiB
TypeScript
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 {
|
||
TickMetricsEvent,
|
||
TogglePerformanceOverlayEvent,
|
||
} from "../../InputHandler";
|
||
import { translateText } from "../../Utils";
|
||
import { FrameProfiler } from "../FrameProfiler";
|
||
import { Layer } from "./Layer";
|
||
|
||
@customElement("performance-overlay")
|
||
export class PerformanceOverlay extends LitElement implements Layer {
|
||
@property({ type: Object })
|
||
public eventBus!: EventBus;
|
||
|
||
@property({ type: Object })
|
||
public userSettings!: UserSettings;
|
||
|
||
@state()
|
||
private currentFPS: number = 0;
|
||
|
||
@state()
|
||
private averageFPS: number = 0;
|
||
|
||
@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;
|
||
|
||
@state()
|
||
private isDragging: boolean = false;
|
||
|
||
@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[] = [];
|
||
private fpsHistory: number[] = [];
|
||
private lastSecondTime: number = 0;
|
||
private framesThisSecond: number = 0;
|
||
private dragStart: { x: number; y: number } = { x: 0, y: 0 };
|
||
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;
|
||
top: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(0, 0, 0, 0.8);
|
||
color: white;
|
||
padding: 8px 16px;
|
||
border-radius: 4px;
|
||
font-family: monospace;
|
||
font-size: 12px;
|
||
z-index: 9999;
|
||
user-select: none;
|
||
cursor: move;
|
||
transition: none;
|
||
min-width: 420px;
|
||
}
|
||
|
||
.performance-overlay.dragging {
|
||
cursor: grabbing;
|
||
transition: none;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.performance-line {
|
||
margin: 2px 0;
|
||
}
|
||
|
||
.performance-good {
|
||
color: #4ade80; /* green-400 */
|
||
}
|
||
|
||
.performance-warning {
|
||
color: #fbbf24; /* amber-400 */
|
||
}
|
||
|
||
.performance-bad {
|
||
color: #f87171; /* red-400 */
|
||
}
|
||
|
||
.close-button {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
width: 20px;
|
||
height: 20px;
|
||
background-color: rgba(0, 0, 0, 0.8);
|
||
border-radius: 4px;
|
||
color: white;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
line-height: 1;
|
||
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() {
|
||
super();
|
||
}
|
||
|
||
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,
|
||
event.backlogTurns,
|
||
event.ticksPerRender,
|
||
event.workerTicksPerSecond,
|
||
event.renderTicksPerSecond,
|
||
event.tileUpdatesCount,
|
||
event.ringBufferUtilization,
|
||
event.ringBufferOverflows,
|
||
event.ringDrainTime,
|
||
);
|
||
});
|
||
}
|
||
|
||
setVisible(visible: boolean) {
|
||
this.isVisible = visible;
|
||
FrameProfiler.setEnabled(visible);
|
||
}
|
||
|
||
private handleClose() {
|
||
this.userSettings.togglePerformanceOverlay();
|
||
}
|
||
|
||
private handleMouseDown = (e: MouseEvent) => {
|
||
// Don't start dragging if clicking on 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;
|
||
}
|
||
|
||
this.isDragging = true;
|
||
this.dragStart = {
|
||
x: e.clientX - this.position.x,
|
||
y: e.clientY - this.position.y,
|
||
};
|
||
|
||
document.addEventListener("mousemove", this.handleMouseMove);
|
||
document.addEventListener("mouseup", this.handleMouseUp);
|
||
e.preventDefault();
|
||
};
|
||
|
||
private handleMouseMove = (e: MouseEvent) => {
|
||
if (!this.isDragging) return;
|
||
|
||
const newX = e.clientX - this.dragStart.x;
|
||
const newY = e.clientY - this.dragStart.y;
|
||
|
||
// Convert to percentage of viewport
|
||
const viewportWidth = window.innerWidth;
|
||
const viewportHeight = window.innerHeight;
|
||
|
||
this.position = {
|
||
x: Math.max(0, Math.min(viewportWidth - 100, newX)), // Keep within viewport bounds
|
||
y: Math.max(0, Math.min(viewportHeight - 100, newY)),
|
||
};
|
||
|
||
this.requestUpdate();
|
||
};
|
||
|
||
private handleMouseUp = () => {
|
||
this.isDragging = false;
|
||
document.removeEventListener("mousemove", this.handleMouseMove);
|
||
document.removeEventListener("mouseup", this.handleMouseUp);
|
||
};
|
||
|
||
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 = [];
|
||
|
||
// reset tile metrics
|
||
this.tileUpdatesPerRender = 0;
|
||
this.tileUpdatesPeak = 0;
|
||
this.ringBufferUtilization = 0;
|
||
this.ringBufferOverflows = 0;
|
||
this.ringDrainTime = 0;
|
||
this.totalTilesUpdated = 0;
|
||
|
||
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();
|
||
|
||
// Initialize timing on first call
|
||
if (this.lastTime === 0) {
|
||
this.lastTime = now;
|
||
this.lastSecondTime = now;
|
||
return;
|
||
}
|
||
|
||
const deltaTime = now - this.lastTime;
|
||
|
||
// Track frame times for current FPS calculation (last 60 frames)
|
||
this.frameTimes.push(deltaTime);
|
||
if (this.frameTimes.length > 60) {
|
||
this.frameTimes.shift();
|
||
}
|
||
|
||
// Calculate current FPS based on average frame time
|
||
if (this.frameTimes.length > 0) {
|
||
const avgFrameTime =
|
||
this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length;
|
||
this.currentFPS = Math.round(1000 / avgFrameTime);
|
||
this.frameTime = Math.round(avgFrameTime);
|
||
}
|
||
|
||
// Track FPS for 60-second average
|
||
this.framesThisSecond++;
|
||
|
||
// Update every second
|
||
if (now - this.lastSecondTime >= 1000) {
|
||
this.fpsHistory.push(this.framesThisSecond);
|
||
if (this.fpsHistory.length > 60) {
|
||
this.fpsHistory.shift();
|
||
}
|
||
|
||
// Calculate 60-second average
|
||
if (this.fpsHistory.length > 0) {
|
||
this.averageFPS = Math.round(
|
||
this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length,
|
||
);
|
||
}
|
||
|
||
this.framesThisSecond = 0;
|
||
this.lastSecondTime = now;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
@state()
|
||
private backlogTurns: number = 0;
|
||
|
||
@state()
|
||
private ticksPerRender: number = 0;
|
||
|
||
@state()
|
||
private workerTicksPerSecond: number = 0;
|
||
|
||
@state()
|
||
private renderTicksPerSecond: number = 0;
|
||
|
||
@state()
|
||
private tileUpdatesPerRender: number = 0;
|
||
|
||
@state()
|
||
private tileUpdatesPeak: number = 0;
|
||
|
||
@state()
|
||
private ringBufferUtilization: number = 0;
|
||
|
||
@state()
|
||
private ringBufferOverflows: number = 0;
|
||
|
||
@state()
|
||
private ringDrainTime: number = 0;
|
||
|
||
@state()
|
||
private totalTilesUpdated: number = 0;
|
||
|
||
updateTickMetrics(
|
||
tickExecutionDuration?: number,
|
||
tickDelay?: number,
|
||
backlogTurns?: number,
|
||
ticksPerRender?: number,
|
||
workerTicksPerSecond?: number,
|
||
renderTicksPerSecond?: number,
|
||
tileUpdatesCount?: number,
|
||
ringBufferUtilization?: number,
|
||
ringBufferOverflows?: number,
|
||
ringDrainTime?: 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));
|
||
}
|
||
}
|
||
|
||
if (backlogTurns !== undefined) {
|
||
this.backlogTurns = backlogTurns;
|
||
}
|
||
|
||
if (ticksPerRender !== undefined) {
|
||
this.ticksPerRender = ticksPerRender;
|
||
}
|
||
|
||
if (workerTicksPerSecond !== undefined) {
|
||
this.workerTicksPerSecond = workerTicksPerSecond;
|
||
}
|
||
|
||
if (renderTicksPerSecond !== undefined) {
|
||
this.renderTicksPerSecond = renderTicksPerSecond;
|
||
}
|
||
|
||
if (tileUpdatesCount !== undefined) {
|
||
this.tileUpdatesPerRender = tileUpdatesCount;
|
||
this.tileUpdatesPeak = Math.max(this.tileUpdatesPeak, tileUpdatesCount);
|
||
this.totalTilesUpdated += tileUpdatesCount;
|
||
}
|
||
|
||
if (ringBufferUtilization !== undefined) {
|
||
this.ringBufferUtilization =
|
||
Math.round(ringBufferUtilization * 100) / 100;
|
||
}
|
||
|
||
if (ringBufferOverflows !== undefined) {
|
||
// Accumulate overflows (overflows is a flag, so add 1 if set)
|
||
this.ringBufferOverflows += ringBufferOverflows;
|
||
}
|
||
|
||
if (ringDrainTime !== undefined) {
|
||
this.ringDrainTime = Math.round(ringDrainTime * 100) / 100;
|
||
}
|
||
|
||
this.requestUpdate();
|
||
}
|
||
|
||
shouldTransform(): boolean {
|
||
return false;
|
||
}
|
||
|
||
private getPerformanceColor(fps: number): string {
|
||
if (fps >= 55) return "performance-good";
|
||
if (fps >= 30) return "performance-warning";
|
||
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],
|
||
},
|
||
tiles: {
|
||
updatesPerRender: this.tileUpdatesPerRender,
|
||
peakUpdates: this.tileUpdatesPeak,
|
||
ringBufferUtilization: this.ringBufferUtilization,
|
||
ringBufferOverflows: this.ringBufferOverflows,
|
||
ringDrainTimeMs: this.ringDrainTime,
|
||
totalTilesUpdated: this.totalTilesUpdated,
|
||
},
|
||
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``;
|
||
}
|
||
|
||
const style = `
|
||
left: ${this.position.x}px;
|
||
top: ${this.position.y}px;
|
||
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">
|
||
${translateText("performance_overlay.fps")}
|
||
<span class="${this.getPerformanceColor(this.currentFPS)}"
|
||
>${this.currentFPS}</span
|
||
>
|
||
</div>
|
||
<div class="performance-line">
|
||
${translateText("performance_overlay.avg_60s")}
|
||
<span class="${this.getPerformanceColor(this.averageFPS)}"
|
||
>${this.averageFPS}</span
|
||
>
|
||
</div>
|
||
<div class="performance-line">
|
||
${translateText("performance_overlay.frame")}
|
||
<span class="${this.getPerformanceColor(1000 / this.frameTime)}"
|
||
>${this.frameTime}ms</span
|
||
>
|
||
</div>
|
||
<div class="performance-line">
|
||
${translateText("performance_overlay.tick_exec")}
|
||
<span>${this.tickExecutionAvg.toFixed(2)}ms</span>
|
||
(max: <span>${this.tickExecutionMax}ms</span>)
|
||
</div>
|
||
<div class="performance-line">
|
||
${translateText("performance_overlay.tick_delay")}
|
||
<span>${this.tickDelayAvg.toFixed(2)}ms</span>
|
||
(max: <span>${this.tickDelayMax}ms</span>)
|
||
</div>
|
||
<div class="performance-line">
|
||
Worker ticks/s:
|
||
<span>${this.workerTicksPerSecond.toFixed(1)}</span>
|
||
</div>
|
||
<div class="performance-line">
|
||
Render ticks/s:
|
||
<span>${this.renderTicksPerSecond.toFixed(1)}</span>
|
||
</div>
|
||
<div class="performance-line">
|
||
Ticks per render:
|
||
<span>${this.ticksPerRender}</span>
|
||
</div>
|
||
<div class="performance-line">
|
||
Backlog turns:
|
||
<span>${this.backlogTurns}</span>
|
||
</div>
|
||
<div class="performance-line">
|
||
Tile updates/render:
|
||
<span>${this.tileUpdatesPerRender}</span>
|
||
(peak: <span>${this.tileUpdatesPeak}</span>)
|
||
</div>
|
||
<div class="performance-line">
|
||
Ring buffer:
|
||
<span>${this.ringBufferUtilization}%</span>
|
||
(${this.totalTilesUpdated} total, ${this.ringBufferOverflows}
|
||
overflows)
|
||
</div>
|
||
<div class="performance-line">
|
||
Ring drain time:
|
||
<span>${this.ringDrainTime.toFixed(2)}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>
|
||
`;
|
||
}
|
||
}
|