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

graphty-org / graphty-monorepo / 20673181586

03 Jan 2026 05:40AM UTC coverage: 77.93% (+0.01%) from 77.92%
20673181586

push

github

apowers313
chore: merge logs fix into master

13442 of 17824 branches covered (75.42%)

Branch coverage included in aggregate %.

73 of 133 new or added lines in 8 files covered. (54.89%)

6 existing lines in 3 files now uncovered.

41276 of 52390 relevant lines covered (78.79%)

145404.96 hits per line

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

85.38
/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 { GraphtyLogger, type Logger } from "../logging";
15✔
11
import type { EventManager } from "./EventManager";
12
import type { Manager } from "./interfaces";
13

14
const logger: Logger = GraphtyLogger.getLogger(["graphty", "stats"]);
15✔
15

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

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

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

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

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

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

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

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

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

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

160
    // Scene and engine instrumentation
161
    private sceneInstrumentation: SceneInstrumentation | null = null;
16✔
162
    private babylonInstrumentation: EngineInstrumentation | null = null;
16✔
163

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

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

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

185
    // Layout session tracking
186
    private layoutSessionStartTime: number | null = null;
16✔
187
    private layoutSessionEndTime: number | null = null;
16✔
188

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

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

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

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

221
        if (this.babylonInstrumentation) {
779!
222
            this.babylonInstrumentation.dispose();
778✔
223
            this.babylonInstrumentation = null;
778✔
224
        }
778✔
225

226
        // Reset counters
227
        this.resetCounters();
779✔
228

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

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

252
        // Engine instrumentation
253
        this.babylonInstrumentation = new EngineInstrumentation(engine);
991✔
254
        this.babylonInstrumentation.captureGPUFrameTime = true;
991✔
255
        this.babylonInstrumentation.captureShaderCompilationTime = true;
991✔
256
    }
991✔
257

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

272
        if (sceneInstrumentation !== undefined) {
8✔
273
            this.sceneInstrumentation = sceneInstrumentation;
6✔
274
        }
6✔
275
    }
8✔
276

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

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

297
    /**
298
     * Increment update counter
299
     */
300
    step(): void {
16✔
301
        this.totalUpdates++;
10,040✔
302

303
        // Emit stats update event periodically (every 60 updates)
304
        if (this.totalUpdates % 60 === 0) {
10,040✔
305
            this.eventManager.emitGraphEvent("stats-update", {
77✔
306
                totalUpdates: this.totalUpdates,
77✔
307
                stats: this.getStats(),
77✔
308
            });
77✔
309
        }
77✔
310
    }
10,040✔
311

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

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

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

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

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

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

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

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

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

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

423
        return statsStr;
1✔
424
    }
1✔
425

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

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

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

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

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

486
        // Setup Long Task observer to detect >50ms blocking
487
        if (typeof PerformanceObserver !== "undefined") {
32✔
488
            try {
32✔
489
                this.longTaskObserver = new PerformanceObserver((list) => {
32✔
490
                    for (const entry of list.getEntries()) {
×
NEW
491
                        logger.warn("Long task detected (>50ms blocking)", {
×
NEW
492
                            duration: entry.duration,
×
NEW
493
                            startTime: entry.startTime,
×
NEW
494
                        });
×
UNCOV
495
                    }
×
496
                });
32✔
497
                this.longTaskObserver.observe({ type: "longtask", buffered: true });
32✔
498
            } catch {
32!
499
                // Long Task API not supported in this browser
500
            }
×
501
        }
32✔
502
    }
32✔
503

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

512
        if (this.longTaskObserver) {
28✔
513
            this.longTaskObserver.disconnect();
28✔
514
            this.longTaskObserver = null;
28✔
515
        }
28✔
516
    }
28✔
517

518
    /**
519
     * Start profiling a new frame
520
     * Should be called at the beginning of each frame
521
     */
522
    startFrameProfiling(): void {
16✔
523
        if (!this.frameProfilingEnabled) {
28,206!
524
            return;
28,201✔
525
        }
28,201!
526

527
        this.currentFrameNumber++;
5✔
528
        this.currentFrameOperations = [];
5✔
529
    }
28,206✔
530

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

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

545
        const profile: FrameProfile = {
28,206✔
546
            frameNumber: this.currentFrameNumber,
28,206✔
547
            operations: [...this.currentFrameOperations],
28,206✔
548
            totalCpuTime,
28,206✔
549
            interFrameTime,
28,206✔
550
            blockingTime,
28,206✔
551
            blockingRatio,
28,206✔
552
        };
28,206✔
553

554
        this.frameProfiles.push(profile);
28,206✔
555

556
        // Keep only last 100 frames to avoid memory issues
557
        if (this.frameProfiles.length > 100) {
28,206!
558
            this.frameProfiles.shift();
×
559
        }
✔
560

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

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

574
        logger.warn("High blocking frame detected", {
2✔
575
            frameNumber: profile.frameNumber,
2✔
576
            totalFrameTime: profile.interFrameTime,
2✔
577
            cpuTime: profile.totalCpuTime,
2✔
578
            blockingTime: profile.blockingTime,
2✔
579
            blockingRatio: profile.blockingRatio,
2✔
580
            topOperations: topOps.map((op) => ({ label: op.label, duration: op.duration })),
2✔
581
        });
2✔
582
    }
2✔
583

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

594
        const operationStats = new Map<
4✔
595
            string,
596
            {
597
                totalCpuTime: number;
598
                appearanceCount: number;
599
                highBlockingFrames: number;
600
                blockingRatiosWhenPresent: number[];
601
            }
602
        >();
4✔
603

604
        const highBlockingThreshold = 1.0; // Blocking > 1x CPU time
4✔
605

606
        for (const frame of this.frameProfiles) {
4✔
607
            const isHighBlocking = frame.blockingRatio > highBlockingThreshold;
4✔
608

609
            const opsInFrame = new Set<string>();
4✔
610
            for (const op of frame.operations) {
4✔
611
                opsInFrame.add(op.label);
5✔
612

613
                if (!operationStats.has(op.label)) {
5✔
614
                    operationStats.set(op.label, {
5✔
615
                        totalCpuTime: 0,
5✔
616
                        appearanceCount: 0,
5✔
617
                        highBlockingFrames: 0,
5✔
618
                        blockingRatiosWhenPresent: [],
5✔
619
                    });
5✔
620
                }
5✔
621

622
                const stats = operationStats.get(op.label);
5✔
623
                if (stats) {
5✔
624
                    stats.totalCpuTime += op.duration;
5✔
625
                }
5✔
626
            }
5✔
627

628
            // Track appearances and blocking for each unique operation in frame
629
            for (const opLabel of opsInFrame) {
4✔
630
                const stats = operationStats.get(opLabel);
5✔
631
                if (stats) {
5✔
632
                    stats.appearanceCount++;
5✔
633
                    if (isHighBlocking) {
5✔
634
                        stats.highBlockingFrames++;
4✔
635
                    }
4✔
636

637
                    stats.blockingRatiosWhenPresent.push(frame.blockingRatio);
5✔
638
                }
5✔
639
            }
5✔
640
        }
4✔
641

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

655
    /**
656
     * Measure synchronous code execution
657
     * @param label - Label for this measurement
658
     * @param fn - Function to measure
659
     * @returns The return value of fn
660
     */
661
    measure<T>(label: string, fn: () => T): T {
16✔
662
        if (!this.enabled) {
86,338✔
663
            return fn();
84,606✔
664
        }
84,606!
665

666
        const start = performance.now();
1,732✔
667
        try {
1,732✔
668
            return fn();
1,732✔
669
        } finally {
1,732✔
670
            const duration = performance.now() - start;
1,732✔
671
            this.recordMeasurement(label, duration);
1,732✔
672

673
            // Also track for frame-level blocking detection
674
            if (this.frameProfilingEnabled) {
1,732!
675
                this.currentFrameOperations.push({ label, duration });
6✔
676
            }
6✔
677
        }
1,732✔
678
    }
86,338✔
679

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

691
        const start = performance.now();
2✔
692
        try {
2✔
693
            return await fn();
2✔
694
        } finally {
2✔
695
            const duration = performance.now() - start;
2✔
696
            this.recordMeasurement(label, duration);
2✔
697

698
            // Also track for frame-level blocking detection
699
            if (this.frameProfilingEnabled) {
2!
700
                this.currentFrameOperations.push({ label, duration });
×
701
            }
×
702
        }
2✔
703
    }
2✔
704

705
    /**
706
     * Start manual timing
707
     * @param label - Label for this measurement
708
     */
709
    startMeasurement(label: string): void {
16✔
710
        if (!this.enabled) {
819,838✔
711
            return;
819,831✔
712
        }
819,831!
713

714
        this.activeStack.push({ label, startTime: performance.now() });
7✔
715
    }
819,838✔
716

717
    /**
718
     * End manual timing
719
     * @param label - Label for this measurement (must match startMeasurement)
720
     */
721
    endMeasurement(label: string): void {
16✔
722
        if (!this.enabled) {
819,838✔
723
            return;
819,831✔
724
        }
819,831!
725

726
        const entry = this.activeStack.pop();
7✔
727
        if (entry?.label !== label) {
819,838!
728
            console.warn(`StatsManager: Mismatched measurement end for "${label}"`);
1✔
729
            return;
1✔
730
        }
1✔
731

732
        const duration = performance.now() - entry.startTime;
6✔
733
        this.recordMeasurement(label, duration);
6✔
734

735
        // Also track for frame-level blocking detection
736
        if (this.frameProfilingEnabled) {
8!
737
            this.currentFrameOperations.push({ label, duration });
×
738
        }
×
739
    }
819,838✔
740

741
    /**
742
     * Reset detailed measurements (keep BabylonJS instrumentation running)
743
     */
744
    resetMeasurements(): void {
16✔
745
        this.measurements.clear();
512✔
746
        this.activeStack = [];
512✔
747
        this.counters.clear();
512✔
748
    }
512✔
749

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

771
        const stats = this.measurements.get(label);
1,740✔
772
        if (!stats) {
1,740!
773
            return;
×
774
        }
×
775

776
        stats.count++;
1,740✔
777
        stats.total += duration;
1,740✔
778
        stats.min = Math.min(stats.min, duration);
1,740✔
779
        stats.max = Math.max(stats.max, duration);
1,740✔
780
        stats.avg = stats.total / stats.count;
1,740✔
781
        stats.lastDuration = duration;
1,740✔
782

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

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

804
        if (validDurations.length === 0) {
87!
805
            return 0;
×
806
        }
×
807

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

811
        // Calculate index (percentile as fraction * length)
812
        const index = Math.ceil((percentile / 100) * sorted.length) - 1;
87✔
813

814
        // Clamp to valid range
815
        return sorted[Math.max(0, Math.min(index, sorted.length - 1))];
87✔
816
    }
87✔
817

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

836
        // Helper to create PerfCounterSnapshot from BabylonJS PerfCounter
837
        const toPerfCounterSnapshot = (counter: PerfCounter): PerfCounterSnapshot => ({
581✔
838
            current: counter.current,
4,361✔
839
            avg: counter.average,
4,361✔
840
            min: counter.min,
4,361✔
841
            max: counter.max,
4,361✔
842
            total: counter.total,
4,361✔
843
            lastSecAvg: counter.lastSecAverage,
4,361✔
844
        });
4,361✔
845

846
        return {
581✔
847
            cpu: cpuMeasurements,
581✔
848

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

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

889
            // Layout session metrics (if session has completed)
890
            layoutSession: this.getLayoutSessionMetrics(),
581✔
891

892
            timestamp: performance.now(),
581✔
893
        };
581✔
894
    }
581✔
895

896
    /**
897
     * Start tracking a layout session
898
     */
899
    startLayoutSession(): void {
16✔
900
        this.layoutSessionStartTime = performance.now();
13,518✔
901
        this.layoutSessionEndTime = null;
13,518✔
902
    }
13,518✔
903

904
    /**
905
     * End tracking a layout session
906
     */
907
    endLayoutSession(): void {
16✔
908
        this.layoutSessionEndTime = performance.now();
537✔
909
    }
537✔
910

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

920
        // Get frame count from Graph.update measurement
921
        const graphUpdateMeasurement = this.measurements.get("Graph.update");
537✔
922
        const frameCount = graphUpdateMeasurement?.count ?? 0;
581!
923

924
        if (frameCount === 0) {
581!
925
            return undefined;
537✔
926
        }
537!
927

928
        // Calculate totals
929
        const totalElapsed = this.layoutSessionEndTime - this.layoutSessionStartTime;
×
930
        const totalCpuTime = graphUpdateMeasurement?.total ?? 0;
581!
931
        const totalGpuTime = this.sceneInstrumentation?.frameTimeCounter.total ?? 0;
581!
932
        const blockingOverhead = totalElapsed - totalCpuTime - totalGpuTime;
581✔
933

934
        // Calculate percentages
935
        const cpuPercentage = (totalCpuTime / totalElapsed) * 100;
581✔
936
        const gpuPercentage = (totalGpuTime / totalElapsed) * 100;
581✔
937
        const blockingPercentage = (blockingOverhead / totalElapsed) * 100;
581✔
938

939
        // Calculate per-frame averages
940
        const totalPerFrame = totalElapsed / frameCount;
581✔
941
        const cpuPerFrame = totalCpuTime / frameCount;
581✔
942
        const gpuPerFrame = totalGpuTime / frameCount;
581✔
943
        const blockingPerFrame = blockingOverhead / frameCount;
581✔
944

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

967
    /* eslint-disable no-console -- reportDetailed intentionally uses browser console APIs for rich DevTools output */
968
    /**
969
     * Report detailed performance data to console.
970
     * This method intentionally uses browser console APIs (console.group, console.table)
971
     * for rich formatted output in browser DevTools.
972
     */
973
    reportDetailed(): void {
16✔
974
        // Don't print anything if profiling is disabled
975
        if (!this.enabled) {
524✔
976
            return;
513✔
977
        }
513!
978

979
        const snapshot = this.getSnapshot();
11✔
980

981
        console.group("📊 Performance Report");
11✔
982

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

999
            // Also output as simple logs for console capture utilities (e.g., Storybook)
1000

1001
            console.log("CPU Metrics:");
5✔
1002
            snapshot.cpu.forEach((m) => {
5✔
1003
                console.log(`  ${m.label}: ${m.count} calls, ${m.total.toFixed(2)}ms total, ${m.avg.toFixed(2)}ms avg`);
6✔
1004
            });
5✔
1005

1006

1007
            console.groupEnd();
5✔
1008
        }
5✔
1009

1010
        // Event Counters
1011
        const countersSnapshot = this.getCountersSnapshot();
11✔
1012
        if (countersSnapshot.length > 0) {
51!
1013

1014
            console.group("Event Counters");
2✔
1015

1016
            console.table(
2✔
1017
                countersSnapshot.map((c) => ({
2✔
1018
                    Label: c.label,
4✔
1019
                    Value: c.value,
4✔
1020
                    Operations: c.operations,
4✔
1021
                })),
2✔
1022
            );
2✔
1023

1024
            console.groupEnd();
2✔
1025
        }
2✔
1026

1027
        // GPU metrics (VERBOSE - all properties)
1028
        if (snapshot.gpu) {
13✔
1029

1030
            console.log("GPU Metrics (BabylonJS EngineInstrumentation):");
3✔
1031

1032
            console.group("GPU Metrics (BabylonJS EngineInstrumentation)");
3✔
1033

1034

1035
            console.log("  GPU Frame Time (ms):");
3✔
1036

1037
            console.group("GPU Frame Time (ms)");
3✔
1038

1039
            console.log("  Current:", snapshot.gpu.gpuFrameTime.current.toFixed(3));
3✔
1040

1041
            console.log("  Average:", snapshot.gpu.gpuFrameTime.avg.toFixed(3));
3✔
1042

1043
            console.log("  Last Sec Avg:", snapshot.gpu.gpuFrameTime.lastSecAvg.toFixed(3));
3✔
1044

1045
            console.log("  Min:", snapshot.gpu.gpuFrameTime.min.toFixed(3));
3✔
1046

1047
            console.log("  Max:", snapshot.gpu.gpuFrameTime.max.toFixed(3));
3✔
1048

1049
            console.log("  Total:", snapshot.gpu.gpuFrameTime.total.toFixed(3));
3✔
1050

1051
            console.groupEnd();
3✔
1052

1053

1054
            console.log("  Shader Compilation (ms):");
3✔
1055

1056
            console.group("Shader Compilation (ms)");
3✔
1057

1058
            console.log("  Current:", snapshot.gpu.shaderCompilation.current.toFixed(2));
3✔
1059

1060
            console.log("  Average:", snapshot.gpu.shaderCompilation.avg.toFixed(2));
3✔
1061

1062
            console.log("  Last Sec Avg:", snapshot.gpu.shaderCompilation.lastSecAvg.toFixed(2));
3✔
1063

1064
            console.log("  Min:", snapshot.gpu.shaderCompilation.min.toFixed(2));
3✔
1065

1066
            console.log("  Max:", snapshot.gpu.shaderCompilation.max.toFixed(2));
3✔
1067

1068
            console.log("  Total:", snapshot.gpu.shaderCompilation.total.toFixed(2));
3✔
1069

1070
            console.groupEnd();
3✔
1071

1072

1073
            console.groupEnd();
3✔
1074
        }
3✔
1075

1076
        // Scene metrics (VERBOSE - all properties for all 7 counters)
1077
        if (snapshot.scene) {
13✔
1078

1079
            console.log("Scene Metrics (BabylonJS SceneInstrumentation):");
3✔
1080

1081
            console.group("Scene Metrics (BabylonJS SceneInstrumentation)");
3✔
1082

1083
            // Helper to print counter stats
1084
            const printCounterStats = (name: string, counter: PerfCounterSnapshot, unit = "ms"): void => {
3✔
1085
                console.log(`  ${name}:`);
18✔
1086
                console.group(name);
18✔
1087
                console.log(`  Current: ${counter.current.toFixed(2)} ${unit}`);
18✔
1088
                console.log(`  Average: ${counter.avg.toFixed(2)} ${unit}`);
18✔
1089
                console.log(`  Last Sec Avg: ${counter.lastSecAvg.toFixed(2)} ${unit}`);
18✔
1090
                console.log(`  Min: ${counter.min.toFixed(2)} ${unit}`);
18✔
1091
                console.log(`  Max: ${counter.max.toFixed(2)} ${unit}`);
18✔
1092
                console.log(`  Total: ${counter.total.toFixed(2)} ${unit}`);
18✔
1093
                console.groupEnd();
18✔
1094
            };
18✔
1095

1096
            printCounterStats("Frame Time", snapshot.scene.frameTime);
3✔
1097
            printCounterStats("Render Time", snapshot.scene.renderTime);
3✔
1098
            printCounterStats("Inter-Frame Time", snapshot.scene.interFrameTime);
3✔
1099
            printCounterStats("Camera Render Time", snapshot.scene.cameraRenderTime);
3✔
1100
            printCounterStats("Active Meshes Evaluation", snapshot.scene.activeMeshesEvaluation);
3✔
1101
            printCounterStats("Render Targets Render Time", snapshot.scene.renderTargetsRenderTime);
3✔
1102

1103
            // Draw Calls is special - count metric + timing
1104

1105
            console.log("  Draw Calls:");
3✔
1106

1107
            console.group("Draw Calls");
3✔
1108

1109
            console.log(`  Count: ${snapshot.scene.drawCalls.count}`);
3✔
1110

1111
            console.log(`  Current: ${snapshot.scene.drawCalls.current.toFixed(0)}`);
3✔
1112

1113
            console.log(`  Average: ${snapshot.scene.drawCalls.avg.toFixed(2)}`);
3✔
1114

1115
            console.log(`  Last Sec Avg: ${snapshot.scene.drawCalls.lastSecAvg.toFixed(2)}`);
3✔
1116

1117
            console.log(`  Min: ${snapshot.scene.drawCalls.min.toFixed(0)}`);
3✔
1118

1119
            console.log(`  Max: ${snapshot.scene.drawCalls.max.toFixed(0)}`);
3✔
1120

1121
            console.log(`  Total: ${snapshot.scene.drawCalls.total.toFixed(0)}`);
3✔
1122

1123
            console.groupEnd();
3✔
1124

1125

1126
            console.groupEnd();
3✔
1127
        }
3✔
1128

1129
        // Layout session summary (if available)
1130
        if (snapshot.layoutSession) {
14!
1131
            const ls = snapshot.layoutSession;
×
1132

1133
            console.log("Layout Session Performance:");
×
1134

1135
            console.group("Layout Session Performance");
×
1136

1137

1138
            console.log(`Total Time: ${ls.totalElapsed.toFixed(2)}ms (${ls.frameCount} frames)`);
×
1139

1140
            console.log(`├─ CPU Work: ${ls.totalCpuTime.toFixed(2)}ms (${ls.percentages.cpu.toFixed(1)}%)`);
×
1141

1142
            console.log(`├─ GPU Rendering: ${ls.totalGpuTime.toFixed(2)}ms (${ls.percentages.gpu.toFixed(1)}%)`);
×
1143

1144
            console.log(
×
1145
                `└─ Blocking/Overhead: ${ls.blockingOverhead.toFixed(2)}ms (${ls.percentages.blocking.toFixed(1)}%)`,
×
1146
            );
×
1147

1148
            console.log("");
×
1149

1150
            console.log("Per-Frame Averages:");
×
1151

1152
            console.log(`├─ Total: ${ls.perFrame.total.toFixed(2)}ms/frame`);
×
1153

1154
            console.log(`├─ CPU: ${ls.perFrame.cpu.toFixed(2)}ms/frame`);
×
1155

1156
            console.log(`├─ GPU: ${ls.perFrame.gpu.toFixed(2)}ms/frame`);
×
1157

1158
            console.log(`└─ Blocking: ${ls.perFrame.blocking.toFixed(2)}ms/frame`);
×
1159

1160

1161
            console.groupEnd();
×
1162
        }
✔
1163

1164
        // Blocking correlation report (if frame profiling is enabled)
1165
        if (this.frameProfilingEnabled && this.frameProfiles.length > 0) {
524!
1166
            const blockingReport = this.getBlockingReport();
1✔
1167

1168
            if (blockingReport.length > 0) {
1✔
1169
                console.log("");
1✔
1170
                console.log("🔍 Blocking Correlation Analysis:");
1✔
1171
                console.group("Blocking Correlation Analysis");
1✔
1172

1173
                console.log(`Analyzed ${this.frameProfiles.length} frames`);
1✔
1174
                console.log("High-blocking threshold: blocking > 1.0x CPU time");
1✔
1175
                console.log("");
1✔
1176

1177
                // Show top 10 operations by high-blocking percentage
1178
                const topBlockingOps = blockingReport.slice(0, 10);
1✔
1179

1180
                console.log("Top operations correlated with blocking:");
1✔
1181
                console.table(
1✔
1182
                    topBlockingOps.map((op) => ({
1✔
1183
                        Operation: op.label,
1✔
1184
                        "Total CPU (ms)": op.totalCpuTime.toFixed(2),
1✔
1185
                        Appearances: op.appearanceCount,
1✔
1186
                        "High-Blocking Frames": op.highBlockingFrames,
1✔
1187
                        "High-Blocking %": `${op.highBlockingPercentage.toFixed(1)}%`,
1✔
1188
                        "Avg Blocking Ratio": Number.isNaN(op.avgBlockingRatioWhenPresent)
1!
1189
                            ? "N/A"
×
1190
                            : `${op.avgBlockingRatioWhenPresent.toFixed(2)}x`,
1✔
1191
                    })),
1✔
1192
                );
1✔
1193

1194
                // Also output as simple logs
1195
                console.log("Top operations correlated with blocking:");
1✔
1196
                topBlockingOps.forEach((op, i) => {
1✔
1197
                    console.log(
1✔
1198
                        `  ${i + 1}. ${op.label}: ${op.highBlockingPercentage.toFixed(1)}% high-blocking frames (${op.highBlockingFrames}/${op.appearanceCount})`,
1✔
1199
                    );
1✔
1200
                    const ratioStr = Number.isNaN(op.avgBlockingRatioWhenPresent)
1!
1201
                        ? "N/A"
×
1202
                        : `${op.avgBlockingRatioWhenPresent.toFixed(2)}x`;
1✔
1203
                    console.log(`     Avg blocking ratio: ${ratioStr}`);
1✔
1204
                });
1✔
1205

1206
                console.groupEnd();
1✔
1207
            }
1✔
1208
        }
1✔
1209

1210
        console.groupEnd();
11✔
1211
    }
524✔
1212
    /* eslint-enable no-console */
1213

1214
    /**
1215
     * Increment a counter by a specified amount
1216
     * @param label Counter identifier
1217
     * @param amount Amount to increment (default: 1)
1218
     */
1219
    incrementCounter(label: string, amount = 1): void {
16✔
1220
        if (!this.enabled) {
201✔
1221
            return;
2✔
1222
        }
2✔
1223

1224
        let counter = this.counters.get(label);
199✔
1225
        if (!counter) {
201✔
1226
            counter = {
25✔
1227
                label,
25✔
1228
                value: 0,
25✔
1229
                operations: 0,
25✔
1230
            };
25✔
1231
            this.counters.set(label, counter);
25✔
1232
        }
25✔
1233

1234
        counter.value += amount;
199✔
1235
        counter.operations++;
199✔
1236
    }
201✔
1237

1238
    /**
1239
     * Decrement a counter by a specified amount
1240
     * @param label Counter identifier
1241
     * @param amount Amount to decrement (default: 1)
1242
     */
1243
    decrementCounter(label: string, amount = 1): void {
16✔
1244
        if (!this.enabled) {
7!
1245
            return;
×
1246
        }
×
1247

1248
        let counter = this.counters.get(label);
7✔
1249
        if (!counter) {
7!
1250
            counter = {
×
1251
                label,
×
1252
                value: 0,
×
1253
                operations: 0,
×
1254
            };
×
1255
            this.counters.set(label, counter);
×
1256
        }
×
1257

1258
        counter.value -= amount;
7✔
1259
        counter.operations++;
7✔
1260
    }
7✔
1261

1262
    /**
1263
     * Set a counter to a specific value
1264
     * @param label - Counter identifier
1265
     * @param value - Value to set
1266
     */
1267
    setCounter(label: string, value: number): void {
16✔
1268
        if (!this.enabled) {
5✔
1269
            return;
1✔
1270
        }
1✔
1271

1272
        let counter = this.counters.get(label);
4✔
1273
        if (!counter) {
5✔
1274
            counter = {
3✔
1275
                label,
3✔
1276
                value: 0,
3✔
1277
                operations: 0,
3✔
1278
            };
3✔
1279
            this.counters.set(label, counter);
3✔
1280
        }
3✔
1281

1282
        counter.value = value;
4✔
1283
        counter.operations++;
4✔
1284
    }
5✔
1285

1286
    /**
1287
     * Get current value of a counter
1288
     * @param label - Counter identifier
1289
     * @returns Current counter value (0 if not found)
1290
     */
1291
    getCounter(label: string): number {
16✔
1292
        if (!this.enabled) {
23✔
1293
            return 0;
2✔
1294
        }
2✔
1295

1296
        const counter = this.counters.get(label);
21✔
1297
        return counter?.value ?? 0;
23✔
1298
    }
23✔
1299

1300
    /**
1301
     * Reset a specific counter to 0
1302
     * @param label - Counter identifier
1303
     */
1304
    resetCounter(label: string): void {
16✔
1305
        if (!this.enabled) {
1!
1306
            return;
×
1307
        }
×
1308

1309
        const counter = this.counters.get(label);
1✔
1310
        if (counter) {
1✔
1311
            counter.value = 0;
1✔
1312
            // Don't reset operations count - this is still an operation
1313
            counter.operations++;
1✔
1314
        }
1✔
1315
    }
1✔
1316

1317
    /**
1318
     * Reset all counters to 0
1319
     */
1320
    resetAllCounters(): void {
16✔
1321
        if (!this.enabled) {
1!
1322
            return;
×
1323
        }
×
1324

1325
        for (const counter of this.counters.values()) {
1✔
1326
            counter.value = 0;
2✔
1327
            counter.operations++;
2✔
1328
        }
2✔
1329
    }
1✔
1330

1331
    /**
1332
     * Get snapshot of all counters
1333
     * @returns Array of counter snapshots
1334
     */
1335
    getCountersSnapshot(): CounterSnapshot[] {
16✔
1336
        return Array.from(this.counters.values()).map((c) => ({
14✔
1337
            label: c.label,
8✔
1338
            value: c.value,
8✔
1339
            operations: c.operations,
8✔
1340
        }));
14✔
1341
    }
14✔
1342
}
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