@gnsx/three 0.185.8 → 0.185.10

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.
@@ -1,4 +1,7 @@
1
1
  // WITH_GENESYS
2
+ import { TimestampQuery } from '../constants.js';
3
+ import { warnOnce } from '../utils.js';
4
+
2
5
  /**
3
6
  * ProfilerService — per-label CPU timing with ring-buffer aggregation and DevTools integration.
4
7
  *
@@ -19,12 +22,16 @@ const RING_SIZE = 120;
19
22
  const FRAME_BUDGET_MS = 1000 / 60;
20
23
  const TRACE_TID_MAIN = 1;
21
24
  const TRACE_TID_ASYNC = 2;
25
+ const TRACE_TID_GPU = 3;
22
26
  const NOOP = () => {};
23
27
 
24
28
  const DUMMY_SPAN = Object.freeze( { label: '', t0: 0, _seq: 0 } );
29
+ const DUMMY_GPU_SPAN = Object.freeze( { label: '', renderer: null, t0: 0, _seq: 0, _generation: 0 } );
25
30
  const NOOP_BEGIN_SPAN = () => DUMMY_SPAN;
26
31
  const NOOP_END_SPAN = () => {};
27
32
 
33
+ const NOOP_BEGIN_GPU_SPAN = () => DUMMY_GPU_SPAN;
34
+
28
35
  class ProfilerServiceClass {
29
36
 
30
37
  constructor() {
@@ -59,6 +66,18 @@ class ProfilerServiceClass {
59
66
  /** @type {Map<string, number>} Total (uncapped) invocation count since last reset, for calls-per-frame. */
60
67
  this.invocations = new Map();
61
68
 
69
+ /** @type {Map<string, Float64Array>} GPU time ring buffers (one per label). */
70
+ this.gpuBuffers = new Map();
71
+ /** @type {Map<string, number>} */
72
+ this.gpuCursors = new Map();
73
+ /** @type {Map<string, number>} */
74
+ this.gpuCounts = new Map();
75
+ /** @type {Map<string, number>} */
76
+ this.gpuInvocations = new Map();
77
+ /** @type {Map<Object, import('./ProfilerService.js').GpuRendererState>} */
78
+ this.gpuRendererStates = new Map();
79
+ this._gpuGeneration = 1;
80
+
62
81
  if ( this._enabled ) {
63
82
 
64
83
  this.traceStartTime = performance.now();
@@ -66,6 +85,8 @@ class ProfilerServiceClass {
66
85
  this.end = this._endImpl.bind( this );
67
86
  this.beginSpan = this._beginSpanImpl.bind( this );
68
87
  this.endSpan = this._endSpanImpl.bind( this );
88
+ this.beginGpu = this._beginGpuImpl.bind( this );
89
+ this.endGpu = this._endGpuImpl.bind( this );
69
90
  this._exposeGlobal();
70
91
  console.log( `[ProfilerService] auto-started from env (profile: ${this._profile})` );
71
92
 
@@ -75,6 +96,8 @@ class ProfilerServiceClass {
75
96
  this.end = NOOP;
76
97
  this.beginSpan = NOOP_BEGIN_SPAN;
77
98
  this.endSpan = NOOP_END_SPAN;
99
+ this.beginGpu = NOOP_BEGIN_GPU_SPAN;
100
+ this.endGpu = NOOP_END_SPAN;
78
101
 
79
102
  }
80
103
 
@@ -108,6 +131,8 @@ class ProfilerServiceClass {
108
131
  this.end = this._endImpl.bind( this );
109
132
  this.beginSpan = this._beginSpanImpl.bind( this );
110
133
  this.endSpan = this._endSpanImpl.bind( this );
134
+ this.beginGpu = this._beginGpuImpl.bind( this );
135
+ this.endGpu = this._endGpuImpl.bind( this );
111
136
  this._exposeGlobal();
112
137
  console.log( `[ProfilerService] enabled (profile: ${this._profile}) — call __gnsx_profiler.report() or downloadTrace() from the console` );
113
138
 
@@ -121,6 +146,9 @@ class ProfilerServiceClass {
121
146
  this.end = NOOP;
122
147
  this.beginSpan = NOOP_BEGIN_SPAN;
123
148
  this.endSpan = NOOP_END_SPAN;
149
+ this.beginGpu = NOOP_BEGIN_GPU_SPAN;
150
+ this.endGpu = NOOP_END_SPAN;
151
+ this._detachGpuRenderers();
124
152
  console.log( '[ProfilerService] disabled' );
125
153
 
126
154
  }
@@ -141,6 +169,467 @@ class ProfilerServiceClass {
141
169
 
142
170
  }
143
171
 
172
+ /**
173
+ * Enables GPU timestamp collection for a renderer.
174
+ *
175
+ * @param {Object} renderer
176
+ * @return {Promise<boolean>} Whether GPU timestamps are available.
177
+ */
178
+ async attachGpuRenderer( renderer ) {
179
+
180
+ if ( this._enabled === false || renderer === null || renderer === undefined ) return false;
181
+
182
+ const existingState = this.gpuRendererStates.get( renderer );
183
+ if ( existingState !== undefined ) return existingState.available;
184
+
185
+ if ( renderer.isWebGLRenderer === true ) {
186
+
187
+ const gl = renderer.getContext();
188
+ const extension = gl.getExtension( 'EXT_disjoint_timer_query_webgl2' );
189
+ const state = {
190
+ kind: 'legacy-webgl',
191
+ available: extension !== null,
192
+ renderer,
193
+ gl,
194
+ extension,
195
+ activeSpan: null,
196
+ pendingSpans: [],
197
+ flushPromise: null,
198
+ flushScheduled: false,
199
+ };
200
+ this.gpuRendererStates.set( renderer, state );
201
+
202
+ if ( extension === null ) {
203
+
204
+ warnOnce( 'ProfilerService: EXT_disjoint_timer_query_webgl2 is unavailable; GPU profiling is disabled for this WebGLRenderer.' );
205
+
206
+ }
207
+
208
+ return state.available;
209
+
210
+ }
211
+
212
+ if ( renderer.isRenderer === true && renderer.backend !== undefined ) {
213
+
214
+ await renderer.init();
215
+ const previousTrackTimestamp = renderer.backend.trackTimestamp;
216
+ renderer.backend.trackTimestamp = true;
217
+
218
+ const available = renderer.backend.hasTimestamp === true && renderer.hasFeature( 'timestamp-query' ) === true;
219
+ const listener = ( type, uid, label ) => this._captureGpuTimestampQuery( renderer, type, uid, label );
220
+ const state = {
221
+ kind: 'common',
222
+ available,
223
+ renderer,
224
+ listener,
225
+ previousTrackTimestamp,
226
+ activeSpans: [],
227
+ pendingSpans: [],
228
+ queryCpuTimes: new Map(),
229
+ gpuTimestampOrigin: null,
230
+ gpuTraceOrigin: null,
231
+ flushPromise: null,
232
+ flushScheduled: false,
233
+ };
234
+ this.gpuRendererStates.set( renderer, state );
235
+
236
+ if ( available ) {
237
+
238
+ renderer.backend.addTimestampQueryListener( listener );
239
+
240
+ } else {
241
+
242
+ warnOnce( 'ProfilerService: Timestamp queries are unavailable; GPU profiling is disabled for this renderer.' );
243
+
244
+ }
245
+
246
+ return available;
247
+
248
+ }
249
+
250
+ warnOnce( 'ProfilerService: Unsupported renderer; expected Renderer or WebGLRenderer.' );
251
+ return false;
252
+
253
+ }
254
+
255
+ /**
256
+ * @param {string} label
257
+ * @param {Object} renderer
258
+ * @return {import('./ProfilerService.js').GpuSpanHandle}
259
+ */
260
+ _beginGpuImpl( label, renderer ) {
261
+
262
+ const state = this.gpuRendererStates.get( renderer );
263
+ if ( state === undefined || state.available === false ) return DUMMY_GPU_SPAN;
264
+
265
+ const handle = {
266
+ label,
267
+ renderer,
268
+ t0: performance.now(),
269
+ _seq: ++ this._markId,
270
+ _generation: this._gpuGeneration,
271
+ };
272
+
273
+ if ( state.kind === 'common' ) {
274
+
275
+ handle._queries = {
276
+ [ TimestampQuery.RENDER ]: new Set(),
277
+ [ TimestampQuery.COMPUTE ]: new Set(),
278
+ };
279
+ state.activeSpans.push( handle );
280
+ return handle;
281
+
282
+ }
283
+
284
+ if ( state.activeSpan !== null ) {
285
+
286
+ warnOnce( 'ProfilerService: Nested GPU spans are unsupported by legacy WebGLRenderer.' );
287
+ return DUMMY_GPU_SPAN;
288
+
289
+ }
290
+
291
+ const query = state.gl.createQuery();
292
+ if ( query === null ) return DUMMY_GPU_SPAN;
293
+
294
+ try {
295
+
296
+ state.gl.beginQuery( state.extension.TIME_ELAPSED_EXT, query );
297
+ handle._query = query;
298
+ state.activeSpan = handle;
299
+ return handle;
300
+
301
+ } catch ( error ) {
302
+
303
+ state.gl.deleteQuery( query );
304
+ warnOnce( `ProfilerService: Unable to begin a WebGL GPU span (${error.message}).` );
305
+ return DUMMY_GPU_SPAN;
306
+
307
+ }
308
+
309
+ }
310
+
311
+ /**
312
+ * @param {import('./ProfilerService.js').GpuSpanHandle} handle
313
+ */
314
+ _endGpuImpl( handle ) {
315
+
316
+ if ( handle._seq === 0 || handle._generation !== this._gpuGeneration ) return;
317
+
318
+ const state = this.gpuRendererStates.get( handle.renderer );
319
+ if ( state === undefined || state.available === false ) return;
320
+
321
+ handle._endTime = performance.now();
322
+
323
+ if ( state.kind === 'common' ) {
324
+
325
+ const index = state.activeSpans.indexOf( handle );
326
+ if ( index === - 1 ) return;
327
+
328
+ state.activeSpans.splice( index, 1 );
329
+ if ( handle._queries.render.size > 0 || handle._queries.compute.size > 0 ) {
330
+
331
+ state.pendingSpans.push( handle );
332
+ this._scheduleGpuFlush( state );
333
+
334
+ }
335
+
336
+ return;
337
+
338
+ }
339
+
340
+ if ( state.activeSpan !== handle ) return;
341
+
342
+ try {
343
+
344
+ state.gl.endQuery( state.extension.TIME_ELAPSED_EXT );
345
+ state.activeSpan = null;
346
+ state.pendingSpans.push( handle );
347
+ this._scheduleGpuFlush( state );
348
+
349
+ } catch ( error ) {
350
+
351
+ state.activeSpan = null;
352
+ state.gl.deleteQuery( handle._query );
353
+ warnOnce( `ProfilerService: Unable to end a WebGL GPU span (${error.message}).` );
354
+
355
+ }
356
+
357
+ }
358
+
359
+ /**
360
+ * Resolves pending GPU timestamp queries for a renderer.
361
+ *
362
+ * @param {Object} renderer
363
+ * @return {Promise<void>}
364
+ */
365
+ async flushGpu( renderer ) {
366
+
367
+ const state = this.gpuRendererStates.get( renderer );
368
+ if ( state === undefined || state.available === false ) return;
369
+ if ( state.flushPromise !== null ) return state.flushPromise;
370
+
371
+ state.flushPromise = state.kind === 'common'
372
+ ? this._flushCommonGpuState( state )
373
+ : this._flushLegacyWebGLState( state );
374
+
375
+ try {
376
+
377
+ await state.flushPromise;
378
+
379
+ } finally {
380
+
381
+ state.flushPromise = null;
382
+ if ( state.pendingSpans.length > 0 ) this._scheduleGpuFlush( state );
383
+
384
+ }
385
+
386
+ }
387
+
388
+ /**
389
+ * @param {Object} renderer
390
+ * @param {'render'|'compute'} type
391
+ * @param {string} uid
392
+ * @param {?string} [label=null]
393
+ */
394
+ _captureGpuTimestampQuery( renderer, type, uid, label = null ) {
395
+
396
+ const state = this.gpuRendererStates.get( renderer );
397
+ if ( state === undefined || state.kind !== 'common' ) return;
398
+
399
+ const cpuTime = performance.now();
400
+ state.queryCpuTimes.set( uid, cpuTime );
401
+
402
+ for ( const span of state.activeSpans ) {
403
+
404
+ span._queries[ type ].add( uid );
405
+
406
+ }
407
+
408
+ if ( label !== null && label !== undefined && label !== '' ) {
409
+
410
+ const passSpan = {
411
+ label: `GPU pass: ${label}`,
412
+ renderer,
413
+ t0: cpuTime,
414
+ _seq: ++ this._markId,
415
+ _generation: this._gpuGeneration,
416
+ _queries: {
417
+ [ TimestampQuery.RENDER ]: new Set(),
418
+ [ TimestampQuery.COMPUTE ]: new Set(),
419
+ },
420
+ };
421
+ passSpan._queries[ type ].add( uid );
422
+ state.pendingSpans.push( passSpan );
423
+ this._scheduleGpuFlush( state );
424
+
425
+ }
426
+
427
+ }
428
+
429
+ /**
430
+ * @param {import('./ProfilerService.js').GpuRendererState} state
431
+ */
432
+ _scheduleGpuFlush( state ) {
433
+
434
+ if ( state.flushScheduled ) return;
435
+ state.flushScheduled = true;
436
+
437
+ const callback = () => {
438
+
439
+ state.flushScheduled = false;
440
+ if ( this.gpuRendererStates.get( state.renderer ) === state ) {
441
+
442
+ this.flushGpu( state.renderer ).catch( error => {
443
+
444
+ warnOnce( `ProfilerService: Unable to resolve GPU timestamps (${error.message}).` );
445
+
446
+ } );
447
+
448
+ }
449
+
450
+ };
451
+
452
+ if ( typeof requestAnimationFrame === 'function' ) {
453
+
454
+ requestAnimationFrame( callback );
455
+
456
+ } else {
457
+
458
+ setTimeout( callback, 0 );
459
+
460
+ }
461
+
462
+ }
463
+
464
+ /**
465
+ * @param {import('./ProfilerService.js').CommonGpuRendererState} state
466
+ */
467
+ async _flushCommonGpuState( state ) {
468
+
469
+ const spans = state.pendingSpans.splice( 0 );
470
+ if ( spans.length === 0 ) return;
471
+
472
+ const hasRenderQueries = spans.some( span => span._queries.render.size > 0 );
473
+ const hasComputeQueries = spans.some( span => span._queries.compute.size > 0 );
474
+ const resolutions = [];
475
+
476
+ if ( hasRenderQueries ) resolutions.push( state.renderer.resolveTimestampsAsync( TimestampQuery.RENDER ) );
477
+ if ( hasComputeQueries ) resolutions.push( state.renderer.resolveTimestampsAsync( TimestampQuery.COMPUTE ) );
478
+ await Promise.all( resolutions );
479
+
480
+ if ( this._enabled === false ) return;
481
+
482
+ if ( state.gpuTimestampOrigin === null ) {
483
+
484
+ let earliestRange = null;
485
+ let earliestUid = null;
486
+ for ( const span of spans ) {
487
+
488
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
489
+
490
+ for ( const uid of span._queries[ type ] ) {
491
+
492
+ const range = state.renderer.backend.getTimestampRange( uid );
493
+ if ( range !== null && ( earliestRange === null || range.start < earliestRange.start ) ) {
494
+
495
+ earliestRange = range;
496
+ earliestUid = uid;
497
+
498
+ }
499
+
500
+ }
501
+
502
+ }
503
+
504
+ }
505
+
506
+ if ( earliestRange !== null ) {
507
+
508
+ state.gpuTimestampOrigin = earliestRange.start;
509
+ state.gpuTraceOrigin = state.queryCpuTimes.get( earliestUid ) ?? spans[ 0 ].t0;
510
+
511
+ }
512
+
513
+ }
514
+
515
+ const resolvedUids = new Set();
516
+ for ( const span of spans ) {
517
+
518
+ if ( span._generation !== this._gpuGeneration ) continue;
519
+
520
+ let duration = 0;
521
+ let resolvedQueries = 0;
522
+ let rangedQueries = 0;
523
+ let gpuStart = null;
524
+ let gpuEnd = null;
525
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
526
+
527
+ for ( const uid of span._queries[ type ] ) {
528
+
529
+ if ( state.renderer.backend.hasTimestampQuery( uid ) ) {
530
+
531
+ duration += state.renderer.backend.getTimestamp( uid );
532
+ resolvedQueries ++;
533
+ resolvedUids.add( uid );
534
+
535
+ const range = state.renderer.backend.getTimestampRange( uid );
536
+ if ( range !== null ) {
537
+
538
+ rangedQueries ++;
539
+ if ( gpuStart === null || range.start < gpuStart ) gpuStart = range.start;
540
+ if ( gpuEnd === null || range.end > gpuEnd ) gpuEnd = range.end;
541
+
542
+ }
543
+
544
+ }
545
+
546
+ }
547
+
548
+ }
549
+
550
+ if ( resolvedQueries > 0 ) {
551
+
552
+ let startTime = span.t0;
553
+ if ( rangedQueries === resolvedQueries && gpuStart !== null && gpuEnd !== null && state.gpuTimestampOrigin !== null ) {
554
+
555
+ duration = Number( gpuEnd - gpuStart ) / 1e6;
556
+ startTime = state.gpuTraceOrigin + Number( gpuStart - state.gpuTimestampOrigin ) / 1e6;
557
+
558
+ }
559
+
560
+ this._commitGpuDurationSample( span.label, startTime, duration );
561
+
562
+ }
563
+
564
+ }
565
+
566
+ for ( const uid of resolvedUids ) state.queryCpuTimes.delete( uid );
567
+
568
+ }
569
+
570
+ /**
571
+ * @param {import('./ProfilerService.js').LegacyWebGLGpuRendererState} state
572
+ */
573
+ async _flushLegacyWebGLState( state ) {
574
+
575
+ const spans = state.pendingSpans.splice( 0 );
576
+
577
+ for ( const span of spans ) {
578
+
579
+ const duration = await this._resolveLegacyWebGLQuery( state, span._query );
580
+ if ( duration !== null && span._generation === this._gpuGeneration && this._enabled ) {
581
+
582
+ this._commitGpuDurationSample( span.label, span.t0, duration );
583
+
584
+ }
585
+
586
+ }
587
+
588
+ }
589
+
590
+ /**
591
+ * @param {import('./ProfilerService.js').LegacyWebGLGpuRendererState} state
592
+ * @param {WebGLQuery} query
593
+ * @return {Promise<?number>}
594
+ */
595
+ _resolveLegacyWebGLQuery( state, query ) {
596
+
597
+ return new Promise( resolve => {
598
+
599
+ const poll = () => {
600
+
601
+ if ( state.gl.isContextLost() ) {
602
+
603
+ state.gl.deleteQuery( query );
604
+ resolve( null );
605
+ return;
606
+
607
+ }
608
+
609
+ const disjoint = state.gl.getParameter( state.extension.GPU_DISJOINT_EXT );
610
+ const available = state.gl.getQueryParameter( query, state.gl.QUERY_RESULT_AVAILABLE );
611
+
612
+ if ( available === false ) {
613
+
614
+ setTimeout( poll, 1 );
615
+ return;
616
+
617
+ }
618
+
619
+ const result = disjoint
620
+ ? null
621
+ : Number( state.gl.getQueryParameter( query, state.gl.QUERY_RESULT ) ) / 1e6;
622
+ state.gl.deleteQuery( query );
623
+ resolve( result );
624
+
625
+ };
626
+
627
+ poll();
628
+
629
+ } );
630
+
631
+ }
632
+
144
633
  /**
145
634
  * @param {string} label
146
635
  */
@@ -298,6 +787,45 @@ class ProfilerServiceClass {
298
787
 
299
788
  }
300
789
 
790
+ /**
791
+ * @param {string} label
792
+ * @param {number} startTime
793
+ * @param {number} durationMs
794
+ */
795
+ _commitGpuDurationSample( label, startTime, durationMs ) {
796
+
797
+ let buffer = this.gpuBuffers.get( label );
798
+ if ( buffer === undefined ) {
799
+
800
+ buffer = new Float64Array( RING_SIZE );
801
+ this.gpuBuffers.set( label, buffer );
802
+ this.gpuCursors.set( label, 0 );
803
+ this.gpuCounts.set( label, 0 );
804
+
805
+ }
806
+
807
+ const cursor = this.gpuCursors.get( label );
808
+ buffer[ cursor ] = durationMs;
809
+ this.gpuCursors.set( label, ( cursor + 1 ) % RING_SIZE );
810
+ this.gpuCounts.set( label, Math.min( this.gpuCounts.get( label ) + 1, RING_SIZE ) );
811
+ this.gpuInvocations.set( label, ( this.gpuInvocations.get( label ) ?? 0 ) + 1 );
812
+
813
+ if ( this._profile === 'full' ) {
814
+
815
+ this.traceEvents.push( {
816
+ name: label,
817
+ ph: 'X',
818
+ ts: Math.round( this._getTraceTimestamp( startTime ) ),
819
+ dur: Math.max( 1, Math.round( durationMs * 1000 ) ),
820
+ pid: 1,
821
+ tid: TRACE_TID_GPU,
822
+ cat: 'gnsx-gpu',
823
+ } );
824
+
825
+ }
826
+
827
+ }
828
+
301
829
  /**
302
830
  * @param {string} label
303
831
  * @return {number[]}
@@ -313,6 +841,21 @@ class ProfilerServiceClass {
313
841
 
314
842
  }
315
843
 
844
+ /**
845
+ * @param {string} label
846
+ * @return {number[]}
847
+ */
848
+ getValidGpuSamples( label ) {
849
+
850
+ const buffer = this.gpuBuffers.get( label );
851
+ const count = this.gpuCounts.get( label ) ?? 0;
852
+ if ( buffer === undefined || count === 0 ) return [];
853
+ return count < RING_SIZE
854
+ ? Array.from( buffer.subarray( 0, count ) )
855
+ : Array.from( buffer );
856
+
857
+ }
858
+
316
859
  /**
317
860
  * @param {string} label
318
861
  * @return {import('./ProfilerService.js').ProfilerStats|null}
@@ -360,6 +903,31 @@ class ProfilerServiceClass {
360
903
 
361
904
  }
362
905
 
906
+ /**
907
+ * @param {string} label
908
+ * @return {import('./ProfilerService.js').GpuProfilerStats|null}
909
+ */
910
+ getGpuStats( label ) {
911
+
912
+ const samples = this.getValidGpuSamples( label );
913
+ if ( samples.length === 0 ) return null;
914
+
915
+ const sorted = [ ...samples ].sort( ( a, b ) => a - b );
916
+ const avg = sorted.reduce( ( a, b ) => a + b, 0 ) / sorted.length;
917
+
918
+ return {
919
+ label,
920
+ samples: samples.length,
921
+ avg,
922
+ min: sorted[ 0 ],
923
+ max: sorted[ sorted.length - 1 ],
924
+ p95: sorted[ Math.floor( sorted.length * 0.95 ) ],
925
+ frameBudget: ( avg / FRAME_BUDGET_MS ) * 100,
926
+ totalInvocations: this.gpuInvocations.get( label ) ?? 0,
927
+ };
928
+
929
+ }
930
+
363
931
  /**
364
932
  * @return {import('./ProfilerService.js').ProfilerStats[]}
365
933
  */
@@ -371,10 +939,22 @@ class ProfilerServiceClass {
371
939
 
372
940
  }
373
941
 
942
+ /**
943
+ * @return {import('./ProfilerService.js').GpuProfilerStats[]}
944
+ */
945
+ getAllGpuStats() {
946
+
947
+ return [ ...this.gpuBuffers.keys() ]
948
+ .map( label => this.getGpuStats( label ) )
949
+ .filter( stats => stats !== null );
950
+
951
+ }
952
+
374
953
  report() {
375
954
 
376
955
  const stats = this.getAllStats();
377
- if ( stats.length === 0 ) {
956
+ const gpuStats = this.getAllGpuStats();
957
+ if ( stats.length === 0 && gpuStats.length === 0 ) {
378
958
 
379
959
  console.log( '[ProfilerService] No data. Enable profiling first and wait a few frames.' );
380
960
  return;
@@ -394,6 +974,24 @@ class ProfilerServiceClass {
394
974
  } ) )
395
975
  );
396
976
 
977
+ if ( gpuStats.length > 0 ) {
978
+
979
+ gpuStats.sort( ( a, b ) => b.avg - a.avg );
980
+ console.log( '[ProfilerService] GPU timings' );
981
+ console.table(
982
+ gpuStats.map( stats => ( {
983
+ label: stats.label,
984
+ 'avg ms': stats.avg.toFixed( 3 ),
985
+ 'min ms': stats.min.toFixed( 3 ),
986
+ 'max ms': stats.max.toFixed( 3 ),
987
+ 'p95 ms': stats.p95.toFixed( 3 ),
988
+ 'budget %': stats.frameBudget.toFixed( 1 ) + '%',
989
+ samples: stats.samples,
990
+ } ) )
991
+ );
992
+
993
+ }
994
+
397
995
  }
398
996
 
399
997
  /**
@@ -410,6 +1008,7 @@ class ProfilerServiceClass {
410
1008
 
411
1009
  } );
412
1010
  const hasAsync = slices.some( e => e.tid === TRACE_TID_ASYNC );
1011
+ const hasGpu = slices.some( e => e.tid === TRACE_TID_GPU );
413
1012
  /** @type {import('./ProfilerService.js').ChromeTraceMetadataEvent[]} */
414
1013
  const prefix = [
415
1014
  {
@@ -445,6 +1044,20 @@ class ProfilerServiceClass {
445
1044
 
446
1045
  }
447
1046
 
1047
+ if ( hasGpu ) {
1048
+
1049
+ prefix.push( {
1050
+ cat: '__metadata',
1051
+ name: 'thread_name',
1052
+ ph: 'M',
1053
+ pid: 1,
1054
+ tid: TRACE_TID_GPU,
1055
+ ts: 0,
1056
+ args: { name: 'GPU (device timestamps)' },
1057
+ } );
1058
+
1059
+ }
1060
+
448
1061
  return {
449
1062
  displayTimeUnit: 'ms',
450
1063
  traceEvents: [ ...prefix, ...slices ],
@@ -509,8 +1122,24 @@ class ProfilerServiceClass {
509
1122
 
510
1123
  }
511
1124
 
1125
+ /**
1126
+ * @return {import('./ProfilerService.js').GpuProfilerStats[]}
1127
+ */
1128
+ exportGpuJSON() {
1129
+
1130
+ return this.getAllGpuStats();
1131
+
1132
+ }
1133
+
512
1134
  _clearState() {
513
1135
 
1136
+ this._gpuGeneration ++;
1137
+ for ( const state of this.gpuRendererStates.values() ) {
1138
+
1139
+ this._clearGpuRendererState( state );
1140
+
1141
+ }
1142
+
514
1143
  this.buffers.clear();
515
1144
  this.cursors.clear();
516
1145
  this.counts.clear();
@@ -523,6 +1152,79 @@ class ProfilerServiceClass {
523
1152
  this.selfCounts.clear();
524
1153
  this.callStack.length = 0;
525
1154
  this.invocations.clear();
1155
+ this.gpuBuffers.clear();
1156
+ this.gpuCursors.clear();
1157
+ this.gpuCounts.clear();
1158
+ this.gpuInvocations.clear();
1159
+
1160
+ }
1161
+
1162
+ /**
1163
+ * @param {import('./ProfilerService.js').GpuRendererState} state
1164
+ */
1165
+ _clearGpuRendererState( state ) {
1166
+
1167
+ if ( state.kind === 'common' ) {
1168
+
1169
+ state.activeSpans.length = 0;
1170
+ state.pendingSpans.length = 0;
1171
+ state.queryCpuTimes.clear();
1172
+ state.gpuTimestampOrigin = null;
1173
+ state.gpuTraceOrigin = null;
1174
+ return;
1175
+
1176
+ }
1177
+
1178
+ if ( state.activeSpan !== null ) {
1179
+
1180
+ try {
1181
+
1182
+ state.gl.endQuery( state.extension.TIME_ELAPSED_EXT );
1183
+
1184
+ } catch {
1185
+
1186
+ // The context may have been lost while the query was active.
1187
+
1188
+ }
1189
+
1190
+ state.gl.deleteQuery( state.activeSpan._query );
1191
+ state.activeSpan = null;
1192
+
1193
+ }
1194
+
1195
+ if ( state.flushPromise === null ) {
1196
+
1197
+ for ( const span of state.pendingSpans ) {
1198
+
1199
+ state.gl.deleteQuery( span._query );
1200
+
1201
+ }
1202
+
1203
+ }
1204
+
1205
+ state.pendingSpans.length = 0;
1206
+
1207
+ }
1208
+
1209
+ _detachGpuRenderers() {
1210
+
1211
+ for ( const state of this.gpuRendererStates.values() ) {
1212
+
1213
+ if ( state.kind === 'common' && state.available ) {
1214
+
1215
+ state.renderer.backend.removeTimestampQueryListener( state.listener );
1216
+
1217
+ }
1218
+
1219
+ if ( state.kind === 'common' ) {
1220
+
1221
+ state.renderer.backend.trackTimestamp = state.previousTrackTimestamp;
1222
+
1223
+ }
1224
+
1225
+ }
1226
+
1227
+ this.gpuRendererStates.clear();
526
1228
 
527
1229
  }
528
1230
 
@@ -552,6 +1254,18 @@ class ProfilerServiceClass {
552
1254
  * @property {number|undefined} selfFrameBudget Exclusive time as percentage of a 60 fps frame budget.
553
1255
  */
554
1256
 
1257
+ /**
1258
+ * @typedef {Object} GpuProfilerStats
1259
+ * @property {string} label
1260
+ * @property {number} samples
1261
+ * @property {number} avg
1262
+ * @property {number} min
1263
+ * @property {number} max
1264
+ * @property {number} p95
1265
+ * @property {number} frameBudget Percentage of a 60 fps frame budget (16.67 ms)
1266
+ * @property {number} totalInvocations
1267
+ */
1268
+
555
1269
  /**
556
1270
  * @typedef {Object} SpanHandle
557
1271
  * @property {string} label
@@ -560,6 +1274,51 @@ class ProfilerServiceClass {
560
1274
  * @property {string} _startMark
561
1275
  */
562
1276
 
1277
+ /**
1278
+ * @typedef {Object} GpuSpanHandle
1279
+ * @property {string} label
1280
+ * @property {?Object} renderer
1281
+ * @property {number} t0
1282
+ * @property {number} _seq
1283
+ * @property {number} _generation
1284
+ * @property {{render: Set<string>, compute: Set<string>}} [_queries]
1285
+ * @property {WebGLQuery} [_query]
1286
+ * @property {number} [_endTime]
1287
+ */
1288
+
1289
+ /**
1290
+ * @typedef {Object} CommonGpuRendererState
1291
+ * @property {'common'} kind
1292
+ * @property {boolean} available
1293
+ * @property {Object} renderer
1294
+ * @property {function('render'|'compute', string, ?string): void} listener
1295
+ * @property {boolean} previousTrackTimestamp
1296
+ * @property {GpuSpanHandle[]} activeSpans
1297
+ * @property {GpuSpanHandle[]} pendingSpans
1298
+ * @property {Map<string, number>} queryCpuTimes
1299
+ * @property {?bigint} gpuTimestampOrigin
1300
+ * @property {?number} gpuTraceOrigin
1301
+ * @property {?Promise<void>} flushPromise
1302
+ * @property {boolean} flushScheduled
1303
+ */
1304
+
1305
+ /**
1306
+ * @typedef {Object} LegacyWebGLGpuRendererState
1307
+ * @property {'legacy-webgl'} kind
1308
+ * @property {boolean} available
1309
+ * @property {Object} renderer
1310
+ * @property {WebGL2RenderingContext} gl
1311
+ * @property {?EXT_disjoint_timer_query_webgl2} extension
1312
+ * @property {?GpuSpanHandle} activeSpan
1313
+ * @property {GpuSpanHandle[]} pendingSpans
1314
+ * @property {?Promise<void>} flushPromise
1315
+ * @property {boolean} flushScheduled
1316
+ */
1317
+
1318
+ /**
1319
+ * @typedef {CommonGpuRendererState|LegacyWebGLGpuRendererState} GpuRendererState
1320
+ */
1321
+
563
1322
  /**
564
1323
  * @typedef {Object} EndSpanOptions
565
1324
  * @property {boolean} [asyncTimeline] When true, trace slice uses `tid=2` (virtual async row).
@@ -573,7 +1332,7 @@ class ProfilerServiceClass {
573
1332
  * @property {number} dur
574
1333
  * @property {1} pid
575
1334
  * @property {number} tid
576
- * @property {'gnsx'} cat
1335
+ * @property {'gnsx'|'gnsx-gpu'} cat
577
1336
  */
578
1337
 
579
1338
  /**