/** * RollingAverage - Maintains a rolling window of values for smooth averaging */ class RollingAverage { constructor(capacity) { this.values = []; this.capacity = capacity; this.sum = 0; } push(value) { if (this.values.length >= this.capacity) { const old = this.values.shift(); this.sum -= old; } this.values.push(value); this.sum += value; } average() { return this.values.length > 0 ? this.sum / this.values.length : 0; } min() { return this.values.length > 0 ? Math.min(...this.values) : 0; } max() { return this.values.length > 0 ? Math.max(...this.values) : 0; } } /** * PerformanceMetrics - Tracks and analyzes frame performance */ export class PerformanceMetrics { constructor(rollingWindow = 60, historyCapacity = 10000) { // Rolling averages this.frameTime = new RollingAverage(rollingWindow); this.updateTime = new RollingAverage(rollingWindow); this.renderTime = new RollingAverage(rollingWindow); this.vertexCount = new RollingAverage(rollingWindow); this.lineCount = new RollingAverage(rollingWindow); // History for export this.history = []; this.historyCapacity = historyCapacity; // Frame timing this.frameStart = 0; this.updateStart = 0; this.renderStart = 0; this.totalFrames = 0; } beginFrame() { this.frameStart = performance.now(); } beginUpdate() { this.updateStart = performance.now(); } endUpdate() { const duration = performance.now() - this.updateStart; return duration; } beginRender() { this.renderStart = performance.now(); } endRender() { const duration = performance.now() - this.renderStart; return duration; } endFrame(updateMs, renderMs, vertexCount, lineCount) { const totalMs = performance.now() - this.frameStart; // Update rolling averages this.frameTime.push(totalMs); this.updateTime.push(updateMs); this.renderTime.push(renderMs); this.vertexCount.push(vertexCount); this.lineCount.push(lineCount); // Store in history const record = { frame: this.totalFrames, totalMs, updateMs, renderMs, vertexCount, lineCount, fps: totalMs > 0 ? 1000 / totalMs : 0, }; if (this.history.length >= this.historyCapacity) { this.history.shift(); } this.history.push(record); this.totalFrames++; } getFPS() { const avg = this.frameTime.average(); return avg > 0 ? 1000 / avg : 0; } getMinFPS() { const max = this.frameTime.max(); return max > 0 ? 1000 / max : 0; } getMaxFPS() { const min = this.frameTime.min(); return min > 0 ? 1000 / min : 0; } formatSummary() { return `FPS: ${this.getFPS().toFixed(1)} (min: ${this.getMinFPS().toFixed(1)}, max: ${this.getMaxFPS().toFixed(1)}) | ` + `Frame: ${this.frameTime.average().toFixed(2)}ms | ` + `Update: ${this.updateTime.average().toFixed(2)}ms | ` + `Render: ${this.renderTime.average().toFixed(2)}ms | ` + `Vertices: ${Math.round(this.vertexCount.average())} | ` + `Lines: ${Math.round(this.lineCount.average())}`; } exportToCSV() { let csv = 'frame,total_ms,update_ms,render_ms,vertex_count,line_count,fps\n'; for (const record of this.history) { csv += `${record.frame},${record.totalMs},${record.updateMs},${record.renderMs},` + `${record.vertexCount},${record.lineCount},${record.fps}\n`; } return csv; } }