• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

graphty-org / graphty-monorepo / 20661584252

02 Jan 2026 03:50PM UTC coverage: 77.924% (+7.3%) from 70.62%
20661584252

push

github

apowers313
ci: fix flakey performance test

13438 of 17822 branches covered (75.4%)

Branch coverage included in aggregate %.

41247 of 52355 relevant lines covered (78.78%)

145534.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.46
/graphty-element/src/managers/StatsManager.ts
1
import {
15✔
2
    type Engine,
3
    EngineInstrumentation,
14✔
4
    PerfCounter,
14✔
5
    type Scene,
6
    SceneInstrumentation,
14✔
7
    type WebGPUEngine,
8
} from "@babylonjs/core";
14✔
9

10
import type { EventManager } from "./EventManager";
11
import type { Manager } from "./interfaces";
12

13
/**
14
 * Internal measurement statistics for profiling
15
 */
16
interface MeasurementStats {
17
    label: string;
18
    count: number;
19
    total: number;
20
    min: number;
21
    max: number;
22
    avg: number;
23
    lastDuration: number;
24
    durations: number[]; // Ring buffer for percentile calculation
25
    durationsIndex: number; // Circular buffer index
26
    durationsFilled: boolean; // Track if buffer is full
27
}
28

29
/**
30
 * Counter statistics
31
 */
32
interface CounterStats {
33
    label: string;
34
    value: number;
35
    operations: number; // Number of increment/decrement/set operations
36
}
37

38
/**
39
 * Counter snapshot for reporting
40
 */
41
export interface CounterSnapshot {
42
    label: string;
43
    value: number;
44
    operations: number;
45
}
46

47
/**
48
 * Comprehensive performance counter data
49
 */
50
interface PerfCounterSnapshot {
51
    current: number;
52
    avg: number;
53
    min: number;
54
    max: number;
55
    total: number;
56
    lastSecAvg: number;
57
}
58

59
/**
60
 * Draw calls counter data (includes count in addition to timing)
61
 */
62
interface DrawCallsSnapshot extends PerfCounterSnapshot {
63
    count: number;
64
}
65

66
/**
67
 * Layout session performance metrics
68
 * Tracks the complete layout session from start to settlement
69
 */
70
export interface LayoutSessionMetrics {
71
    startTime: number;
72
    endTime: number;
73
    totalElapsed: number;
74
    frameCount: number;
75
    totalCpuTime: number;
76
    totalGpuTime: number;
77
    blockingOverhead: number;
78
    percentages: {
79
        cpu: number;
80
        gpu: number;
81
        blocking: number;
82
    };
83
    perFrame: {
84
        total: number;
85
        cpu: number;
86
        gpu: number;
87
        blocking: number;
88
    };
89
}
90

91
/**
92
 * Frame-level profiling data
93
 * Tracks operations and blocking within a single frame
94
 */
95
interface FrameProfile {
96
    frameNumber: number;
97
    operations: { label: string; duration: number }[];
98
    totalCpuTime: number;
99
    interFrameTime: number;
100
    blockingTime: number;
101
    blockingRatio: number;
102
}
103

104
/**
105
 * Operation blocking correlation statistics
106
 * Shows which operations correlate with high blocking
107
 */
108
export interface OperationBlockingStats {
109
    label: string;
110
    totalCpuTime: number;
111
    appearanceCount: number;
112
    highBlockingFrames: number;
113
    highBlockingPercentage: number;
114
    avgBlockingRatioWhenPresent: number;
115
}
116

117
/**
118
 * Performance snapshot including CPU, GPU, and scene metrics
119
 */
120
export interface PerformanceSnapshot {
121
    cpu: {
122
        label: string;
123
        count: number;
124
        total: number;
125
        min: number;
126
        max: number;
127
        avg: number;
128
        p50: number;
129
        p95: number;
130
        p99: number;
131
        lastDuration: number;
132
    }[];
133
    gpu?: {
134
        gpuFrameTime: PerfCounterSnapshot;
135
        shaderCompilation: PerfCounterSnapshot;
136
    };
137
    scene?: {
138
        frameTime: PerfCounterSnapshot;
139
        renderTime: PerfCounterSnapshot;
140
        interFrameTime: PerfCounterSnapshot;
141
        cameraRenderTime: PerfCounterSnapshot;
142
        activeMeshesEvaluation: PerfCounterSnapshot;
143
        renderTargetsRenderTime: PerfCounterSnapshot;
144
        drawCalls: DrawCallsSnapshot;
145
    };
146
    layoutSession?: LayoutSessionMetrics;
147
    timestamp: number;
148
}
149

150
/**
151
 * Manages performance statistics and monitoring
152
 * Centralizes all performance tracking and reporting
153
 */
154
export class StatsManager implements Manager {
15✔
155
    private static readonly RING_BUFFER_SIZE = 1000;
16✔
156

157
    // Scene and engine instrumentation
158
    private sceneInstrumentation: SceneInstrumentation | null = null;
16✔
159
    private babylonInstrumentation: EngineInstrumentation | null = null;
16✔
160

161
    // Performance counters
162
    graphStep = new PerfCounter();
16✔
163
    nodeUpdate = new PerfCounter();
16✔
164
    edgeUpdate = new PerfCounter();
16✔
165
    arrowCapUpdate = new PerfCounter();
16✔
166
    intersectCalc = new PerfCounter();
16✔
167
    loadTime = new PerfCounter();
16✔
168
    totalUpdates = 0;
16✔
169

170
    // Cache statistics (will be updated by other managers)
171
    private meshCacheHits = 0;
16✔
172
    private meshCacheMisses = 0;
16✔
173
    private nodeCount = 0;
16✔
174
    private edgeCount = 0;
16✔
175

176
    // Profiling fields
177
    private enabled = false;
16✔
178
    private measurements = new Map<string, MeasurementStats>();
16✔
179
    private activeStack: { label: string; startTime: number }[] = [];
16✔
180
    private counters = new Map<string, CounterStats>();
16✔
181

182
    // Layout session tracking
183
    private layoutSessionStartTime: number | null = null;
16✔
184
    private layoutSessionEndTime: number | null = null;
16✔
185

186
    // Frame-level blocking detection
187
    private frameProfilingEnabled = false;
16✔
188
    private frameProfiles: FrameProfile[] = [];
16✔
189
    private currentFrameOperations: { label: string; duration: number }[] = [];
16✔
190
    private currentFrameNumber = 0;
16✔
191
    private longTaskObserver: PerformanceObserver | null = null;
16✔
192

193
    /**
194
     * Creates a new stats manager for performance tracking
195
     * @param eventManager - Event manager for emitting stats events
196
     */
197
    constructor(private eventManager: EventManager) {}
16✔
198

199
    /**
200
     * Initialize the stats manager
201
     * @returns Promise that resolves when initialization is complete
202
     */
203
    async init(): Promise<void> {
16✔
204
        // StatsManager doesn't need async initialization
205
        return Promise.resolve();
920✔
206
    }
920✔
207

208
    /**
209
     * Dispose the stats manager and clean up instrumentation
210
     */
211
    dispose(): void {
16✔
212
        // Dispose instrumentation
213
        if (this.sceneInstrumentation) {
779!
214
            this.sceneInstrumentation.dispose();
778✔
215
            this.sceneInstrumentation = null;
778✔
216
        }
778✔
217

218
        if (this.babylonInstrumentation) {
779!
219
            this.babylonInstrumentation.dispose();
778✔
220
            this.babylonInstrumentation = null;
778✔
221
        }
778✔
222

223
        // Reset counters
224
        this.resetCounters();
779✔
225

226
        // Clear profiling state
227
        this.measurements.clear();
779✔
228
        this.activeStack = [];
779✔
229
        this.counters.clear();
779✔
230
        this.enabled = false;
779✔
231
    }
779✔
232

233
    /**
234
     * Initialize Babylon.js instrumentation
235
     * Should be called after scene and engine are created
236
     * @param scene - The Babylon.js scene
237
     * @param engine - The Babylon.js engine (Engine or WebGPUEngine)
238
     */
239
    initializeBabylonInstrumentation(scene: Scene, engine: Engine | WebGPUEngine): void {
16✔
240
        // Scene instrumentation
241
        this.sceneInstrumentation = new SceneInstrumentation(scene);
991✔
242
        this.sceneInstrumentation.captureFrameTime = true;
991✔
243
        this.sceneInstrumentation.captureRenderTime = true;
991✔
244
        this.sceneInstrumentation.captureInterFrameTime = true;
991✔
245
        this.sceneInstrumentation.captureCameraRenderTime = true;
991✔
246
        this.sceneInstrumentation.captureActiveMeshesEvaluationTime = true;
991✔
247
        this.sceneInstrumentation.captureRenderTargetsRenderTime = true;
991✔
248

249
        // Engine instrumentation
250
        this.babylonInstrumentation = new EngineInstrumentation(engine);
991✔
251
        this.babylonInstrumentation.captureGPUFrameTime = true;
991✔
252
        this.babylonInstrumentation.captureShaderCompilationTime = true;
991✔
253
    }
991✔
254

255
    /**
256
     * Inject mocked instrumentation for testing
257
     * @param engineInstrumentation - Mock engine instrumentation
258
     * @param sceneInstrumentation - Mock scene instrumentation
259
     * @internal
260
     */
261
    _injectMockInstrumentation(
16✔
262
        engineInstrumentation?: EngineInstrumentation | null,
8✔
263
        sceneInstrumentation?: SceneInstrumentation | null,
8✔
264
    ): void {
8✔
265
        if (engineInstrumentation !== undefined) {
8✔
266
            this.babylonInstrumentation = engineInstrumentation;
3✔
267
        }
3✔
268

269
        if (sceneInstrumentation !== undefined) {
8✔
270
            this.sceneInstrumentation = sceneInstrumentation;
6✔
271
        }
6✔
272
    }
8✔
273

274
    /**
275
     * Update cache statistics
276
     * @param hits - Number of cache hits
277
     * @param misses - Number of cache misses
278
     */
279
    updateCacheStats(hits: number, misses: number): void {
16✔
280
        this.meshCacheHits = hits;
10,400✔
281
        this.meshCacheMisses = misses;
10,400✔
282
    }
10,400✔
283

284
    /**
285
     * Update node/edge counts
286
     * @param nodeCount - Current number of nodes
287
     * @param edgeCount - Current number of edges
288
     */
289
    updateCounts(nodeCount: number, edgeCount: number): void {
16✔
290
        this.nodeCount = nodeCount;
10,401✔
291
        this.edgeCount = edgeCount;
10,401✔
292
    }
10,401✔
293

294
    /**
295
     * Increment update counter
296
     */
297
    step(): void {
16✔
298
        this.totalUpdates++;
10,045✔
299

300
        // Emit stats update event periodically (every 60 updates)
301
        if (this.totalUpdates % 60 === 0) {
10,045✔
302
            this.eventManager.emitGraphEvent("stats-update", {
76✔
303
                totalUpdates: this.totalUpdates,
76✔
304
                stats: this.getStats(),
76✔
305
            });
76✔
306
        }
76✔
307
    }
10,045✔
308

309
    /**
310
     * Reset all counters
311
     */
312
    reset(): void {
16✔
313
        this.totalUpdates = 0;
1✔
314
        this.resetCounters();
1✔
315
    }
1✔
316

317
    /**
318
     * Reset performance counters
319
     */
320
    private resetCounters(): void {
16✔
321
        this.graphStep = new PerfCounter();
780✔
322
        this.nodeUpdate = new PerfCounter();
780✔
323
        this.edgeUpdate = new PerfCounter();
780✔
324
        this.arrowCapUpdate = new PerfCounter();
780✔
325
        this.intersectCalc = new PerfCounter();
780✔
326
        this.loadTime = new PerfCounter();
780✔
327
        this.totalUpdates = 0;
780✔
328
    }
780✔
329

330
    /**
331
     * Get current statistics
332
     * @returns Current graph statistics including counts and performance metrics
333
     */
334
    getStats(): {
16✔
335
        numNodes: number;
336
        numEdges: number;
337
        totalUpdates: number;
338
        meshCacheHits: number;
339
        meshCacheMisses: number;
340
        nodeUpdateCount: number;
341
        edgeUpdateCount: number;
342
        arrowCapUpdateCount: number;
343
    } {
81✔
344
        return {
81✔
345
            numNodes: this.nodeCount,
81✔
346
            numEdges: this.edgeCount,
81✔
347
            totalUpdates: this.totalUpdates,
81✔
348
            meshCacheHits: this.meshCacheHits,
81✔
349
            meshCacheMisses: this.meshCacheMisses,
81✔
350
            nodeUpdateCount: this.nodeUpdate.count,
81✔
351
            edgeUpdateCount: this.edgeUpdate.count,
81✔
352
            arrowCapUpdateCount: this.arrowCapUpdate.count,
81✔
353
        };
81✔
354
    }
81✔
355

356
    /**
357
     * Generate a human-readable statistics report
358
     * @returns Formatted string with all statistics
359
     */
360
    toString(): string {
16✔
361
        let statsStr = "";
1✔
362

363
        function appendStat(name: string, stat: string | number, units = ""): void {
1✔
364
            statsStr += `${name}: ${stat}${units}\n`;
8✔
365
        }
8✔
366

367
        function statsSection(name: string): void {
1✔
368
            statsStr += `\n${name}\n`;
2✔
369
             
370
            for (let i = 0; i < name.length; i++) {
2✔
371
                statsStr += "-";
29✔
372
            }
29✔
373
            statsStr += "\n";
2✔
374
        }
2✔
375

376
        function appendPerf(name: string, stat: PerfCounter, multiplier = 1): void {
1✔
377
            statsStr += `${name} (min/avg/last sec/max [total]): `;
6✔
378
            statsStr += `${(stat.min * multiplier).toFixed(2)} / `;
6✔
379
            statsStr += `${(stat.average * multiplier).toFixed(2)} / `;
6✔
380
            statsStr += `${(stat.lastSecAverage * multiplier).toFixed(2)} / `;
6✔
381
            statsStr += `${(stat.max * multiplier).toFixed(2)} `;
6✔
382
            statsStr += `[${(stat.total * multiplier).toFixed(2)}] ms\n`;
6✔
383
        }
6✔
384

385
        // Graph statistics
386
        statsSection("Graph");
1✔
387
        appendStat("Num Nodes", this.nodeCount);
1✔
388
        appendStat("Num Edges", this.edgeCount);
1✔
389
        appendStat("Total Updates", this.totalUpdates);
1✔
390
        appendStat("Mesh Cache Hits", this.meshCacheHits);
1✔
391
        appendStat("Mesh Cache Misses", this.meshCacheMisses);
1✔
392
        appendStat("Number of Node Updates", this.nodeUpdate.count);
1✔
393
        appendStat("Number of Edge Updates", this.edgeUpdate.count);
1✔
394
        appendStat("Number of ArrowCap Updates", this.arrowCapUpdate.count);
1✔
395

396
        // Graph engine performance
397
        statsSection("Graph Engine Performance");
1✔
398
        appendPerf("JSON Load Time", this.loadTime);
1✔
399
        appendPerf("Graph Physics Engine Time", this.graphStep);
1✔
400
        appendPerf("Node Update Time", this.nodeUpdate);
1✔
401
        appendPerf("Edge Update Time", this.edgeUpdate);
1✔
402
        appendPerf("Arrow Cap Update Time", this.arrowCapUpdate);
1✔
403
        appendPerf("Ray Intersect Calculation Time", this.intersectCalc);
1✔
404

405
        // BabylonJS performance (if available)
406
        if (this.sceneInstrumentation && this.babylonInstrumentation) {
1!
407
            statsSection("BabylonJS Performance");
×
408
            appendStat("Draw Calls", this.sceneInstrumentation.drawCallsCounter.count);
×
409
            appendPerf("GPU Time", this.babylonInstrumentation.gpuFrameTimeCounter, 0.000001);
×
410
            appendPerf("Shader Time", this.babylonInstrumentation.shaderCompilationTimeCounter);
×
411
            appendPerf("Mesh Evaluation Time", this.sceneInstrumentation.activeMeshesEvaluationTimeCounter);
×
412
            appendPerf("Render Targets Time", this.sceneInstrumentation.renderTargetsRenderTimeCounter);
×
413
            appendPerf("Draw Calls Time", this.sceneInstrumentation.drawCallsCounter);
×
414
            appendPerf("Frame Time", this.sceneInstrumentation.frameTimeCounter);
×
415
            appendPerf("Render Time", this.sceneInstrumentation.renderTimeCounter);
×
416
            appendPerf("Time Between Frames", this.sceneInstrumentation.interFrameTimeCounter);
×
417
            appendPerf("Camera Render Time", this.sceneInstrumentation.cameraRenderTimeCounter);
×
418
        }
×
419

420
        return statsStr;
1✔
421
    }
1✔
422

423
    /**
424
     * Get performance summary
425
     * @returns Summary of key performance metrics
426
     */
427
    getPerformanceSummary(): {
16✔
428
        fps: number;
429
        frameTime: number;
430
        renderTime: number;
431
        gpuTime: number;
432
        drawCalls: number;
433
    } {
16✔
434
        if (!this.sceneInstrumentation || !this.babylonInstrumentation) {
16!
435
            return {
1✔
436
                fps: 0,
1✔
437
                frameTime: 0,
1✔
438
                renderTime: 0,
1✔
439
                gpuTime: 0,
1✔
440
                drawCalls: 0,
1✔
441
            };
1✔
442
        }
1!
443

444
        return {
15✔
445
            fps:
15✔
446
                this.sceneInstrumentation.frameTimeCounter.average > 0
15✔
447
                    ? 1000 / this.sceneInstrumentation.frameTimeCounter.average
15!
448
                    : 0,
×
449
            frameTime: this.sceneInstrumentation.frameTimeCounter.lastSecAverage,
16✔
450
            renderTime: this.sceneInstrumentation.renderTimeCounter.lastSecAverage,
16✔
451
            gpuTime: this.babylonInstrumentation.gpuFrameTimeCounter.lastSecAverage * 0.000001,
16✔
452
            drawCalls: this.sceneInstrumentation.drawCallsCounter.count,
16✔
453
        };
16✔
454
    }
16✔
455

456
    /**
457
     * Enable detailed profiling
458
     */
459
    enableProfiling(): void {
16✔
460
        this.enabled = true;
83✔
461
    }
83✔
462

463
    /**
464
     * Disable detailed profiling and clear measurements
465
     */
466
    disableProfiling(): void {
16✔
467
        this.enabled = false;
28✔
468
        this.measurements.clear();
28✔
469
        this.activeStack = [];
28✔
470
        this.counters.clear();
28✔
471
    }
28✔
472

473
    /**
474
     * Enable frame-level blocking detection
475
     * This tracks operations within each frame and correlates them with inter-frame time
476
     * to identify which operations cause blocking overhead
477
     */
478
    enableFrameProfiling(): void {
16✔
479
        this.frameProfilingEnabled = true;
32✔
480
        this.frameProfiles = [];
32✔
481
        this.currentFrameNumber = 0;
32✔
482

483
        // Setup Long Task observer to detect >50ms blocking
484
        if (typeof PerformanceObserver !== "undefined") {
32✔
485
            try {
32✔
486
                this.longTaskObserver = new PerformanceObserver((list) => {
32✔
487
                    for (const entry of list.getEntries()) {
×
488
                        // eslint-disable-next-line no-console
489
                        console.log(
×
490
                            `⚠️ Long Task Detected (>50ms blocking): ${entry.duration.toFixed(2)}ms at ${entry.startTime.toFixed(2)}ms`,
×
491
                        );
×
492
                    }
×
493
                });
32✔
494
                this.longTaskObserver.observe({ type: "longtask", buffered: true });
32✔
495
            } catch {
32!
496
                // Long Task API not supported in this browser
497
            }
×
498
        }
32✔
499
    }
32✔
500

501
    /**
502
     * Disable frame-level blocking detection and clear data
503
     */
504
    disableFrameProfiling(): void {
16✔
505
        this.frameProfilingEnabled = false;
28✔
506
        this.frameProfiles = [];
28✔
507
        this.currentFrameOperations = [];
28✔
508

509
        if (this.longTaskObserver) {
28✔
510
            this.longTaskObserver.disconnect();
28✔
511
            this.longTaskObserver = null;
28✔
512
        }
28✔
513
    }
28✔
514

515
    /**
516
     * Start profiling a new frame
517
     * Should be called at the beginning of each frame
518
     */
519
    startFrameProfiling(): void {
16✔
520
        if (!this.frameProfilingEnabled) {
28,076!
521
            return;
28,071✔
522
        }
28,071!
523

524
        this.currentFrameNumber++;
5✔
525
        this.currentFrameOperations = [];
5✔
526
    }
28,076✔
527

528
    /**
529
     * End profiling for the current frame
530
     * Should be called at the end of each frame
531
     */
532
    endFrameProfiling(): void {
16✔
533
        if (!this.frameProfilingEnabled || !this.sceneInstrumentation) {
28,076!
534
            return;
28,072✔
535
        }
28,072!
536

537
        const totalCpuTime = this.currentFrameOperations.reduce((sum, op) => sum + op.duration, 0);
4✔
538
        const interFrameTime = this.sceneInstrumentation.interFrameTimeCounter.current;
4✔
539
        const blockingTime = Math.max(0, interFrameTime - totalCpuTime);
4✔
540
        const blockingRatio = totalCpuTime > 0 ? blockingTime / totalCpuTime : 0;
28,076!
541

542
        const profile: FrameProfile = {
28,076✔
543
            frameNumber: this.currentFrameNumber,
28,076✔
544
            operations: [...this.currentFrameOperations],
28,076✔
545
            totalCpuTime,
28,076✔
546
            interFrameTime,
28,076✔
547
            blockingTime,
28,076✔
548
            blockingRatio,
28,076✔
549
        };
28,076✔
550

551
        this.frameProfiles.push(profile);
28,076✔
552

553
        // Keep only last 100 frames to avoid memory issues
554
        if (this.frameProfiles.length > 100) {
28,076!
555
            this.frameProfiles.shift();
×
556
        }
✔
557

558
        // Flag high-blocking frames (blocking > 2x CPU time AND > 20ms frame time)
559
        if (blockingRatio > 2.0 && interFrameTime > 20) {
28,076!
560
            this.reportHighBlockingFrame(profile);
2✔
561
        }
2✔
562
    }
28,076✔
563

564
    /**
565
     * Report a frame with high blocking overhead
566
     * @param profile - Frame profile data with blocking information
567
     */
568
    private reportHighBlockingFrame(profile: FrameProfile): void {
16✔
569
        const topOps = [...profile.operations].sort((a, b) => b.duration - a.duration).slice(0, 5);
2✔
570

571
        // eslint-disable-next-line no-console
572
        console.log(`⚠️ High Blocking Frame #${profile.frameNumber}:`);
2✔
573
        // eslint-disable-next-line no-console
574
        console.log(`├─ Total Frame Time: ${profile.interFrameTime.toFixed(2)}ms`);
2✔
575
        // eslint-disable-next-line no-console
576
        console.log(`├─ CPU Time: ${profile.totalCpuTime.toFixed(2)}ms`);
2✔
577
        // eslint-disable-next-line no-console
578
        console.log(
2✔
579
            `├─ Blocking Time: ${profile.blockingTime.toFixed(2)}ms (${profile.blockingRatio.toFixed(2)}x CPU time)`,
2✔
580
        );
2✔
581
        // eslint-disable-next-line no-console
582
        console.log("└─ Top Operations:");
2✔
583
        topOps.forEach((op, i) => {
2✔
584
            // eslint-disable-next-line no-console
585
            console.log(`   ${i + 1}. ${op.label}: ${op.duration.toFixed(2)}ms`);
2✔
586
        });
2✔
587
    }
2✔
588

589
    /**
590
     * Get blocking correlation report
591
     * Shows which operations appear most often in high-blocking frames
592
     * @returns Array of operation statistics sorted by blocking correlation
593
     */
594
    getBlockingReport(): OperationBlockingStats[] {
16✔
595
        if (this.frameProfiles.length === 0) {
31!
596
            return [];
27✔
597
        }
27!
598

599
        const operationStats = new Map<
4✔
600
            string,
601
            {
602
                totalCpuTime: number;
603
                appearanceCount: number;
604
                highBlockingFrames: number;
605
                blockingRatiosWhenPresent: number[];
606
            }
607
        >();
4✔
608

609
        const highBlockingThreshold = 1.0; // Blocking > 1x CPU time
4✔
610

611
        for (const frame of this.frameProfiles) {
4✔
612
            const isHighBlocking = frame.blockingRatio > highBlockingThreshold;
4✔
613

614
            const opsInFrame = new Set<string>();
4✔
615
            for (const op of frame.operations) {
4✔
616
                opsInFrame.add(op.label);
5✔
617

618
                if (!operationStats.has(op.label)) {
5✔
619
                    operationStats.set(op.label, {
5✔
620
                        totalCpuTime: 0,
5✔
621
                        appearanceCount: 0,
5✔
622
                        highBlockingFrames: 0,
5✔
623
                        blockingRatiosWhenPresent: [],
5✔
624
                    });
5✔
625
                }
5✔
626

627
                const stats = operationStats.get(op.label);
5✔
628
                if (stats) {
5✔
629
                    stats.totalCpuTime += op.duration;
5✔
630
                }
5✔
631
            }
5✔
632

633
            // Track appearances and blocking for each unique operation in frame
634
            for (const opLabel of opsInFrame) {
4✔
635
                const stats = operationStats.get(opLabel);
5✔
636
                if (stats) {
5✔
637
                    stats.appearanceCount++;
5✔
638
                    if (isHighBlocking) {
5✔
639
                        stats.highBlockingFrames++;
4✔
640
                    }
4✔
641

642
                    stats.blockingRatiosWhenPresent.push(frame.blockingRatio);
5✔
643
                }
5✔
644
            }
5✔
645
        }
4✔
646

647
        return Array.from(operationStats.entries())
4✔
648
            .map(([label, stats]) => ({
4✔
649
                label,
5✔
650
                totalCpuTime: stats.totalCpuTime,
5✔
651
                appearanceCount: stats.appearanceCount,
5✔
652
                highBlockingFrames: stats.highBlockingFrames,
5✔
653
                highBlockingPercentage: (stats.highBlockingFrames / stats.appearanceCount) * 100,
5✔
654
                avgBlockingRatioWhenPresent:
5✔
655
                    stats.blockingRatiosWhenPresent.reduce((a, b) => a + b, 0) / stats.blockingRatiosWhenPresent.length,
5✔
656
            }))
4✔
657
            .sort((a, b) => b.highBlockingPercentage - a.highBlockingPercentage);
4✔
658
    }
31✔
659

660
    /**
661
     * Measure synchronous code execution
662
     * @param label - Label for this measurement
663
     * @param fn - Function to measure
664
     * @returns The return value of fn
665
     */
666
    measure<T>(label: string, fn: () => T): T {
16✔
667
        if (!this.enabled) {
85,948✔
668
            return fn();
84,216✔
669
        }
84,216!
670

671
        const start = performance.now();
1,732✔
672
        try {
1,732✔
673
            return fn();
1,732✔
674
        } finally {
1,732✔
675
            const duration = performance.now() - start;
1,732✔
676
            this.recordMeasurement(label, duration);
1,732✔
677

678
            // Also track for frame-level blocking detection
679
            if (this.frameProfilingEnabled) {
1,732!
680
                this.currentFrameOperations.push({ label, duration });
6✔
681
            }
6✔
682
        }
1,732✔
683
    }
85,948✔
684

685
    /**
686
     * Measure async code execution
687
     * @param label - Label for this measurement
688
     * @param fn - Async function to measure
689
     * @returns Promise resolving to the return value of fn
690
     */
691
    async measureAsync<T>(label: string, fn: () => Promise<T>): Promise<T> {
16✔
692
        if (!this.enabled) {
2!
693
            return await fn();
×
694
        }
×
695

696
        const start = performance.now();
2✔
697
        try {
2✔
698
            return await fn();
2✔
699
        } finally {
2✔
700
            const duration = performance.now() - start;
2✔
701
            this.recordMeasurement(label, duration);
2✔
702

703
            // Also track for frame-level blocking detection
704
            if (this.frameProfilingEnabled) {
2!
705
                this.currentFrameOperations.push({ label, duration });
×
706
            }
×
707
        }
2✔
708
    }
2✔
709

710
    /**
711
     * Start manual timing
712
     * @param label - Label for this measurement
713
     */
714
    startMeasurement(label: string): void {
16✔
715
        if (!this.enabled) {
821,943✔
716
            return;
821,936✔
717
        }
821,936!
718

719
        this.activeStack.push({ label, startTime: performance.now() });
7✔
720
    }
821,943✔
721

722
    /**
723
     * End manual timing
724
     * @param label - Label for this measurement (must match startMeasurement)
725
     */
726
    endMeasurement(label: string): void {
16✔
727
        if (!this.enabled) {
821,943✔
728
            return;
821,936✔
729
        }
821,936!
730

731
        const entry = this.activeStack.pop();
7✔
732
        if (entry?.label !== label) {
821,943!
733
            console.warn(`StatsManager: Mismatched measurement end for "${label}"`);
1✔
734
            return;
1✔
735
        }
1✔
736

737
        const duration = performance.now() - entry.startTime;
6✔
738
        this.recordMeasurement(label, duration);
6✔
739

740
        // Also track for frame-level blocking detection
741
        if (this.frameProfilingEnabled) {
8!
742
            this.currentFrameOperations.push({ label, duration });
×
743
        }
×
744
    }
821,943✔
745

746
    /**
747
     * Reset detailed measurements (keep BabylonJS instrumentation running)
748
     */
749
    resetMeasurements(): void {
16✔
750
        this.measurements.clear();
512✔
751
        this.activeStack = [];
512✔
752
        this.counters.clear();
512✔
753
    }
512✔
754

755
    /**
756
     * Record a measurement and update statistics
757
     * @param label - Label for the measurement
758
     * @param duration - Duration in milliseconds
759
     */
760
    private recordMeasurement(label: string, duration: number): void {
16✔
761
        if (!this.measurements.has(label)) {
1,740✔
762
            this.measurements.set(label, {
37✔
763
                label,
37✔
764
                count: 0,
37✔
765
                total: 0,
37✔
766
                min: Infinity,
37✔
767
                max: -Infinity,
37✔
768
                avg: 0,
37✔
769
                lastDuration: 0,
37✔
770
                durations: new Array(StatsManager.RING_BUFFER_SIZE),
37✔
771
                durationsIndex: 0,
37✔
772
                durationsFilled: false,
37✔
773
            });
37✔
774
        }
37✔
775

776
        const stats = this.measurements.get(label);
1,740✔
777
        if (!stats) {
1,740!
778
            return;
×
779
        }
×
780

781
        stats.count++;
1,740✔
782
        stats.total += duration;
1,740✔
783
        stats.min = Math.min(stats.min, duration);
1,740✔
784
        stats.max = Math.max(stats.max, duration);
1,740✔
785
        stats.avg = stats.total / stats.count;
1,740✔
786
        stats.lastDuration = duration;
1,740✔
787

788
        // Optimized ring buffer: use circular index instead of shift/push
789
        stats.durations[stats.durationsIndex] = duration;
1,740✔
790
        stats.durationsIndex = (stats.durationsIndex + 1) % StatsManager.RING_BUFFER_SIZE;
1,740✔
791
        if (stats.durationsIndex === 0) {
1,740!
792
            stats.durationsFilled = true;
1✔
793
        }
1✔
794
    }
1,740✔
795

796
    /**
797
     * Calculate percentile from stored durations
798
     * Uses simple sorting approach (accurate but not streaming)
799
     * @param durations - Array of duration measurements
800
     * @param percentile - Percentile to calculate (0-100)
801
     * @param filled - Whether the ring buffer is completely filled
802
     * @param currentIndex - Current index in the ring buffer
803
     * @returns The calculated percentile value
804
     */
805
    private getPercentile(durations: number[], percentile: number, filled: boolean, currentIndex: number): number {
16✔
806
        // Only use filled portion of ring buffer
807
        const validDurations = filled ? durations : durations.slice(0, currentIndex);
87!
808

809
        if (validDurations.length === 0) {
87!
810
            return 0;
×
811
        }
×
812

813
        // Copy and sort to avoid mutating original array
814
        const sorted = [...validDurations].sort((a, b) => a - b);
87✔
815

816
        // Calculate index (percentile as fraction * length)
817
        const index = Math.ceil((percentile / 100) * sorted.length) - 1;
87✔
818

819
        // Clamp to valid range
820
        return sorted[Math.max(0, Math.min(index, sorted.length - 1))];
87✔
821
    }
87✔
822

823
    /**
824
     * Get comprehensive performance snapshot
825
     * @returns Complete performance data including CPU, GPU, and scene metrics
826
     */
827
    getSnapshot(): PerformanceSnapshot {
16✔
828
        const cpuMeasurements = Array.from(this.measurements.values()).map((m) => ({
581✔
829
            label: m.label,
29✔
830
            count: m.count,
29✔
831
            total: m.total,
29✔
832
            min: m.min,
29✔
833
            max: m.max,
29✔
834
            avg: m.avg,
29✔
835
            p50: this.getPercentile(m.durations, 50, m.durationsFilled, m.durationsIndex),
29✔
836
            p95: this.getPercentile(m.durations, 95, m.durationsFilled, m.durationsIndex),
29✔
837
            p99: this.getPercentile(m.durations, 99, m.durationsFilled, m.durationsIndex),
29✔
838
            lastDuration: m.lastDuration,
29✔
839
        }));
581✔
840

841
        // Helper to create PerfCounterSnapshot from BabylonJS PerfCounter
842
        const toPerfCounterSnapshot = (counter: PerfCounter): PerfCounterSnapshot => ({
581✔
843
            current: counter.current,
4,361✔
844
            avg: counter.average,
4,361✔
845
            min: counter.min,
4,361✔
846
            max: counter.max,
4,361✔
847
            total: counter.total,
4,361✔
848
            lastSecAvg: counter.lastSecAverage,
4,361✔
849
        });
4,361✔
850

851
        return {
581✔
852
            cpu: cpuMeasurements,
581✔
853

854
            // GPU metrics (EngineInstrumentation)
855
            gpu: this.babylonInstrumentation
581✔
856
                ? {
546✔
857
                      // GPU frame time is in nanoseconds, convert to milliseconds
858
                      gpuFrameTime: {
546✔
859
                          current: this.babylonInstrumentation.gpuFrameTimeCounter.current * 0.000001,
546✔
860
                          avg: this.babylonInstrumentation.gpuFrameTimeCounter.average * 0.000001,
546✔
861
                          min: this.babylonInstrumentation.gpuFrameTimeCounter.min * 0.000001,
546✔
862
                          max: this.babylonInstrumentation.gpuFrameTimeCounter.max * 0.000001,
546✔
863
                          total: this.babylonInstrumentation.gpuFrameTimeCounter.total * 0.000001,
546✔
864
                          lastSecAvg: this.babylonInstrumentation.gpuFrameTimeCounter.lastSecAverage * 0.000001,
546✔
865
                      },
546✔
866
                      // Shader compilation is already in milliseconds
867
                      shaderCompilation: toPerfCounterSnapshot(
546✔
868
                          this.babylonInstrumentation.shaderCompilationTimeCounter,
546✔
869
                      ),
546✔
870
                  }
546!
871
                : undefined,
35✔
872

873
            // Scene metrics (SceneInstrumentation)
874
            scene: this.sceneInstrumentation
581✔
875
                ? {
545✔
876
                      frameTime: toPerfCounterSnapshot(this.sceneInstrumentation.frameTimeCounter),
545✔
877
                      renderTime: toPerfCounterSnapshot(this.sceneInstrumentation.renderTimeCounter),
545✔
878
                      interFrameTime: toPerfCounterSnapshot(this.sceneInstrumentation.interFrameTimeCounter),
545✔
879
                      cameraRenderTime: toPerfCounterSnapshot(this.sceneInstrumentation.cameraRenderTimeCounter),
545✔
880
                      activeMeshesEvaluation: toPerfCounterSnapshot(
545✔
881
                          this.sceneInstrumentation.activeMeshesEvaluationTimeCounter,
545✔
882
                      ),
545✔
883
                      renderTargetsRenderTime: toPerfCounterSnapshot(
545✔
884
                          this.sceneInstrumentation.renderTargetsRenderTimeCounter,
545✔
885
                      ),
545✔
886
                      // Draw calls includes count property in addition to timing
887
                      drawCalls: {
545✔
888
                          ...toPerfCounterSnapshot(this.sceneInstrumentation.drawCallsCounter),
545✔
889
                          count: this.sceneInstrumentation.drawCallsCounter.count,
545✔
890
                      },
545✔
891
                  }
545!
892
                : undefined,
36✔
893

894
            // Layout session metrics (if session has completed)
895
            layoutSession: this.getLayoutSessionMetrics(),
581✔
896

897
            timestamp: performance.now(),
581✔
898
        };
581✔
899
    }
581✔
900

901
    /**
902
     * Start tracking a layout session
903
     */
904
    startLayoutSession(): void {
16✔
905
        this.layoutSessionStartTime = performance.now();
13,518✔
906
        this.layoutSessionEndTime = null;
13,518✔
907
    }
13,518✔
908

909
    /**
910
     * End tracking a layout session
911
     */
912
    endLayoutSession(): void {
16✔
913
        this.layoutSessionEndTime = performance.now();
537✔
914
    }
537✔
915

916
    /**
917
     * Calculate layout session metrics
918
     * @returns Layout session metrics if available, undefined otherwise
919
     */
920
    private getLayoutSessionMetrics(): LayoutSessionMetrics | undefined {
16✔
921
        if (this.layoutSessionStartTime === null || this.layoutSessionEndTime === null) {
581!
922
            return undefined;
44✔
923
        }
44!
924

925
        // Get frame count from Graph.update measurement
926
        const graphUpdateMeasurement = this.measurements.get("Graph.update");
537✔
927
        const frameCount = graphUpdateMeasurement?.count ?? 0;
581!
928

929
        if (frameCount === 0) {
581!
930
            return undefined;
537✔
931
        }
537!
932

933
        // Calculate totals
934
        const totalElapsed = this.layoutSessionEndTime - this.layoutSessionStartTime;
×
935
        const totalCpuTime = graphUpdateMeasurement?.total ?? 0;
581!
936
        const totalGpuTime = this.sceneInstrumentation?.frameTimeCounter.total ?? 0;
581!
937
        const blockingOverhead = totalElapsed - totalCpuTime - totalGpuTime;
581✔
938

939
        // Calculate percentages
940
        const cpuPercentage = (totalCpuTime / totalElapsed) * 100;
581✔
941
        const gpuPercentage = (totalGpuTime / totalElapsed) * 100;
581✔
942
        const blockingPercentage = (blockingOverhead / totalElapsed) * 100;
581✔
943

944
        // Calculate per-frame averages
945
        const totalPerFrame = totalElapsed / frameCount;
581✔
946
        const cpuPerFrame = totalCpuTime / frameCount;
581✔
947
        const gpuPerFrame = totalGpuTime / frameCount;
581✔
948
        const blockingPerFrame = blockingOverhead / frameCount;
581✔
949

950
        return {
581✔
951
            startTime: this.layoutSessionStartTime,
581✔
952
            endTime: this.layoutSessionEndTime,
581✔
953
            totalElapsed,
581✔
954
            frameCount,
581✔
955
            totalCpuTime,
581✔
956
            totalGpuTime,
581✔
957
            blockingOverhead,
581✔
958
            percentages: {
581✔
959
                cpu: cpuPercentage,
581✔
960
                gpu: gpuPercentage,
581✔
961
                blocking: blockingPercentage,
581✔
962
            },
581✔
963
            perFrame: {
581✔
964
                total: totalPerFrame,
581✔
965
                cpu: cpuPerFrame,
581✔
966
                gpu: gpuPerFrame,
581✔
967
                blocking: blockingPerFrame,
581✔
968
            },
581✔
969
        };
581✔
970
    }
581✔
971

972
    /**
973
     * Report detailed performance data to console
974
     */
975
    reportDetailed(): void {
16✔
976
        // Don't print anything if profiling is disabled
977
        if (!this.enabled) {
524✔
978
            return;
513✔
979
        }
513!
980

981
        const snapshot = this.getSnapshot();
11✔
982

983
        // eslint-disable-next-line no-console
984
        console.group("📊 Performance Report");
11✔
985

986
        // CPU metrics
987
        if (snapshot.cpu.length > 0) {
13✔
988
            // eslint-disable-next-line no-console
989
            console.group("CPU Metrics");
5✔
990
            // eslint-disable-next-line no-console
991
            console.table(
5✔
992
                snapshot.cpu.map((m) => ({
5✔
993
                    Label: m.label,
6✔
994
                    Calls: m.count,
6✔
995
                    "Total (ms)": m.total.toFixed(2),
6✔
996
                    "Avg (ms)": m.avg.toFixed(2),
6✔
997
                    "Min (ms)": m.min === Infinity ? 0 : m.min.toFixed(2),
6!
998
                    "Max (ms)": m.max === -Infinity ? 0 : m.max.toFixed(2),
6!
999
                    "P95 (ms)": m.p95.toFixed(2),
6✔
1000
                    "P99 (ms)": m.p99.toFixed(2),
6✔
1001
                })),
5✔
1002
            );
5✔
1003

1004
            // Also output as simple logs for console capture utilities (e.g., Storybook)
1005
            // eslint-disable-next-line no-console
1006
            console.log("CPU Metrics:");
5✔
1007
            snapshot.cpu.forEach((m) => {
5✔
1008
                // eslint-disable-next-line no-console
1009
                console.log(`  ${m.label}: ${m.count} calls, ${m.total.toFixed(2)}ms total, ${m.avg.toFixed(2)}ms avg`);
6✔
1010
            });
5✔
1011

1012
            // eslint-disable-next-line no-console
1013
            console.groupEnd();
5✔
1014
        }
5✔
1015

1016
        // Event Counters
1017
        const countersSnapshot = this.getCountersSnapshot();
11✔
1018
        if (countersSnapshot.length > 0) {
51!
1019
            // eslint-disable-next-line no-console
1020
            console.group("Event Counters");
2✔
1021
            // eslint-disable-next-line no-console
1022
            console.table(
2✔
1023
                countersSnapshot.map((c) => ({
2✔
1024
                    Label: c.label,
4✔
1025
                    Value: c.value,
4✔
1026
                    Operations: c.operations,
4✔
1027
                })),
2✔
1028
            );
2✔
1029
            // eslint-disable-next-line no-console
1030
            console.groupEnd();
2✔
1031
        }
2✔
1032

1033
        // GPU metrics (VERBOSE - all properties)
1034
        if (snapshot.gpu) {
13✔
1035
            // eslint-disable-next-line no-console
1036
            console.log("GPU Metrics (BabylonJS EngineInstrumentation):");
3✔
1037
            // eslint-disable-next-line no-console
1038
            console.group("GPU Metrics (BabylonJS EngineInstrumentation)");
3✔
1039

1040
            // eslint-disable-next-line no-console
1041
            console.log("  GPU Frame Time (ms):");
3✔
1042
            // eslint-disable-next-line no-console
1043
            console.group("GPU Frame Time (ms)");
3✔
1044
            // eslint-disable-next-line no-console
1045
            console.log("  Current:", snapshot.gpu.gpuFrameTime.current.toFixed(3));
3✔
1046
            // eslint-disable-next-line no-console
1047
            console.log("  Average:", snapshot.gpu.gpuFrameTime.avg.toFixed(3));
3✔
1048
            // eslint-disable-next-line no-console
1049
            console.log("  Last Sec Avg:", snapshot.gpu.gpuFrameTime.lastSecAvg.toFixed(3));
3✔
1050
            // eslint-disable-next-line no-console
1051
            console.log("  Min:", snapshot.gpu.gpuFrameTime.min.toFixed(3));
3✔
1052
            // eslint-disable-next-line no-console
1053
            console.log("  Max:", snapshot.gpu.gpuFrameTime.max.toFixed(3));
3✔
1054
            // eslint-disable-next-line no-console
1055
            console.log("  Total:", snapshot.gpu.gpuFrameTime.total.toFixed(3));
3✔
1056
            // eslint-disable-next-line no-console
1057
            console.groupEnd();
3✔
1058

1059
            // eslint-disable-next-line no-console
1060
            console.log("  Shader Compilation (ms):");
3✔
1061
            // eslint-disable-next-line no-console
1062
            console.group("Shader Compilation (ms)");
3✔
1063
            // eslint-disable-next-line no-console
1064
            console.log("  Current:", snapshot.gpu.shaderCompilation.current.toFixed(2));
3✔
1065
            // eslint-disable-next-line no-console
1066
            console.log("  Average:", snapshot.gpu.shaderCompilation.avg.toFixed(2));
3✔
1067
            // eslint-disable-next-line no-console
1068
            console.log("  Last Sec Avg:", snapshot.gpu.shaderCompilation.lastSecAvg.toFixed(2));
3✔
1069
            // eslint-disable-next-line no-console
1070
            console.log("  Min:", snapshot.gpu.shaderCompilation.min.toFixed(2));
3✔
1071
            // eslint-disable-next-line no-console
1072
            console.log("  Max:", snapshot.gpu.shaderCompilation.max.toFixed(2));
3✔
1073
            // eslint-disable-next-line no-console
1074
            console.log("  Total:", snapshot.gpu.shaderCompilation.total.toFixed(2));
3✔
1075
            // eslint-disable-next-line no-console
1076
            console.groupEnd();
3✔
1077

1078
            // eslint-disable-next-line no-console
1079
            console.groupEnd();
3✔
1080
        }
3✔
1081

1082
        // Scene metrics (VERBOSE - all properties for all 7 counters)
1083
        if (snapshot.scene) {
13✔
1084
            // eslint-disable-next-line no-console
1085
            console.log("Scene Metrics (BabylonJS SceneInstrumentation):");
3✔
1086
            // eslint-disable-next-line no-console
1087
            console.group("Scene Metrics (BabylonJS SceneInstrumentation)");
3✔
1088

1089
            // Helper to print counter stats
1090
            const printCounterStats = (name: string, counter: PerfCounterSnapshot, unit = "ms"): void => {
3✔
1091
                // eslint-disable-next-line no-console
1092
                console.log(`  ${name}:`);
18✔
1093
                // eslint-disable-next-line no-console
1094
                console.group(name);
18✔
1095
                // eslint-disable-next-line no-console
1096
                console.log(`  Current: ${counter.current.toFixed(2)} ${unit}`);
18✔
1097
                // eslint-disable-next-line no-console
1098
                console.log(`  Average: ${counter.avg.toFixed(2)} ${unit}`);
18✔
1099
                // eslint-disable-next-line no-console
1100
                console.log(`  Last Sec Avg: ${counter.lastSecAvg.toFixed(2)} ${unit}`);
18✔
1101
                // eslint-disable-next-line no-console
1102
                console.log(`  Min: ${counter.min.toFixed(2)} ${unit}`);
18✔
1103
                // eslint-disable-next-line no-console
1104
                console.log(`  Max: ${counter.max.toFixed(2)} ${unit}`);
18✔
1105
                // eslint-disable-next-line no-console
1106
                console.log(`  Total: ${counter.total.toFixed(2)} ${unit}`);
18✔
1107
                // eslint-disable-next-line no-console
1108
                console.groupEnd();
18✔
1109
            };
18✔
1110

1111
            printCounterStats("Frame Time", snapshot.scene.frameTime);
3✔
1112
            printCounterStats("Render Time", snapshot.scene.renderTime);
3✔
1113
            printCounterStats("Inter-Frame Time", snapshot.scene.interFrameTime);
3✔
1114
            printCounterStats("Camera Render Time", snapshot.scene.cameraRenderTime);
3✔
1115
            printCounterStats("Active Meshes Evaluation", snapshot.scene.activeMeshesEvaluation);
3✔
1116
            printCounterStats("Render Targets Render Time", snapshot.scene.renderTargetsRenderTime);
3✔
1117

1118
            // Draw Calls is special - count metric + timing
1119
            // eslint-disable-next-line no-console
1120
            console.log("  Draw Calls:");
3✔
1121
            // eslint-disable-next-line no-console
1122
            console.group("Draw Calls");
3✔
1123
            // eslint-disable-next-line no-console
1124
            console.log(`  Count: ${snapshot.scene.drawCalls.count}`);
3✔
1125
            // eslint-disable-next-line no-console
1126
            console.log(`  Current: ${snapshot.scene.drawCalls.current.toFixed(0)}`);
3✔
1127
            // eslint-disable-next-line no-console
1128
            console.log(`  Average: ${snapshot.scene.drawCalls.avg.toFixed(2)}`);
3✔
1129
            // eslint-disable-next-line no-console
1130
            console.log(`  Last Sec Avg: ${snapshot.scene.drawCalls.lastSecAvg.toFixed(2)}`);
3✔
1131
            // eslint-disable-next-line no-console
1132
            console.log(`  Min: ${snapshot.scene.drawCalls.min.toFixed(0)}`);
3✔
1133
            // eslint-disable-next-line no-console
1134
            console.log(`  Max: ${snapshot.scene.drawCalls.max.toFixed(0)}`);
3✔
1135
            // eslint-disable-next-line no-console
1136
            console.log(`  Total: ${snapshot.scene.drawCalls.total.toFixed(0)}`);
3✔
1137
            // eslint-disable-next-line no-console
1138
            console.groupEnd();
3✔
1139

1140
            // eslint-disable-next-line no-console
1141
            console.groupEnd();
3✔
1142
        }
3✔
1143

1144
        // Layout session summary (if available)
1145
        if (snapshot.layoutSession) {
14!
1146
            const ls = snapshot.layoutSession;
×
1147
            // eslint-disable-next-line no-console
1148
            console.log("Layout Session Performance:");
×
1149
            // eslint-disable-next-line no-console
1150
            console.group("Layout Session Performance");
×
1151

1152
            // eslint-disable-next-line no-console
1153
            console.log(`Total Time: ${ls.totalElapsed.toFixed(2)}ms (${ls.frameCount} frames)`);
×
1154
            // eslint-disable-next-line no-console
1155
            console.log(`├─ CPU Work: ${ls.totalCpuTime.toFixed(2)}ms (${ls.percentages.cpu.toFixed(1)}%)`);
×
1156
            // eslint-disable-next-line no-console
1157
            console.log(`├─ GPU Rendering: ${ls.totalGpuTime.toFixed(2)}ms (${ls.percentages.gpu.toFixed(1)}%)`);
×
1158
            // eslint-disable-next-line no-console
1159
            console.log(
×
1160
                `└─ Blocking/Overhead: ${ls.blockingOverhead.toFixed(2)}ms (${ls.percentages.blocking.toFixed(1)}%)`,
×
1161
            );
×
1162
            // eslint-disable-next-line no-console
1163
            console.log("");
×
1164
            // eslint-disable-next-line no-console
1165
            console.log("Per-Frame Averages:");
×
1166
            // eslint-disable-next-line no-console
1167
            console.log(`├─ Total: ${ls.perFrame.total.toFixed(2)}ms/frame`);
×
1168
            // eslint-disable-next-line no-console
1169
            console.log(`├─ CPU: ${ls.perFrame.cpu.toFixed(2)}ms/frame`);
×
1170
            // eslint-disable-next-line no-console
1171
            console.log(`├─ GPU: ${ls.perFrame.gpu.toFixed(2)}ms/frame`);
×
1172
            // eslint-disable-next-line no-console
1173
            console.log(`└─ Blocking: ${ls.perFrame.blocking.toFixed(2)}ms/frame`);
×
1174

1175
            // eslint-disable-next-line no-console
1176
            console.groupEnd();
×
1177
        }
✔
1178

1179
        // Blocking correlation report (if frame profiling is enabled)
1180
        if (this.frameProfilingEnabled && this.frameProfiles.length > 0) {
524!
1181
            const blockingReport = this.getBlockingReport();
1✔
1182

1183
            if (blockingReport.length > 0) {
1✔
1184
                // eslint-disable-next-line no-console
1185
                console.log("");
1✔
1186
                // eslint-disable-next-line no-console
1187
                console.log("🔍 Blocking Correlation Analysis:");
1✔
1188
                // eslint-disable-next-line no-console
1189
                console.group("Blocking Correlation Analysis");
1✔
1190

1191
                // eslint-disable-next-line no-console
1192
                console.log(`Analyzed ${this.frameProfiles.length} frames`);
1✔
1193
                // eslint-disable-next-line no-console
1194
                console.log("High-blocking threshold: blocking > 1.0x CPU time");
1✔
1195
                // eslint-disable-next-line no-console
1196
                console.log("");
1✔
1197

1198
                // Show top 10 operations by high-blocking percentage
1199
                const topBlockingOps = blockingReport.slice(0, 10);
1✔
1200

1201
                // eslint-disable-next-line no-console
1202
                console.log("Top operations correlated with blocking:");
1✔
1203
                // eslint-disable-next-line no-console
1204
                console.table(
1✔
1205
                    topBlockingOps.map((op) => ({
1✔
1206
                        Operation: op.label,
1✔
1207
                        "Total CPU (ms)": op.totalCpuTime.toFixed(2),
1✔
1208
                        Appearances: op.appearanceCount,
1✔
1209
                        "High-Blocking Frames": op.highBlockingFrames,
1✔
1210
                        "High-Blocking %": `${op.highBlockingPercentage.toFixed(1)}%`,
1✔
1211
                        "Avg Blocking Ratio": Number.isNaN(op.avgBlockingRatioWhenPresent)
1!
1212
                            ? "N/A"
×
1213
                            : `${op.avgBlockingRatioWhenPresent.toFixed(2)}x`,
1✔
1214
                    })),
1✔
1215
                );
1✔
1216

1217
                // Also output as simple logs
1218
                // eslint-disable-next-line no-console
1219
                console.log("Top operations correlated with blocking:");
1✔
1220
                topBlockingOps.forEach((op, i) => {
1✔
1221
                    // eslint-disable-next-line no-console
1222
                    console.log(
1✔
1223
                        `  ${i + 1}. ${op.label}: ${op.highBlockingPercentage.toFixed(1)}% high-blocking frames (${op.highBlockingFrames}/${op.appearanceCount})`,
1✔
1224
                    );
1✔
1225
                    const ratioStr = Number.isNaN(op.avgBlockingRatioWhenPresent)
1!
1226
                        ? "N/A"
×
1227
                        : `${op.avgBlockingRatioWhenPresent.toFixed(2)}x`;
1✔
1228
                    // eslint-disable-next-line no-console
1229
                    console.log(`     Avg blocking ratio: ${ratioStr}`);
1✔
1230
                });
1✔
1231

1232
                // eslint-disable-next-line no-console
1233
                console.groupEnd();
1✔
1234
            }
1✔
1235
        }
1✔
1236

1237
        // eslint-disable-next-line no-console
1238
        console.groupEnd();
11✔
1239
    }
524✔
1240

1241
    /**
1242
     * Increment a counter by a specified amount
1243
     * @param label Counter identifier
1244
     * @param amount Amount to increment (default: 1)
1245
     */
1246
    incrementCounter(label: string, amount = 1): void {
16✔
1247
        if (!this.enabled) {
201✔
1248
            return;
2✔
1249
        }
2✔
1250

1251
        let counter = this.counters.get(label);
199✔
1252
        if (!counter) {
201✔
1253
            counter = {
25✔
1254
                label,
25✔
1255
                value: 0,
25✔
1256
                operations: 0,
25✔
1257
            };
25✔
1258
            this.counters.set(label, counter);
25✔
1259
        }
25✔
1260

1261
        counter.value += amount;
199✔
1262
        counter.operations++;
199✔
1263
    }
201✔
1264

1265
    /**
1266
     * Decrement a counter by a specified amount
1267
     * @param label Counter identifier
1268
     * @param amount Amount to decrement (default: 1)
1269
     */
1270
    decrementCounter(label: string, amount = 1): void {
16✔
1271
        if (!this.enabled) {
7!
1272
            return;
×
1273
        }
×
1274

1275
        let counter = this.counters.get(label);
7✔
1276
        if (!counter) {
7!
1277
            counter = {
×
1278
                label,
×
1279
                value: 0,
×
1280
                operations: 0,
×
1281
            };
×
1282
            this.counters.set(label, counter);
×
1283
        }
×
1284

1285
        counter.value -= amount;
7✔
1286
        counter.operations++;
7✔
1287
    }
7✔
1288

1289
    /**
1290
     * Set a counter to a specific value
1291
     * @param label - Counter identifier
1292
     * @param value - Value to set
1293
     */
1294
    setCounter(label: string, value: number): void {
16✔
1295
        if (!this.enabled) {
5✔
1296
            return;
1✔
1297
        }
1✔
1298

1299
        let counter = this.counters.get(label);
4✔
1300
        if (!counter) {
5✔
1301
            counter = {
3✔
1302
                label,
3✔
1303
                value: 0,
3✔
1304
                operations: 0,
3✔
1305
            };
3✔
1306
            this.counters.set(label, counter);
3✔
1307
        }
3✔
1308

1309
        counter.value = value;
4✔
1310
        counter.operations++;
4✔
1311
    }
5✔
1312

1313
    /**
1314
     * Get current value of a counter
1315
     * @param label - Counter identifier
1316
     * @returns Current counter value (0 if not found)
1317
     */
1318
    getCounter(label: string): number {
16✔
1319
        if (!this.enabled) {
23✔
1320
            return 0;
2✔
1321
        }
2✔
1322

1323
        const counter = this.counters.get(label);
21✔
1324
        return counter?.value ?? 0;
23✔
1325
    }
23✔
1326

1327
    /**
1328
     * Reset a specific counter to 0
1329
     * @param label - Counter identifier
1330
     */
1331
    resetCounter(label: string): void {
16✔
1332
        if (!this.enabled) {
1!
1333
            return;
×
1334
        }
×
1335

1336
        const counter = this.counters.get(label);
1✔
1337
        if (counter) {
1✔
1338
            counter.value = 0;
1✔
1339
            // Don't reset operations count - this is still an operation
1340
            counter.operations++;
1✔
1341
        }
1✔
1342
    }
1✔
1343

1344
    /**
1345
     * Reset all counters to 0
1346
     */
1347
    resetAllCounters(): void {
16✔
1348
        if (!this.enabled) {
1!
1349
            return;
×
1350
        }
×
1351

1352
        for (const counter of this.counters.values()) {
1✔
1353
            counter.value = 0;
2✔
1354
            counter.operations++;
2✔
1355
        }
2✔
1356
    }
1✔
1357

1358
    /**
1359
     * Get snapshot of all counters
1360
     * @returns Array of counter snapshots
1361
     */
1362
    getCountersSnapshot(): CounterSnapshot[] {
16✔
1363
        return Array.from(this.counters.values()).map((c) => ({
14✔
1364
            label: c.label,
8✔
1365
            value: c.value,
8✔
1366
            operations: c.operations,
8✔
1367
        }));
14✔
1368
    }
14✔
1369
}
16✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc