stats-gl 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/main.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import type * as THREE from 'three';
2
2
  import { Panel } from './panel';
3
+ import { PanelVSync } from './panelVsync';
3
4
 
4
5
  interface StatsOptions {
5
6
  trackGPU?: boolean;
7
+ trackCPT?: boolean;
8
+ trackHz?: boolean;
6
9
  logsPerSecond?: number;
7
10
  graphsPerSecond?: number;
8
11
  samplesLog?: number;
@@ -22,6 +25,13 @@ interface AverageData {
22
25
  graph: number[];
23
26
  }
24
27
 
28
+
29
+ interface VSyncInfo {
30
+ refreshRate: number;
31
+ frameTime: number;
32
+ }
33
+
34
+
25
35
  interface InfoData {
26
36
  render: {
27
37
  timestamp: number;
@@ -37,6 +47,8 @@ class Stats {
37
47
  public horizontal: boolean;
38
48
  public minimal: boolean;
39
49
  public trackGPU: boolean;
50
+ public trackHz: boolean;
51
+ public trackCPT: boolean;
40
52
  public samplesLog: number;
41
53
  public samplesGraph: number;
42
54
  public precision: number;
@@ -55,16 +67,17 @@ class Stats {
55
67
  private frameTimes: number[] = []; // Store frame timestamps
56
68
 
57
69
  private renderCount = 0;
58
- private isRunningCPUProfiling = false;
59
70
 
60
71
  private totalCpuDuration = 0;
61
72
  private totalGpuDuration = 0;
62
73
  private totalGpuDurationCompute = 0;
63
74
 
75
+ private _panelId: number;
64
76
  private fpsPanel: Panel;
65
77
  private msPanel: Panel;
66
78
  private gpuPanel: Panel | null = null;
67
79
  private gpuPanelCompute: Panel | null = null;
80
+ private vsyncPanel: PanelVSync | null = null;
68
81
 
69
82
  public averageFps: AverageData = { logs: [], graph: [] };
70
83
  public averageCpu: AverageData = { logs: [], graph: [] };
@@ -78,11 +91,29 @@ class Stats {
78
91
  private lastValue: { [key: string]: number } = {};
79
92
  private prevTextTime: number;
80
93
 
94
+ private readonly VSYNC_RATES: VSyncInfo[] = [
95
+ { refreshRate: 60, frameTime: 16.67 },
96
+ { refreshRate: 75, frameTime: 13.33 },
97
+ { refreshRate: 90, frameTime: 11.11 },
98
+ { refreshRate: 120, frameTime: 8.33 },
99
+ { refreshRate: 144, frameTime: 6.94 },
100
+ { refreshRate: 165, frameTime: 6.06 },
101
+ { refreshRate: 240, frameTime: 4.17 }
102
+ ];
103
+ private detectedVSync: VSyncInfo | null = null;
104
+ private frameTimeHistory: number[] = [];
105
+ private readonly HISTORY_SIZE = 120; // 2 seconds worth of frames at 60fps
106
+ private readonly VSYNC_THRESHOLD = 0.05; // 5% tolerance
107
+ private lastFrameTime: number = 0;
108
+
109
+
81
110
 
82
111
  static Panel = Panel;
83
112
 
84
113
  constructor({
85
114
  trackGPU = false,
115
+ trackCPT = false,
116
+ trackHz = false,
86
117
  logsPerSecond = 4,
87
118
  graphsPerSecond = 30,
88
119
  samplesLog = 40,
@@ -96,6 +127,8 @@ class Stats {
96
127
  this.horizontal = horizontal;
97
128
  this.minimal = minimal;
98
129
  this.trackGPU = trackGPU;
130
+ this.trackCPT = trackCPT;
131
+ this.trackHz = trackHz;
99
132
  this.samplesLog = samplesLog;
100
133
  this.samplesGraph = samplesGraph;
101
134
  this.precision = precision;
@@ -114,9 +147,16 @@ class Stats {
114
147
 
115
148
  this.prevCpuTime = this.beginTime;
116
149
 
150
+ this._panelId = 0
117
151
  // Create panels
118
- this.fpsPanel = this.addPanel(new Stats.Panel('FPS', '#0ff', '#002'), 0);
119
- this.msPanel = this.addPanel(new Stats.Panel('CPU', '#0f0', '#020'), 1);
152
+ this.fpsPanel = this.addPanel(new Stats.Panel('FPS', '#0ff', '#002'));
153
+ this.msPanel = this.addPanel(new Stats.Panel('CPU', '#0f0', '#020'));
154
+
155
+ if (this.trackHz === true) {
156
+ this.vsyncPanel = new PanelVSync('', '#f0f', '#202');
157
+ this.dom.appendChild(this.vsyncPanel.canvas);
158
+ this.vsyncPanel.setOffset(56, 35);
159
+ }
120
160
 
121
161
  this.setupEventListeners();
122
162
  }
@@ -148,10 +188,10 @@ class Stats {
148
188
  };
149
189
 
150
190
  private handleResize = (): void => {
151
- this.resizePanel(this.fpsPanel, 0);
152
- this.resizePanel(this.msPanel, 1);
153
- if (this.gpuPanel) this.resizePanel(this.gpuPanel, 2);
154
- if (this.gpuPanelCompute) this.resizePanel(this.gpuPanelCompute, 3);
191
+ this.resizePanel(this.fpsPanel);
192
+ this.resizePanel(this.msPanel);
193
+ if (this.gpuPanel) this.resizePanel(this.gpuPanel);
194
+ if (this.gpuPanelCompute) this.resizePanel(this.gpuPanelCompute);
155
195
  };
156
196
 
157
197
  public async init(
@@ -190,24 +230,26 @@ class Stats {
190
230
 
191
231
  private async handleWebGPURenderer(renderer: any): Promise<boolean> {
192
232
  if (renderer.isWebGPURenderer) {
193
- if (this.trackGPU) {
233
+ if (this.trackGPU || this.trackCPT) {
194
234
  renderer.backend.trackTimestamp = true;
195
235
  if (await renderer.hasFeatureAsync('timestamp-query')) {
196
236
  this.initializeWebGPUPanels();
197
237
  }
198
238
  }
199
239
  this.info = renderer.info;
240
+ this.patchThreeWebGPU(renderer);
200
241
  return true;
201
242
  }
202
243
  return false;
203
244
  }
204
245
 
205
246
  private initializeWebGPUPanels(): void {
206
- this.gpuPanel = this.addPanel(new Stats.Panel('GPU', '#ff0', '#220'), 2);
207
- this.gpuPanelCompute = this.addPanel(
208
- new Stats.Panel('CPT', '#e1e1e1', '#212121'),
209
- 3
210
- );
247
+ if (this.trackGPU) {
248
+ this.gpuPanel = this.addPanel(new Stats.Panel('GPU', '#ff0', '#220'));
249
+ }
250
+ if (this.trackCPT) {
251
+ this.gpuPanelCompute = this.addPanel(new Stats.Panel('CPT', '#e1e1e1', '#212121'));
252
+ }
211
253
  }
212
254
 
213
255
  private initializeWebGL(
@@ -237,15 +279,13 @@ class Stats {
237
279
  if (this.gl) {
238
280
  this.ext = this.gl.getExtension('EXT_disjoint_timer_query_webgl2');
239
281
  if (this.ext) {
240
- this.gpuPanel = this.addPanel(new Stats.Panel('GPU', '#ff0', '#220'), 2);
282
+ this.gpuPanel = this.addPanel(new Stats.Panel('GPU', '#ff0', '#220'));
241
283
  }
242
284
  }
243
285
  }
244
286
 
245
287
  public begin(): void {
246
- if (!this.isRunningCPUProfiling) {
247
- this.beginProfiling('cpu-started');
248
- }
288
+ this.beginProfiling('cpu-started');
249
289
 
250
290
  if (!this.gl || !this.ext) return;
251
291
 
@@ -266,15 +306,14 @@ class Stats {
266
306
  this.gpuQueries.push({ query: this.activeQuery });
267
307
  this.activeQuery = null;
268
308
  }
309
+
310
+ this.endProfiling('cpu-started', 'cpu-finished', 'cpu-duration');
269
311
  }
270
312
 
271
313
  public update(): void {
272
- // Always end the current CPU profiling if it's running
273
- if (this.isRunningCPUProfiling) {
274
- this.endProfiling('cpu-started', 'cpu-finished', 'cpu-duration');
275
- // Add to averages immediately after getting the duration
276
- // this.addToAverage(this.totalCpuDuration, this.averageCpu);
277
- }
314
+
315
+
316
+ this.endProfiling('cpu-started', 'cpu-finished', 'cpu-duration');
278
317
 
279
318
  if (!this.info) {
280
319
  this.processGpuQueries();
@@ -294,11 +333,10 @@ class Stats {
294
333
  private resetCounters(): void {
295
334
  this.renderCount = 0;
296
335
  this.totalCpuDuration = 0;
297
- this.beginProfiling('cpu-started');
298
336
  this.beginTime = this.endInternal();
299
337
  }
300
338
 
301
- resizePanel(panel: Panel, offset: number) {
339
+ resizePanel(panel: Panel) {
302
340
 
303
341
  panel.canvas.style.position = 'absolute';
304
342
 
@@ -311,23 +349,24 @@ class Stats {
311
349
  panel.canvas.style.display = 'block';
312
350
  if (this.horizontal) {
313
351
  panel.canvas.style.top = '0px';
314
- panel.canvas.style.left = offset * panel.WIDTH / panel.PR + 'px';
352
+ panel.canvas.style.left = panel.id * panel.WIDTH / panel.PR + 'px';
315
353
  } else {
316
354
  panel.canvas.style.left = '0px';
317
- panel.canvas.style.top = offset * panel.HEIGHT / panel.PR + 'px';
355
+ panel.canvas.style.top = panel.id * panel.HEIGHT / panel.PR + 'px';
318
356
 
319
357
  }
320
358
  }
321
359
 
322
360
  }
323
- addPanel(panel: Panel, offset: number) {
361
+ addPanel(panel: Panel) {
324
362
 
325
363
  if (panel.canvas) {
326
364
 
327
365
  this.dom.appendChild(panel.canvas);
366
+ panel.id = this._panelId;
367
+ this.resizePanel(panel);
328
368
 
329
- this.resizePanel(panel, offset);
330
-
369
+ this._panelId++;
331
370
  }
332
371
 
333
372
  return panel;
@@ -369,6 +408,58 @@ class Stats {
369
408
  }
370
409
  });
371
410
 
411
+ }
412
+ private detectVSync(currentTime: number): void {
413
+ if (this.lastFrameTime === 0) {
414
+ this.lastFrameTime = currentTime;
415
+ return;
416
+ }
417
+
418
+ // Calculate frame time
419
+ const frameTime = currentTime - this.lastFrameTime;
420
+ this.lastFrameTime = currentTime;
421
+
422
+ // Add to histories
423
+ this.frameTimeHistory.push(frameTime);
424
+ if (this.frameTimeHistory.length > this.HISTORY_SIZE) {
425
+ this.frameTimeHistory.shift();
426
+ }
427
+
428
+ // Only start detection when we have enough samples
429
+ if (this.frameTimeHistory.length < 60) return;
430
+
431
+ // Calculate average frame time
432
+ const avgFrameTime = this.frameTimeHistory.reduce((a, b) => a + b) / this.frameTimeHistory.length;
433
+
434
+ // Calculate frame time stability (standard deviation)
435
+ const variance = this.frameTimeHistory.reduce((acc, time) =>
436
+ acc + Math.pow(time - avgFrameTime, 2), 0) / this.frameTimeHistory.length;
437
+ const stability = Math.sqrt(variance);
438
+
439
+ // Only proceed if frame timing is relatively stable
440
+ if (stability > 2) { // 2ms stability threshold
441
+ this.detectedVSync = null;
442
+ return;
443
+ }
444
+
445
+ // Find the closest VSync rate based on frame time
446
+ let closestMatch: VSyncInfo | null = null;
447
+ let smallestDiff = Infinity;
448
+
449
+ for (const rate of this.VSYNC_RATES) {
450
+ const diff = Math.abs(avgFrameTime - rate.frameTime);
451
+ if (diff < smallestDiff) {
452
+ smallestDiff = diff;
453
+ closestMatch = rate;
454
+ }
455
+ }
456
+
457
+ if (closestMatch && (smallestDiff / closestMatch.frameTime <= this.VSYNC_THRESHOLD)) {
458
+ this.detectedVSync = closestMatch;
459
+ } else {
460
+ this.detectedVSync = null;
461
+ }
462
+
372
463
  }
373
464
 
374
465
  endInternal() {
@@ -394,7 +485,7 @@ class Stats {
394
485
  if (this.gpuPanel) {
395
486
  this.updatePanelComponents(this.gpuPanel, this.averageGpu, this.precision, shouldUpdateText, shouldUpdateGraph);
396
487
  }
397
- if (this.gpuPanelCompute) {
488
+ if (this.trackCPT && this.gpuPanelCompute) {
398
489
  this.updatePanelComponents(this.gpuPanelCompute, this.averageGpuCompute, this.precision, shouldUpdateText, shouldUpdateGraph);
399
490
  }
400
491
 
@@ -405,6 +496,16 @@ class Stats {
405
496
  this.prevGraphTime = currentTime;
406
497
  }
407
498
 
499
+ if (this.vsyncPanel !== null) {
500
+ this.detectVSync(currentTime);
501
+
502
+ const vsyncValue = this.detectedVSync?.refreshRate || 0;
503
+
504
+ if (shouldUpdateText && vsyncValue > 0) {
505
+ this.vsyncPanel.update(vsyncValue, vsyncValue);
506
+ }
507
+ }
508
+
408
509
  return currentTime;
409
510
  }
410
511
 
@@ -462,7 +563,6 @@ class Stats {
462
563
  if (window.performance) {
463
564
 
464
565
  window.performance.mark(marker);
465
- this.isRunningCPUProfiling = true
466
566
 
467
567
  }
468
568
 
@@ -470,12 +570,11 @@ class Stats {
470
570
 
471
571
  endProfiling(startMarker: string | PerformanceMeasureOptions | undefined, endMarker: string | undefined, measureName: string) {
472
572
 
473
- if (window.performance && endMarker && this.isRunningCPUProfiling) {
573
+ if (window.performance && endMarker) {
474
574
 
475
575
  window.performance.mark(endMarker);
476
576
  const cpuMeasure = performance.measure(measureName, startMarker, endMarker);
477
577
  this.totalCpuDuration += cpuMeasure.duration;
478
- this.isRunningCPUProfiling = false
479
578
 
480
579
  }
481
580
 
@@ -539,6 +638,7 @@ class Stats {
539
638
  }
540
639
 
541
640
  private updateAverages(): void {
641
+
542
642
  this.addToAverage(this.totalCpuDuration, this.averageCpu);
543
643
  this.addToAverage(this.totalGpuDuration, this.averageGpu);
544
644
  // Add GPU Compute to the main update flow
@@ -549,7 +649,7 @@ class Stats {
549
649
 
550
650
  addToAverage(value: number, averageArray: { logs: any; graph: any; }) {
551
651
  // Validate value
552
- // if (value === undefined || value === null || isNaN(value)) {
652
+ // if (value === undefined || value === null || isNaN(value) || value === 0) {
553
653
  // return;
554
654
  // }
555
655
 
@@ -572,6 +672,22 @@ class Stats {
572
672
 
573
673
  }
574
674
 
675
+ patchThreeWebGPU(renderer: any) {
676
+
677
+ const originalAnimationLoop = renderer.info.reset
678
+
679
+ const statsInstance = this;
680
+
681
+ renderer.info.reset = function () {
682
+
683
+ statsInstance.beginProfiling('cpu-started');
684
+
685
+ originalAnimationLoop.call(this);
686
+
687
+ }
688
+
689
+ }
690
+
575
691
  patchThreeRenderer(renderer: any) {
576
692
 
577
693
  // Store the original render method
@@ -583,13 +699,13 @@ class Stats {
583
699
  // Override the render method on the prototype
584
700
  renderer.render = function (scene: THREE.Scene, camera: THREE.Camera) {
585
701
 
586
-
587
702
  statsInstance.begin(); // Start tracking for this render call
588
703
 
589
704
  // Call the original render method
590
705
  originalRenderMethod.call(this, scene, camera);
591
706
 
592
707
  statsInstance.end(); // End tracking for this render call
708
+
593
709
  };
594
710
 
595
711
 
package/lib/panel.ts CHANGED
@@ -5,6 +5,7 @@ class Panel {
5
5
  fg: string;
6
6
  bg: string;
7
7
  gradient: CanvasGradient | null;
8
+ id: number = 0;
8
9
  PR: number;
9
10
  WIDTH: number;
10
11
  HEIGHT: number;
@@ -81,7 +82,7 @@ class Panel {
81
82
  return gradient;
82
83
  }
83
84
 
84
- private initializeCanvas() {
85
+ public initializeCanvas() {
85
86
  if (!this.context) return;
86
87
 
87
88
  this.context.imageSmoothingEnabled = false;
@@ -104,7 +105,7 @@ class Panel {
104
105
  }
105
106
 
106
107
  // Update only text portion
107
- update(value: number, maxValue: number, decimals: number = 0) {
108
+ public update(value: number, maxValue: number, decimals: number = 0) {
108
109
  if (!this.context || !this.gradient) return;
109
110
 
110
111
  const min = Math.min(Infinity, value);
@@ -125,7 +126,7 @@ class Panel {
125
126
  }
126
127
 
127
128
  // Update only graph portion
128
- updateGraph(valueGraph: number, maxGraph: number) {
129
+ public updateGraph(valueGraph: number, maxGraph: number) {
129
130
  if (!this.context || !this.gradient) return;
130
131
 
131
132
  // Handle zero values appropriately
@@ -0,0 +1,74 @@
1
+ import { Panel } from './panel';
2
+
3
+ class PanelVSync extends Panel {
4
+ private readonly SMALL_HEIGHT: number;
5
+ private vsyncValue: number = 0;
6
+
7
+ constructor(name: string, fg: string, bg: string) {
8
+ super(name, fg, bg);
9
+
10
+ // Redefine dimensions for a smaller panel
11
+ this.SMALL_HEIGHT = 9 * this.PR; // Smaller height
12
+ this.HEIGHT = this.SMALL_HEIGHT;
13
+ this.TEXT_Y = 0 * this.PR; // Adjust text position
14
+
15
+ // Resize the canvas
16
+ this.canvas.height = this.HEIGHT;
17
+ this.canvas.style.height = '9px'; // Match the new height
18
+
19
+ // Style for overlay positioning
20
+ this.canvas.style.cssText = `
21
+ width: 90px;
22
+ height: 9px;
23
+ position: absolute;
24
+ top: 0;
25
+ left: 0;
26
+ pointer-events: none; // Allow clicks to pass through
27
+ `;
28
+
29
+ // Reinitialize with new dimensions
30
+ this.initializeCanvas();
31
+ }
32
+
33
+ public initializeCanvas() {
34
+ if (!this.context) return;
35
+
36
+ this.context.imageSmoothingEnabled = false;
37
+
38
+ // Larger font for better visibility
39
+ this.context.font = 'bold ' + (9 * this.PR) + 'px Helvetica,Arial,sans-serif';
40
+ this.context.textBaseline = 'top';
41
+
42
+ this.context.globalAlpha = 1;
43
+ }
44
+
45
+ // Override update for VSync-specific display
46
+ public update(value: number, _maxValue: number, _decimals: number = 0) {
47
+ if (!this.context) return;
48
+
49
+ this.vsyncValue = value;
50
+
51
+ this.context.clearRect(0, 0, this.WIDTH, this.HEIGHT);
52
+ // Draw VSync text
53
+ this.context.globalAlpha = 1;
54
+ this.context.fillStyle = this.bg;
55
+ this.context.fillText(
56
+ `${value.toFixed(0)}Hz`,
57
+ this.TEXT_X,
58
+ this.TEXT_Y
59
+ );
60
+ }
61
+
62
+ // Override updateGraph to do nothing (we don't need a graph for VSync)
63
+ public updateGraph(_valueGraph: number, _maxGraph: number) {
64
+ // No graph needed for VSync display
65
+ return;
66
+ }
67
+
68
+ // Method to set the offset position relative to parent panel
69
+ public setOffset(x: number, y: number) {
70
+ this.canvas.style.transform = `translate(${x}px, ${y}px)`;
71
+ }
72
+ }
73
+
74
+ export { PanelVSync };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stats-gl",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "type": "module",
5
5
  "author": "Renaud ROHLINGER (https://github.com/RenaudRohlinger)",
6
6
  "homepage": "https://github.com/RenaudRohlinger/stats-gl",