import { appConfig } from '../config'; interface TelemetrySnapshot { frameCpuStartedAt: number; encodeCpuMs: number; activeAgentCount: number; agentBudgetMax: number; canvas: HTMLCanvasElement; devicePixelRatio: number; renderSpeed: number; } const COMMON_DISPLAY_REFRESH_RATES = [ 50, 60, 72, 75, 90, 100, 120, 144, 165, 180, 240, ] as const; const DISPLAY_REFRESH_CONFIRMATION_FRAMES = 8; const DISPLAY_REFRESH_SNAP_TOLERANCE = 0.15; export class FramePerformance { public latestFps = 60; public smoothedFps = 60; public displayRefreshFps = 60; public readonly refreshTargetFps = 60; private lastTelemetryAt = 0; private hasConfirmedDisplayRefreshFps = false; private pendingDisplayRefreshFps = 0; private pendingDisplayRefreshFrameCount = 0; public markCpuStart(): number { return appConfig.telemetry.enabled ? performance.now() : 0; } public measureSince(startedAt: number): number { return appConfig.telemetry.enabled ? performance.now() - startedAt : 0; } public update(deltaTime: number): void { const fps = 1 / Math.max(deltaTime, appConfig.deltaTime.minDeltaTimeSeconds); this.latestFps = fps; this.updateDisplayRefreshEstimate(fps); this.smoothedFps = this.smoothedFps * appConfig.simulation.budget.fpsSmoothingRetain + fps * appConfig.simulation.budget.fpsSmoothingNew; } public renderTelemetry({ frameCpuStartedAt, encodeCpuMs, activeAgentCount, agentBudgetMax, canvas, devicePixelRatio, renderSpeed, }: TelemetrySnapshot): void { if (!appConfig.telemetry.enabled) { return; } const now = performance.now(); if (now - this.lastTelemetryAt < appConfig.telemetry.intervalMs) { return; } this.lastTelemetryAt = now; console.debug('Fleeting Garden telemetry', { fps: Math.round(this.latestFps), smoothedFps: Math.round(this.smoothedFps), refreshTargetFps: Math.round(this.refreshTargetFps), displayRefreshFps: Math.round(this.displayRefreshFps), activeAgentCount, agentBudgetMax, canvasWidth: canvas.width, canvasHeight: canvas.height, dpr: devicePixelRatio, renderSpeed, frameCpuMs: now - frameCpuStartedAt, encodeCpuMs, }); } private updateDisplayRefreshEstimate(fps: number): void { const displayRefreshFps = this.snapDisplayRefreshRate(fps); if (displayRefreshFps === null) { this.resetPendingDisplayRefreshEstimate(); return; } if ( this.hasConfirmedDisplayRefreshFps && displayRefreshFps < this.displayRefreshFps ) { this.resetPendingDisplayRefreshEstimate(); return; } if (displayRefreshFps !== this.pendingDisplayRefreshFps) { this.pendingDisplayRefreshFps = displayRefreshFps; this.pendingDisplayRefreshFrameCount = 1; } else { this.pendingDisplayRefreshFrameCount += 1; } if (this.pendingDisplayRefreshFrameCount < DISPLAY_REFRESH_CONFIRMATION_FRAMES) { return; } this.displayRefreshFps = displayRefreshFps; this.hasConfirmedDisplayRefreshFps = true; this.resetPendingDisplayRefreshEstimate(); } private snapDisplayRefreshRate(fps: number): number | null { if (!Number.isFinite(fps) || fps <= 0) { return null; } let nearestRefreshRate: number = COMMON_DISPLAY_REFRESH_RATES[0]; let nearestDifference = Math.abs(fps - nearestRefreshRate); COMMON_DISPLAY_REFRESH_RATES.forEach((refreshRate) => { const difference = Math.abs(fps - refreshRate); if (difference < nearestDifference) { nearestRefreshRate = refreshRate; nearestDifference = difference; } }); return nearestDifference / nearestRefreshRate <= DISPLAY_REFRESH_SNAP_TOLERANCE ? nearestRefreshRate : null; } private resetPendingDisplayRefreshEstimate(): void { this.pendingDisplayRefreshFps = 0; this.pendingDisplayRefreshFrameCount = 0; } }