@gnsx/three 0.185.9 → 0.185.11

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.
@@ -60003,6 +60003,7 @@ class TextureUtils {
60003
60003
  }
60004
60004
 
60005
60005
  // WITH_GENESYS
60006
+
60006
60007
  /**
60007
60008
  * ProfilerService — per-label CPU timing with ring-buffer aggregation and DevTools integration.
60008
60009
  *
@@ -60023,12 +60024,16 @@ const RING_SIZE = 120;
60023
60024
  const FRAME_BUDGET_MS = 1000 / 60;
60024
60025
  const TRACE_TID_MAIN = 1;
60025
60026
  const TRACE_TID_ASYNC = 2;
60027
+ const TRACE_TID_GPU = 3;
60026
60028
  const NOOP = () => {};
60027
60029
 
60028
60030
  const DUMMY_SPAN = Object.freeze( { label: '', t0: 0, _seq: 0 } );
60031
+ const DUMMY_GPU_SPAN = Object.freeze( { label: '', renderer: null, t0: 0, _seq: 0, _generation: 0 } );
60029
60032
  const NOOP_BEGIN_SPAN = () => DUMMY_SPAN;
60030
60033
  const NOOP_END_SPAN = () => {};
60031
60034
 
60035
+ const NOOP_BEGIN_GPU_SPAN = () => DUMMY_GPU_SPAN;
60036
+
60032
60037
  class ProfilerServiceClass {
60033
60038
 
60034
60039
  constructor() {
@@ -60063,6 +60068,18 @@ class ProfilerServiceClass {
60063
60068
  /** @type {Map<string, number>} Total (uncapped) invocation count since last reset, for calls-per-frame. */
60064
60069
  this.invocations = new Map();
60065
60070
 
60071
+ /** @type {Map<string, Float64Array>} GPU time ring buffers (one per label). */
60072
+ this.gpuBuffers = new Map();
60073
+ /** @type {Map<string, number>} */
60074
+ this.gpuCursors = new Map();
60075
+ /** @type {Map<string, number>} */
60076
+ this.gpuCounts = new Map();
60077
+ /** @type {Map<string, number>} */
60078
+ this.gpuInvocations = new Map();
60079
+ /** @type {Map<Object, import('./ProfilerService.js').GpuRendererState>} */
60080
+ this.gpuRendererStates = new Map();
60081
+ this._gpuGeneration = 1;
60082
+
60066
60083
  if ( this._enabled ) {
60067
60084
 
60068
60085
  this.traceStartTime = performance.now();
@@ -60070,6 +60087,8 @@ class ProfilerServiceClass {
60070
60087
  this.end = this._endImpl.bind( this );
60071
60088
  this.beginSpan = this._beginSpanImpl.bind( this );
60072
60089
  this.endSpan = this._endSpanImpl.bind( this );
60090
+ this.beginGpu = this._beginGpuImpl.bind( this );
60091
+ this.endGpu = this._endGpuImpl.bind( this );
60073
60092
  this._exposeGlobal();
60074
60093
  console.log( `[ProfilerService] auto-started from env (profile: ${this._profile})` );
60075
60094
 
@@ -60079,6 +60098,8 @@ class ProfilerServiceClass {
60079
60098
  this.end = NOOP;
60080
60099
  this.beginSpan = NOOP_BEGIN_SPAN;
60081
60100
  this.endSpan = NOOP_END_SPAN;
60101
+ this.beginGpu = NOOP_BEGIN_GPU_SPAN;
60102
+ this.endGpu = NOOP_END_SPAN;
60082
60103
 
60083
60104
  }
60084
60105
 
@@ -60112,6 +60133,8 @@ class ProfilerServiceClass {
60112
60133
  this.end = this._endImpl.bind( this );
60113
60134
  this.beginSpan = this._beginSpanImpl.bind( this );
60114
60135
  this.endSpan = this._endSpanImpl.bind( this );
60136
+ this.beginGpu = this._beginGpuImpl.bind( this );
60137
+ this.endGpu = this._endGpuImpl.bind( this );
60115
60138
  this._exposeGlobal();
60116
60139
  console.log( `[ProfilerService] enabled (profile: ${this._profile}) — call __gnsx_profiler.report() or downloadTrace() from the console` );
60117
60140
 
@@ -60125,6 +60148,9 @@ class ProfilerServiceClass {
60125
60148
  this.end = NOOP;
60126
60149
  this.beginSpan = NOOP_BEGIN_SPAN;
60127
60150
  this.endSpan = NOOP_END_SPAN;
60151
+ this.beginGpu = NOOP_BEGIN_GPU_SPAN;
60152
+ this.endGpu = NOOP_END_SPAN;
60153
+ this._detachGpuRenderers();
60128
60154
  console.log( '[ProfilerService] disabled' );
60129
60155
 
60130
60156
  }
@@ -60145,6 +60171,687 @@ class ProfilerServiceClass {
60145
60171
 
60146
60172
  }
60147
60173
 
60174
+ /**
60175
+ * Enables GPU timestamp collection for a renderer.
60176
+ *
60177
+ * @param {Object} renderer
60178
+ * @return {Promise<boolean>} Whether GPU timestamps are available.
60179
+ */
60180
+ async attachGpuRenderer( renderer ) {
60181
+
60182
+ if ( this._enabled === false || renderer === null || renderer === undefined ) return false;
60183
+
60184
+ const existingState = this.gpuRendererStates.get( renderer );
60185
+ if ( existingState !== undefined ) return existingState.available;
60186
+
60187
+ if ( renderer.isWebGLRenderer === true ) {
60188
+
60189
+ const gl = renderer.getContext();
60190
+ const extension = gl.getExtension( 'EXT_disjoint_timer_query_webgl2' );
60191
+ const state = {
60192
+ kind: 'legacy-webgl',
60193
+ available: extension !== null,
60194
+ renderer,
60195
+ gl,
60196
+ extension,
60197
+ activeSpan: null,
60198
+ pendingSpans: [],
60199
+ flushPromise: null,
60200
+ flushScheduled: false,
60201
+ };
60202
+ this.gpuRendererStates.set( renderer, state );
60203
+
60204
+ if ( extension === null ) {
60205
+
60206
+ warnOnce( 'ProfilerService: EXT_disjoint_timer_query_webgl2 is unavailable; GPU profiling is disabled for this WebGLRenderer.' );
60207
+
60208
+ }
60209
+
60210
+ return state.available;
60211
+
60212
+ }
60213
+
60214
+ if ( renderer.isRenderer === true && renderer.backend !== undefined ) {
60215
+
60216
+ await renderer.init();
60217
+ const previousTrackTimestamp = renderer.backend.trackTimestamp;
60218
+ renderer.backend.trackTimestamp = true;
60219
+
60220
+ const available = renderer.backend.hasTimestamp === true && renderer.hasFeature( 'timestamp-query' ) === true;
60221
+ const listener = ( type, uid, label ) => this._captureGpuTimestampQuery( renderer, type, uid, label );
60222
+ const state = {
60223
+ kind: 'common',
60224
+ available,
60225
+ renderer,
60226
+ listener,
60227
+ previousTrackTimestamp,
60228
+ activeSpans: [],
60229
+ pendingSpans: [],
60230
+ queryCpuTimes: new Map(),
60231
+ gpuTimestampOrigin: null,
60232
+ gpuTraceOrigin: null,
60233
+ flushPromise: null,
60234
+ flushScheduled: false,
60235
+ };
60236
+ this.gpuRendererStates.set( renderer, state );
60237
+
60238
+ if ( available ) {
60239
+
60240
+ renderer.backend.addTimestampQueryListener( listener );
60241
+
60242
+ } else {
60243
+
60244
+ warnOnce( 'ProfilerService: Timestamp queries are unavailable; GPU profiling is disabled for this renderer.' );
60245
+
60246
+ }
60247
+
60248
+ return available;
60249
+
60250
+ }
60251
+
60252
+ warnOnce( 'ProfilerService: Unsupported renderer; expected Renderer or WebGLRenderer.' );
60253
+ return false;
60254
+
60255
+ }
60256
+
60257
+ /**
60258
+ * @param {string} label
60259
+ * @param {Object} renderer
60260
+ * @return {import('./ProfilerService.js').GpuSpanHandle}
60261
+ */
60262
+ _beginGpuImpl( label, renderer ) {
60263
+
60264
+ const state = this.gpuRendererStates.get( renderer );
60265
+ if ( state === undefined || state.available === false ) return DUMMY_GPU_SPAN;
60266
+
60267
+ const handle = {
60268
+ label,
60269
+ renderer,
60270
+ t0: performance.now(),
60271
+ _seq: ++ this._markId,
60272
+ _generation: this._gpuGeneration,
60273
+ };
60274
+
60275
+ if ( state.kind === 'common' ) {
60276
+
60277
+ handle._queries = {
60278
+ [ TimestampQuery.RENDER ]: new Set(),
60279
+ [ TimestampQuery.COMPUTE ]: new Set(),
60280
+ };
60281
+ state.activeSpans.push( handle );
60282
+ return handle;
60283
+
60284
+ }
60285
+
60286
+ if ( state.activeSpan !== null ) {
60287
+
60288
+ warnOnce( 'ProfilerService: Nested GPU spans are unsupported by legacy WebGLRenderer.' );
60289
+ return DUMMY_GPU_SPAN;
60290
+
60291
+ }
60292
+
60293
+ const query = state.gl.createQuery();
60294
+ if ( query === null ) return DUMMY_GPU_SPAN;
60295
+
60296
+ try {
60297
+
60298
+ state.gl.beginQuery( state.extension.TIME_ELAPSED_EXT, query );
60299
+ handle._query = query;
60300
+ state.activeSpan = handle;
60301
+ return handle;
60302
+
60303
+ } catch ( error ) {
60304
+
60305
+ state.gl.deleteQuery( query );
60306
+ warnOnce( `ProfilerService: Unable to begin a WebGL GPU span (${error.message}).` );
60307
+ return DUMMY_GPU_SPAN;
60308
+
60309
+ }
60310
+
60311
+ }
60312
+
60313
+ /**
60314
+ * @param {import('./ProfilerService.js').GpuSpanHandle} handle
60315
+ */
60316
+ _endGpuImpl( handle ) {
60317
+
60318
+ if ( handle._seq === 0 || handle._generation !== this._gpuGeneration ) return;
60319
+
60320
+ const state = this.gpuRendererStates.get( handle.renderer );
60321
+ if ( state === undefined || state.available === false ) return;
60322
+
60323
+ handle._endTime = performance.now();
60324
+
60325
+ if ( state.kind === 'common' ) {
60326
+
60327
+ const index = state.activeSpans.indexOf( handle );
60328
+ if ( index === -1 ) return;
60329
+
60330
+ state.activeSpans.splice( index, 1 );
60331
+ if ( handle._queries.render.size > 0 || handle._queries.compute.size > 0 ) {
60332
+
60333
+ state.pendingSpans.push( handle );
60334
+ this._scheduleGpuFlush( state );
60335
+
60336
+ }
60337
+
60338
+ return;
60339
+
60340
+ }
60341
+
60342
+ if ( state.activeSpan !== handle ) return;
60343
+
60344
+ try {
60345
+
60346
+ state.gl.endQuery( state.extension.TIME_ELAPSED_EXT );
60347
+ state.activeSpan = null;
60348
+ state.pendingSpans.push( handle );
60349
+ this._scheduleGpuFlush( state );
60350
+
60351
+ } catch ( error ) {
60352
+
60353
+ state.activeSpan = null;
60354
+ state.gl.deleteQuery( handle._query );
60355
+ warnOnce( `ProfilerService: Unable to end a WebGL GPU span (${error.message}).` );
60356
+
60357
+ }
60358
+
60359
+ }
60360
+
60361
+ /**
60362
+ * Resolves pending GPU timestamp queries for a renderer.
60363
+ *
60364
+ * @param {Object} renderer
60365
+ * @return {Promise<void>}
60366
+ */
60367
+ async flushGpu( renderer ) {
60368
+
60369
+ const state = this.gpuRendererStates.get( renderer );
60370
+ if ( state === undefined || state.available === false ) return;
60371
+ if ( state.flushPromise !== null ) return state.flushPromise;
60372
+
60373
+ state.flushPromise = state.kind === 'common'
60374
+ ? this._flushCommonGpuState( state )
60375
+ : this._flushLegacyWebGLState( state );
60376
+
60377
+ try {
60378
+
60379
+ await state.flushPromise;
60380
+
60381
+ } finally {
60382
+
60383
+ state.flushPromise = null;
60384
+ if ( state.pendingSpans.length > 0 ) this._scheduleGpuFlush( state );
60385
+
60386
+ }
60387
+
60388
+ }
60389
+
60390
+ /**
60391
+ * @param {Object} renderer
60392
+ * @param {'render'|'compute'} type
60393
+ * @param {string} uid
60394
+ * @param {?string} [label=null]
60395
+ */
60396
+ _captureGpuTimestampQuery( renderer, type, uid, label = null ) {
60397
+
60398
+ const state = this.gpuRendererStates.get( renderer );
60399
+ if ( state === undefined || state.kind !== 'common' ) return;
60400
+
60401
+ const cpuTime = performance.now();
60402
+ state.queryCpuTimes.set( uid, cpuTime );
60403
+
60404
+ for ( const span of state.activeSpans ) {
60405
+
60406
+ span._queries[ type ].add( uid );
60407
+
60408
+ }
60409
+
60410
+ if ( label !== null && label !== undefined && label !== '' ) {
60411
+
60412
+ // Queue labelled passes with the active beginGpu span(s). Flushing
60413
+ // immediately would commit them before their parent envelope exists and
60414
+ // can anchor gpuTraceOrigin mid-frame, which breaks Speedscope nesting.
60415
+ const passSpan = {
60416
+ label: `GPU pass: ${label}`,
60417
+ renderer,
60418
+ t0: cpuTime,
60419
+ _seq: ++ this._markId,
60420
+ _generation: this._gpuGeneration,
60421
+ _queries: {
60422
+ [ TimestampQuery.RENDER ]: new Set(),
60423
+ [ TimestampQuery.COMPUTE ]: new Set(),
60424
+ },
60425
+ };
60426
+ passSpan._queries[ type ].add( uid );
60427
+ state.pendingSpans.push( passSpan );
60428
+ // Only auto-flush when no beginGpu parent is open. Nested parents flush on endGpu
60429
+ // so pass slices share one origin and nest inside the parent envelope.
60430
+ if ( state.activeSpans.length === 0 ) {
60431
+
60432
+ this._scheduleGpuFlush( state );
60433
+
60434
+ }
60435
+
60436
+ }
60437
+
60438
+ }
60439
+
60440
+ /**
60441
+ * @param {import('./ProfilerService.js').GpuRendererState} state
60442
+ */
60443
+ _scheduleGpuFlush( state ) {
60444
+
60445
+ if ( state.flushScheduled ) return;
60446
+ state.flushScheduled = true;
60447
+
60448
+ const callback = () => {
60449
+
60450
+ state.flushScheduled = false;
60451
+ if ( this.gpuRendererStates.get( state.renderer ) === state ) {
60452
+
60453
+ this.flushGpu( state.renderer ).catch( error => {
60454
+
60455
+ warnOnce( `ProfilerService: Unable to resolve GPU timestamps (${error.message}).` );
60456
+
60457
+ } );
60458
+
60459
+ }
60460
+
60461
+ };
60462
+
60463
+ if ( typeof requestAnimationFrame === 'function' ) {
60464
+
60465
+ requestAnimationFrame( callback );
60466
+
60467
+ } else {
60468
+
60469
+ setTimeout( callback, 0 );
60470
+
60471
+ }
60472
+
60473
+ }
60474
+
60475
+ /**
60476
+ * @param {import('./ProfilerService.js').CommonGpuRendererState} state
60477
+ */
60478
+ async _flushCommonGpuState( state ) {
60479
+
60480
+ const spans = state.pendingSpans.splice( 0 );
60481
+ if ( spans.length === 0 ) return;
60482
+
60483
+ const hasRenderQueries = spans.some( span => span._queries.render.size > 0 );
60484
+ const hasComputeQueries = spans.some( span => span._queries.compute.size > 0 );
60485
+ const resolutions = [];
60486
+
60487
+ if ( hasRenderQueries ) resolutions.push( state.renderer.resolveTimestampsAsync( TimestampQuery.RENDER ) );
60488
+ if ( hasComputeQueries ) resolutions.push( state.renderer.resolveTimestampsAsync( TimestampQuery.COMPUTE ) );
60489
+ await Promise.all( resolutions );
60490
+
60491
+ if ( this._enabled === false ) return;
60492
+
60493
+ if ( state.gpuTimestampOrigin === null ) {
60494
+
60495
+ const origin = this._findEarliestGpuTimestampOrigin( state, spans );
60496
+ if ( origin !== null ) {
60497
+
60498
+ state.gpuTimestampOrigin = origin.gpuTimestampOrigin;
60499
+ state.gpuTraceOrigin = origin.gpuTraceOrigin;
60500
+
60501
+ }
60502
+
60503
+ }
60504
+
60505
+ const resolvedUids = new Set();
60506
+ /** @type {Array<{ label: string, startTime: number, durationMs: number, isPass: boolean }>} */
60507
+ const samples = [];
60508
+ /** @type {Set<string>} */
60509
+ const labelledQueryUids = new Set();
60510
+
60511
+ for ( const span of spans ) {
60512
+
60513
+ if ( span._generation !== this._gpuGeneration ) continue;
60514
+
60515
+ let duration = 0;
60516
+ let resolvedQueries = 0;
60517
+ let rangedQueries = 0;
60518
+ let gpuStart = null;
60519
+ let gpuEnd = null;
60520
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
60521
+
60522
+ for ( const uid of span._queries[ type ] ) {
60523
+
60524
+ if ( state.renderer.backend.hasTimestampQuery( uid ) ) {
60525
+
60526
+ duration += state.renderer.backend.getTimestamp( uid );
60527
+ resolvedQueries ++;
60528
+ resolvedUids.add( uid );
60529
+
60530
+ const range = state.renderer.backend.getTimestampRange( uid );
60531
+ if ( range !== null ) {
60532
+
60533
+ rangedQueries ++;
60534
+ if ( gpuStart === null || range.start < gpuStart ) gpuStart = range.start;
60535
+ if ( gpuEnd === null || range.end > gpuEnd ) gpuEnd = range.end;
60536
+
60537
+ }
60538
+
60539
+ }
60540
+
60541
+ }
60542
+
60543
+ }
60544
+
60545
+ if ( resolvedQueries > 0 ) {
60546
+
60547
+ let startTime = span.t0;
60548
+ // Prefer the device envelope whenever any query has a range. Requiring
60549
+ // *all* queries to be ranged forced a CPU-t0 + summed-duration fallback
60550
+ // that often ended before later GPU pass slices, breaking flame nesting.
60551
+ if ( rangedQueries > 0 && gpuStart !== null && gpuEnd !== null && state.gpuTimestampOrigin !== null ) {
60552
+
60553
+ duration = Number( gpuEnd - gpuStart ) / 1e6;
60554
+ startTime = state.gpuTraceOrigin + Number( gpuStart - state.gpuTimestampOrigin ) / 1e6;
60555
+
60556
+ }
60557
+
60558
+ const isPass = span.label.startsWith( 'GPU pass:' );
60559
+ if ( isPass ) {
60560
+
60561
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
60562
+
60563
+ for ( const uid of span._queries[ type ] ) labelledQueryUids.add( uid );
60564
+
60565
+ }
60566
+
60567
+ }
60568
+
60569
+ samples.push( {
60570
+ label: span.label,
60571
+ startTime,
60572
+ durationMs: duration,
60573
+ isPass,
60574
+ } );
60575
+
60576
+ }
60577
+
60578
+ }
60579
+
60580
+ // Parent beginGpu spans collect every timestamp query, including renders that
60581
+ // had no gpuProfilerLabel. Surface those as pass slices so they are not empty
60582
+ // holes under the parent envelope.
60583
+ for ( const span of spans ) {
60584
+
60585
+ if ( span._generation !== this._gpuGeneration ) continue;
60586
+ if ( span.label.startsWith( 'GPU pass:' ) ) continue;
60587
+ if ( state.gpuTimestampOrigin === null ) continue;
60588
+
60589
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
60590
+
60591
+ for ( const uid of span._queries[ type ] ) {
60592
+
60593
+ if ( labelledQueryUids.has( uid ) ) continue;
60594
+ if ( state.renderer.backend.hasTimestampQuery( uid ) === false ) continue;
60595
+
60596
+ const range = state.renderer.backend.getTimestampRange( uid );
60597
+ if ( range === null ) continue;
60598
+
60599
+ const durationMs = Number( range.end - range.start ) / 1e6;
60600
+ if ( durationMs <= 0 ) continue;
60601
+
60602
+ samples.push( {
60603
+ label: 'GPU pass: (unlabeled)',
60604
+ startTime: state.gpuTraceOrigin + Number( range.start - state.gpuTimestampOrigin ) / 1e6,
60605
+ durationMs,
60606
+ isPass: true,
60607
+ } );
60608
+ labelledQueryUids.add( uid );
60609
+
60610
+ }
60611
+
60612
+ }
60613
+
60614
+ }
60615
+
60616
+ const passSamples = samples.filter( sample => sample.isPass );
60617
+ const otherSamples = samples.filter( sample => sample.isPass === false );
60618
+ this._snapHalfOpenGpuPassTraceIntervals( passSamples );
60619
+ this._expandGpuParentTraceToContainPasses( otherSamples, passSamples );
60620
+
60621
+ for ( const sample of otherSamples ) {
60622
+
60623
+ this._commitGpuDurationSample( sample.label, sample.startTime, sample.durationMs, {
60624
+ ts: sample.traceTs,
60625
+ dur: sample.traceDur,
60626
+ } );
60627
+
60628
+ }
60629
+
60630
+ for ( const sample of passSamples ) {
60631
+
60632
+ this._commitGpuDurationSample( sample.label, sample.startTime, sample.durationMs, {
60633
+ ts: sample.traceTs,
60634
+ dur: sample.traceDur,
60635
+ } );
60636
+
60637
+ }
60638
+
60639
+ // Keep CPU times for queries still referenced by active parent spans.
60640
+ for ( const uid of resolvedUids ) {
60641
+
60642
+ if ( this._isGpuQueryReferencedByActiveSpans( state, uid ) ) continue;
60643
+ state.queryCpuTimes.delete( uid );
60644
+
60645
+ }
60646
+
60647
+ }
60648
+
60649
+ /**
60650
+ * Convert labelled GPU pass samples to non-overlapping half-open µs intervals for
60651
+ * Chrome/Speedscope traces. Stats keep the raw measured durations.
60652
+ *
60653
+ * @param {Array<{ startTime: number, durationMs: number, traceTs?: number, traceDur?: number }>} samples
60654
+ */
60655
+ _snapHalfOpenGpuPassTraceIntervals( samples ) {
60656
+
60657
+ if ( samples.length === 0 ) return;
60658
+
60659
+ for ( const sample of samples ) {
60660
+
60661
+ sample.traceTs = Math.round( this._getTraceTimestamp( sample.startTime ) );
60662
+ sample.traceDur = Math.max( 1, Math.round( sample.durationMs * 1000 ) );
60663
+
60664
+ }
60665
+
60666
+ // Longer first at the same timestamp so the substantial pass keeps its start.
60667
+ samples.sort( ( a, b ) => a.traceTs - b.traceTs || b.traceDur - a.traceDur );
60668
+
60669
+ let cursor = Number.NEGATIVE_INFINITY;
60670
+ for ( const sample of samples ) {
60671
+
60672
+ if ( sample.traceTs < cursor ) {
60673
+
60674
+ const end = Math.max( sample.traceTs + sample.traceDur, cursor + 1 );
60675
+ sample.traceTs = cursor;
60676
+ sample.traceDur = Math.max( 1, end - sample.traceTs );
60677
+
60678
+ }
60679
+
60680
+ cursor = sample.traceTs + sample.traceDur;
60681
+
60682
+ }
60683
+
60684
+ }
60685
+
60686
+ /**
60687
+ * Speedscope nests by time containment on a thread. If a snapped GPU pass
60688
+ * sticks out past its beginGpu parent (rounding / half-open adjust), the
60689
+ * parent fails containment and is pushed onto a lower lane under renderFrame.
60690
+ * Expand overlapping parents so they strictly cover their pass children.
60691
+ *
60692
+ * @param {Array<{ startTime: number, durationMs: number, traceTs?: number, traceDur?: number }>} parents
60693
+ * @param {Array<{ traceTs: number, traceDur: number }>} passes
60694
+ */
60695
+ _expandGpuParentTraceToContainPasses( parents, passes ) {
60696
+
60697
+ if ( parents.length === 0 || passes.length === 0 ) return;
60698
+
60699
+ for ( const parent of parents ) {
60700
+
60701
+ parent.traceTs = Math.round( this._getTraceTimestamp( parent.startTime ) );
60702
+ parent.traceDur = Math.max( 1, Math.round( parent.durationMs * 1000 ) );
60703
+
60704
+ const parentEnd = parent.traceTs + parent.traceDur;
60705
+ let minTs = parent.traceTs;
60706
+ let maxEnd = parentEnd;
60707
+ let touched = false;
60708
+
60709
+ for ( const pass of passes ) {
60710
+
60711
+ const passEnd = pass.traceTs + pass.traceDur;
60712
+ if ( pass.traceTs >= parentEnd || passEnd <= parent.traceTs ) continue;
60713
+
60714
+ touched = true;
60715
+ if ( pass.traceTs < minTs ) minTs = pass.traceTs;
60716
+ if ( passEnd > maxEnd ) maxEnd = passEnd;
60717
+
60718
+ }
60719
+
60720
+ if ( touched === false ) continue;
60721
+
60722
+ // Start 1µs before the first child so equal-start pairs still nest
60723
+ // (Speedscope sorts same-ts by longer-first; a child that begins
60724
+ // strictly after the parent is unambiguous).
60725
+ parent.traceTs = minTs > 0 ? minTs - 1 : minTs;
60726
+ parent.traceDur = Math.max( 1, maxEnd - parent.traceTs );
60727
+
60728
+ }
60729
+
60730
+ }
60731
+
60732
+ /**
60733
+ * @param {import('./ProfilerService.js').CommonGpuRendererState} state
60734
+ * @param {import('./ProfilerService.js').GpuSpanHandle[]} spans
60735
+ * @return {?{ gpuTimestampOrigin: bigint, gpuTraceOrigin: number }}
60736
+ */
60737
+ _findEarliestGpuTimestampOrigin( state, spans ) {
60738
+
60739
+ let earliestRange = null;
60740
+ let earliestUid = null;
60741
+ const candidates = spans.concat( state.activeSpans );
60742
+
60743
+ for ( const span of candidates ) {
60744
+
60745
+ for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
60746
+
60747
+ const queries = span._queries?.[ type ];
60748
+ if ( queries === undefined ) continue;
60749
+
60750
+ for ( const uid of queries ) {
60751
+
60752
+ const range = state.renderer.backend.getTimestampRange( uid );
60753
+ if ( range !== null && ( earliestRange === null || range.start < earliestRange.start ) ) {
60754
+
60755
+ earliestRange = range;
60756
+ earliestUid = uid;
60757
+
60758
+ }
60759
+
60760
+ }
60761
+
60762
+ }
60763
+
60764
+ }
60765
+
60766
+ if ( earliestRange === null ) return null;
60767
+
60768
+ return {
60769
+ gpuTimestampOrigin: earliestRange.start,
60770
+ gpuTraceOrigin: state.queryCpuTimes.get( earliestUid ) ?? spans[ 0 ].t0,
60771
+ };
60772
+
60773
+ }
60774
+
60775
+ /**
60776
+ * @param {import('./ProfilerService.js').CommonGpuRendererState} state
60777
+ * @param {string} uid
60778
+ * @return {boolean}
60779
+ */
60780
+ _isGpuQueryReferencedByActiveSpans( state, uid ) {
60781
+
60782
+ for ( const span of state.activeSpans ) {
60783
+
60784
+ if ( span._queries.render.has( uid ) || span._queries.compute.has( uid ) ) return true;
60785
+
60786
+ }
60787
+
60788
+ return false;
60789
+
60790
+ }
60791
+
60792
+ /**
60793
+ * @param {import('./ProfilerService.js').LegacyWebGLGpuRendererState} state
60794
+ */
60795
+ async _flushLegacyWebGLState( state ) {
60796
+
60797
+ const spans = state.pendingSpans.splice( 0 );
60798
+
60799
+ for ( const span of spans ) {
60800
+
60801
+ const duration = await this._resolveLegacyWebGLQuery( state, span._query );
60802
+ if ( duration !== null && span._generation === this._gpuGeneration && this._enabled ) {
60803
+
60804
+ this._commitGpuDurationSample( span.label, span.t0, duration );
60805
+
60806
+ }
60807
+
60808
+ }
60809
+
60810
+ }
60811
+
60812
+ /**
60813
+ * @param {import('./ProfilerService.js').LegacyWebGLGpuRendererState} state
60814
+ * @param {WebGLQuery} query
60815
+ * @return {Promise<?number>}
60816
+ */
60817
+ _resolveLegacyWebGLQuery( state, query ) {
60818
+
60819
+ return new Promise( resolve => {
60820
+
60821
+ const poll = () => {
60822
+
60823
+ if ( state.gl.isContextLost() ) {
60824
+
60825
+ state.gl.deleteQuery( query );
60826
+ resolve( null );
60827
+ return;
60828
+
60829
+ }
60830
+
60831
+ const disjoint = state.gl.getParameter( state.extension.GPU_DISJOINT_EXT );
60832
+ const available = state.gl.getQueryParameter( query, state.gl.QUERY_RESULT_AVAILABLE );
60833
+
60834
+ if ( available === false ) {
60835
+
60836
+ setTimeout( poll, 1 );
60837
+ return;
60838
+
60839
+ }
60840
+
60841
+ const result = disjoint
60842
+ ? null
60843
+ : Number( state.gl.getQueryParameter( query, state.gl.QUERY_RESULT ) ) / 1e6;
60844
+ state.gl.deleteQuery( query );
60845
+ resolve( result );
60846
+
60847
+ };
60848
+
60849
+ poll();
60850
+
60851
+ } );
60852
+
60853
+ }
60854
+
60148
60855
  /**
60149
60856
  * @param {string} label
60150
60857
  */
@@ -60302,6 +61009,46 @@ class ProfilerServiceClass {
60302
61009
 
60303
61010
  }
60304
61011
 
61012
+ /**
61013
+ * @param {string} label
61014
+ * @param {number} startTime
61015
+ * @param {number} durationMs
61016
+ * @param {{ ts: number, dur: number }} [traceOverride]
61017
+ */
61018
+ _commitGpuDurationSample( label, startTime, durationMs, traceOverride = null ) {
61019
+
61020
+ let buffer = this.gpuBuffers.get( label );
61021
+ if ( buffer === undefined ) {
61022
+
61023
+ buffer = new Float64Array( RING_SIZE );
61024
+ this.gpuBuffers.set( label, buffer );
61025
+ this.gpuCursors.set( label, 0 );
61026
+ this.gpuCounts.set( label, 0 );
61027
+
61028
+ }
61029
+
61030
+ const cursor = this.gpuCursors.get( label );
61031
+ buffer[ cursor ] = durationMs;
61032
+ this.gpuCursors.set( label, ( cursor + 1 ) % RING_SIZE );
61033
+ this.gpuCounts.set( label, Math.min( this.gpuCounts.get( label ) + 1, RING_SIZE ) );
61034
+ this.gpuInvocations.set( label, ( this.gpuInvocations.get( label ) ?? 0 ) + 1 );
61035
+
61036
+ if ( this._profile === 'full' ) {
61037
+
61038
+ this.traceEvents.push( {
61039
+ name: label,
61040
+ ph: 'X',
61041
+ ts: traceOverride?.ts ?? Math.round( this._getTraceTimestamp( startTime ) ),
61042
+ dur: traceOverride?.dur ?? Math.max( 1, Math.round( durationMs * 1000 ) ),
61043
+ pid: 1,
61044
+ tid: TRACE_TID_GPU,
61045
+ cat: 'gnsx-gpu',
61046
+ } );
61047
+
61048
+ }
61049
+
61050
+ }
61051
+
60305
61052
  /**
60306
61053
  * @param {string} label
60307
61054
  * @return {number[]}
@@ -60317,6 +61064,21 @@ class ProfilerServiceClass {
60317
61064
 
60318
61065
  }
60319
61066
 
61067
+ /**
61068
+ * @param {string} label
61069
+ * @return {number[]}
61070
+ */
61071
+ getValidGpuSamples( label ) {
61072
+
61073
+ const buffer = this.gpuBuffers.get( label );
61074
+ const count = this.gpuCounts.get( label ) ?? 0;
61075
+ if ( buffer === undefined || count === 0 ) return [];
61076
+ return count < RING_SIZE
61077
+ ? Array.from( buffer.subarray( 0, count ) )
61078
+ : Array.from( buffer );
61079
+
61080
+ }
61081
+
60320
61082
  /**
60321
61083
  * @param {string} label
60322
61084
  * @return {import('./ProfilerService.js').ProfilerStats|null}
@@ -60364,6 +61126,31 @@ class ProfilerServiceClass {
60364
61126
 
60365
61127
  }
60366
61128
 
61129
+ /**
61130
+ * @param {string} label
61131
+ * @return {import('./ProfilerService.js').GpuProfilerStats|null}
61132
+ */
61133
+ getGpuStats( label ) {
61134
+
61135
+ const samples = this.getValidGpuSamples( label );
61136
+ if ( samples.length === 0 ) return null;
61137
+
61138
+ const sorted = [ ...samples ].sort( ( a, b ) => a - b );
61139
+ const avg = sorted.reduce( ( a, b ) => a + b, 0 ) / sorted.length;
61140
+
61141
+ return {
61142
+ label,
61143
+ samples: samples.length,
61144
+ avg,
61145
+ min: sorted[ 0 ],
61146
+ max: sorted[ sorted.length - 1 ],
61147
+ p95: sorted[ Math.floor( sorted.length * 0.95 ) ],
61148
+ frameBudget: ( avg / FRAME_BUDGET_MS ) * 100,
61149
+ totalInvocations: this.gpuInvocations.get( label ) ?? 0,
61150
+ };
61151
+
61152
+ }
61153
+
60367
61154
  /**
60368
61155
  * @return {import('./ProfilerService.js').ProfilerStats[]}
60369
61156
  */
@@ -60375,10 +61162,22 @@ class ProfilerServiceClass {
60375
61162
 
60376
61163
  }
60377
61164
 
61165
+ /**
61166
+ * @return {import('./ProfilerService.js').GpuProfilerStats[]}
61167
+ */
61168
+ getAllGpuStats() {
61169
+
61170
+ return [ ...this.gpuBuffers.keys() ]
61171
+ .map( label => this.getGpuStats( label ) )
61172
+ .filter( stats => stats !== null );
61173
+
61174
+ }
61175
+
60378
61176
  report() {
60379
61177
 
60380
61178
  const stats = this.getAllStats();
60381
- if ( stats.length === 0 ) {
61179
+ const gpuStats = this.getAllGpuStats();
61180
+ if ( stats.length === 0 && gpuStats.length === 0 ) {
60382
61181
 
60383
61182
  console.log( '[ProfilerService] No data. Enable profiling first and wait a few frames.' );
60384
61183
  return;
@@ -60398,6 +61197,24 @@ class ProfilerServiceClass {
60398
61197
  } ) )
60399
61198
  );
60400
61199
 
61200
+ if ( gpuStats.length > 0 ) {
61201
+
61202
+ gpuStats.sort( ( a, b ) => b.avg - a.avg );
61203
+ console.log( '[ProfilerService] GPU timings' );
61204
+ console.table(
61205
+ gpuStats.map( stats => ( {
61206
+ label: stats.label,
61207
+ 'avg ms': stats.avg.toFixed( 3 ),
61208
+ 'min ms': stats.min.toFixed( 3 ),
61209
+ 'max ms': stats.max.toFixed( 3 ),
61210
+ 'p95 ms': stats.p95.toFixed( 3 ),
61211
+ 'budget %': stats.frameBudget.toFixed( 1 ) + '%',
61212
+ samples: stats.samples,
61213
+ } ) )
61214
+ );
61215
+
61216
+ }
61217
+
60401
61218
  }
60402
61219
 
60403
61220
  /**
@@ -60414,6 +61231,7 @@ class ProfilerServiceClass {
60414
61231
 
60415
61232
  } );
60416
61233
  const hasAsync = slices.some( e => e.tid === TRACE_TID_ASYNC );
61234
+ const hasGpu = slices.some( e => e.tid === TRACE_TID_GPU );
60417
61235
  /** @type {import('./ProfilerService.js').ChromeTraceMetadataEvent[]} */
60418
61236
  const prefix = [
60419
61237
  {
@@ -60449,6 +61267,20 @@ class ProfilerServiceClass {
60449
61267
 
60450
61268
  }
60451
61269
 
61270
+ if ( hasGpu ) {
61271
+
61272
+ prefix.push( {
61273
+ cat: '__metadata',
61274
+ name: 'thread_name',
61275
+ ph: 'M',
61276
+ pid: 1,
61277
+ tid: TRACE_TID_GPU,
61278
+ ts: 0,
61279
+ args: { name: 'GPU (device timestamps)' },
61280
+ } );
61281
+
61282
+ }
61283
+
60452
61284
  return {
60453
61285
  displayTimeUnit: 'ms',
60454
61286
  traceEvents: [ ...prefix, ...slices ],
@@ -60513,8 +61345,24 @@ class ProfilerServiceClass {
60513
61345
 
60514
61346
  }
60515
61347
 
61348
+ /**
61349
+ * @return {import('./ProfilerService.js').GpuProfilerStats[]}
61350
+ */
61351
+ exportGpuJSON() {
61352
+
61353
+ return this.getAllGpuStats();
61354
+
61355
+ }
61356
+
60516
61357
  _clearState() {
60517
61358
 
61359
+ this._gpuGeneration ++;
61360
+ for ( const state of this.gpuRendererStates.values() ) {
61361
+
61362
+ this._clearGpuRendererState( state );
61363
+
61364
+ }
61365
+
60518
61366
  this.buffers.clear();
60519
61367
  this.cursors.clear();
60520
61368
  this.counts.clear();
@@ -60527,6 +61375,79 @@ class ProfilerServiceClass {
60527
61375
  this.selfCounts.clear();
60528
61376
  this.callStack.length = 0;
60529
61377
  this.invocations.clear();
61378
+ this.gpuBuffers.clear();
61379
+ this.gpuCursors.clear();
61380
+ this.gpuCounts.clear();
61381
+ this.gpuInvocations.clear();
61382
+
61383
+ }
61384
+
61385
+ /**
61386
+ * @param {import('./ProfilerService.js').GpuRendererState} state
61387
+ */
61388
+ _clearGpuRendererState( state ) {
61389
+
61390
+ if ( state.kind === 'common' ) {
61391
+
61392
+ state.activeSpans.length = 0;
61393
+ state.pendingSpans.length = 0;
61394
+ state.queryCpuTimes.clear();
61395
+ state.gpuTimestampOrigin = null;
61396
+ state.gpuTraceOrigin = null;
61397
+ return;
61398
+
61399
+ }
61400
+
61401
+ if ( state.activeSpan !== null ) {
61402
+
61403
+ try {
61404
+
61405
+ state.gl.endQuery( state.extension.TIME_ELAPSED_EXT );
61406
+
61407
+ } catch {
61408
+
61409
+ // The context may have been lost while the query was active.
61410
+
61411
+ }
61412
+
61413
+ state.gl.deleteQuery( state.activeSpan._query );
61414
+ state.activeSpan = null;
61415
+
61416
+ }
61417
+
61418
+ if ( state.flushPromise === null ) {
61419
+
61420
+ for ( const span of state.pendingSpans ) {
61421
+
61422
+ state.gl.deleteQuery( span._query );
61423
+
61424
+ }
61425
+
61426
+ }
61427
+
61428
+ state.pendingSpans.length = 0;
61429
+
61430
+ }
61431
+
61432
+ _detachGpuRenderers() {
61433
+
61434
+ for ( const state of this.gpuRendererStates.values() ) {
61435
+
61436
+ if ( state.kind === 'common' && state.available ) {
61437
+
61438
+ state.renderer.backend.removeTimestampQueryListener( state.listener );
61439
+
61440
+ }
61441
+
61442
+ if ( state.kind === 'common' ) {
61443
+
61444
+ state.renderer.backend.trackTimestamp = state.previousTrackTimestamp;
61445
+
61446
+ }
61447
+
61448
+ }
61449
+
61450
+ this.gpuRendererStates.clear();
60530
61451
 
60531
61452
  }
60532
61453
 
@@ -60556,6 +61477,18 @@ class ProfilerServiceClass {
60556
61477
  * @property {number|undefined} selfFrameBudget Exclusive time as percentage of a 60 fps frame budget.
60557
61478
  */
60558
61479
 
61480
+ /**
61481
+ * @typedef {Object} GpuProfilerStats
61482
+ * @property {string} label
61483
+ * @property {number} samples
61484
+ * @property {number} avg
61485
+ * @property {number} min
61486
+ * @property {number} max
61487
+ * @property {number} p95
61488
+ * @property {number} frameBudget Percentage of a 60 fps frame budget (16.67 ms)
61489
+ * @property {number} totalInvocations
61490
+ */
61491
+
60559
61492
  /**
60560
61493
  * @typedef {Object} SpanHandle
60561
61494
  * @property {string} label
@@ -60564,6 +61497,51 @@ class ProfilerServiceClass {
60564
61497
  * @property {string} _startMark
60565
61498
  */
60566
61499
 
61500
+ /**
61501
+ * @typedef {Object} GpuSpanHandle
61502
+ * @property {string} label
61503
+ * @property {?Object} renderer
61504
+ * @property {number} t0
61505
+ * @property {number} _seq
61506
+ * @property {number} _generation
61507
+ * @property {{render: Set<string>, compute: Set<string>}} [_queries]
61508
+ * @property {WebGLQuery} [_query]
61509
+ * @property {number} [_endTime]
61510
+ */
61511
+
61512
+ /**
61513
+ * @typedef {Object} CommonGpuRendererState
61514
+ * @property {'common'} kind
61515
+ * @property {boolean} available
61516
+ * @property {Object} renderer
61517
+ * @property {function('render'|'compute', string, ?string): void} listener
61518
+ * @property {boolean} previousTrackTimestamp
61519
+ * @property {GpuSpanHandle[]} activeSpans
61520
+ * @property {GpuSpanHandle[]} pendingSpans
61521
+ * @property {Map<string, number>} queryCpuTimes
61522
+ * @property {?bigint} gpuTimestampOrigin
61523
+ * @property {?number} gpuTraceOrigin
61524
+ * @property {?Promise<void>} flushPromise
61525
+ * @property {boolean} flushScheduled
61526
+ */
61527
+
61528
+ /**
61529
+ * @typedef {Object} LegacyWebGLGpuRendererState
61530
+ * @property {'legacy-webgl'} kind
61531
+ * @property {boolean} available
61532
+ * @property {Object} renderer
61533
+ * @property {WebGL2RenderingContext} gl
61534
+ * @property {?EXT_disjoint_timer_query_webgl2} extension
61535
+ * @property {?GpuSpanHandle} activeSpan
61536
+ * @property {GpuSpanHandle[]} pendingSpans
61537
+ * @property {?Promise<void>} flushPromise
61538
+ * @property {boolean} flushScheduled
61539
+ */
61540
+
61541
+ /**
61542
+ * @typedef {CommonGpuRendererState|LegacyWebGLGpuRendererState} GpuRendererState
61543
+ */
61544
+
60567
61545
  /**
60568
61546
  * @typedef {Object} EndSpanOptions
60569
61547
  * @property {boolean} [asyncTimeline] When true, trace slice uses `tid=2` (virtual async row).
@@ -60577,7 +61555,7 @@ class ProfilerServiceClass {
60577
61555
  * @property {number} dur
60578
61556
  * @property {1} pid
60579
61557
  * @property {number} tid
60580
- * @property {'gnsx'} cat
61558
+ * @property {'gnsx'|'gnsx-gpu'} cat
60581
61559
  */
60582
61560
 
60583
61561
  /**