@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.
- package/build/three.cjs +980 -2
- package/build/three.core.js +980 -2
- package/build/three.core.min.js +1 -1
- package/build/three.webgpu.js +215 -68
- package/build/three.webgpu.min.js +1 -1
- package/build/three.webgpu.nodes.js +215 -68
- package/build/three.webgpu.nodes.min.js +1 -1
- package/package.json +1 -1
- package/src/profiler/ProfilerService.js +982 -2
- package/src/renderers/common/Backend.js +60 -0
- package/src/renderers/common/RenderContext.js +10 -0
- package/src/renderers/common/Renderer.js +20 -1
- package/src/renderers/common/TimestampQueryPool.js +23 -0
- package/src/renderers/webgl-fallback/WebGLBackend.js +14 -3
- package/src/renderers/webgl-fallback/utils/WebGLTimestampQueryPool.js +8 -4
- package/src/renderers/webgpu/WebGPUBackend.js +37 -8
- package/src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js +44 -5
|
@@ -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,687 @@ 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
|
+
// Queue labelled passes with the active beginGpu span(s). Flushing
|
|
411
|
+
// immediately would commit them before their parent envelope exists and
|
|
412
|
+
// can anchor gpuTraceOrigin mid-frame, which breaks Speedscope nesting.
|
|
413
|
+
const passSpan = {
|
|
414
|
+
label: `GPU pass: ${label}`,
|
|
415
|
+
renderer,
|
|
416
|
+
t0: cpuTime,
|
|
417
|
+
_seq: ++ this._markId,
|
|
418
|
+
_generation: this._gpuGeneration,
|
|
419
|
+
_queries: {
|
|
420
|
+
[ TimestampQuery.RENDER ]: new Set(),
|
|
421
|
+
[ TimestampQuery.COMPUTE ]: new Set(),
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
passSpan._queries[ type ].add( uid );
|
|
425
|
+
state.pendingSpans.push( passSpan );
|
|
426
|
+
// Only auto-flush when no beginGpu parent is open. Nested parents flush on endGpu
|
|
427
|
+
// so pass slices share one origin and nest inside the parent envelope.
|
|
428
|
+
if ( state.activeSpans.length === 0 ) {
|
|
429
|
+
|
|
430
|
+
this._scheduleGpuFlush( state );
|
|
431
|
+
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* @param {import('./ProfilerService.js').GpuRendererState} state
|
|
440
|
+
*/
|
|
441
|
+
_scheduleGpuFlush( state ) {
|
|
442
|
+
|
|
443
|
+
if ( state.flushScheduled ) return;
|
|
444
|
+
state.flushScheduled = true;
|
|
445
|
+
|
|
446
|
+
const callback = () => {
|
|
447
|
+
|
|
448
|
+
state.flushScheduled = false;
|
|
449
|
+
if ( this.gpuRendererStates.get( state.renderer ) === state ) {
|
|
450
|
+
|
|
451
|
+
this.flushGpu( state.renderer ).catch( error => {
|
|
452
|
+
|
|
453
|
+
warnOnce( `ProfilerService: Unable to resolve GPU timestamps (${error.message}).` );
|
|
454
|
+
|
|
455
|
+
} );
|
|
456
|
+
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
if ( typeof requestAnimationFrame === 'function' ) {
|
|
462
|
+
|
|
463
|
+
requestAnimationFrame( callback );
|
|
464
|
+
|
|
465
|
+
} else {
|
|
466
|
+
|
|
467
|
+
setTimeout( callback, 0 );
|
|
468
|
+
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* @param {import('./ProfilerService.js').CommonGpuRendererState} state
|
|
475
|
+
*/
|
|
476
|
+
async _flushCommonGpuState( state ) {
|
|
477
|
+
|
|
478
|
+
const spans = state.pendingSpans.splice( 0 );
|
|
479
|
+
if ( spans.length === 0 ) return;
|
|
480
|
+
|
|
481
|
+
const hasRenderQueries = spans.some( span => span._queries.render.size > 0 );
|
|
482
|
+
const hasComputeQueries = spans.some( span => span._queries.compute.size > 0 );
|
|
483
|
+
const resolutions = [];
|
|
484
|
+
|
|
485
|
+
if ( hasRenderQueries ) resolutions.push( state.renderer.resolveTimestampsAsync( TimestampQuery.RENDER ) );
|
|
486
|
+
if ( hasComputeQueries ) resolutions.push( state.renderer.resolveTimestampsAsync( TimestampQuery.COMPUTE ) );
|
|
487
|
+
await Promise.all( resolutions );
|
|
488
|
+
|
|
489
|
+
if ( this._enabled === false ) return;
|
|
490
|
+
|
|
491
|
+
if ( state.gpuTimestampOrigin === null ) {
|
|
492
|
+
|
|
493
|
+
const origin = this._findEarliestGpuTimestampOrigin( state, spans );
|
|
494
|
+
if ( origin !== null ) {
|
|
495
|
+
|
|
496
|
+
state.gpuTimestampOrigin = origin.gpuTimestampOrigin;
|
|
497
|
+
state.gpuTraceOrigin = origin.gpuTraceOrigin;
|
|
498
|
+
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const resolvedUids = new Set();
|
|
504
|
+
/** @type {Array<{ label: string, startTime: number, durationMs: number, isPass: boolean }>} */
|
|
505
|
+
const samples = [];
|
|
506
|
+
/** @type {Set<string>} */
|
|
507
|
+
const labelledQueryUids = new Set();
|
|
508
|
+
|
|
509
|
+
for ( const span of spans ) {
|
|
510
|
+
|
|
511
|
+
if ( span._generation !== this._gpuGeneration ) continue;
|
|
512
|
+
|
|
513
|
+
let duration = 0;
|
|
514
|
+
let resolvedQueries = 0;
|
|
515
|
+
let rangedQueries = 0;
|
|
516
|
+
let gpuStart = null;
|
|
517
|
+
let gpuEnd = null;
|
|
518
|
+
for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
|
|
519
|
+
|
|
520
|
+
for ( const uid of span._queries[ type ] ) {
|
|
521
|
+
|
|
522
|
+
if ( state.renderer.backend.hasTimestampQuery( uid ) ) {
|
|
523
|
+
|
|
524
|
+
duration += state.renderer.backend.getTimestamp( uid );
|
|
525
|
+
resolvedQueries ++;
|
|
526
|
+
resolvedUids.add( uid );
|
|
527
|
+
|
|
528
|
+
const range = state.renderer.backend.getTimestampRange( uid );
|
|
529
|
+
if ( range !== null ) {
|
|
530
|
+
|
|
531
|
+
rangedQueries ++;
|
|
532
|
+
if ( gpuStart === null || range.start < gpuStart ) gpuStart = range.start;
|
|
533
|
+
if ( gpuEnd === null || range.end > gpuEnd ) gpuEnd = range.end;
|
|
534
|
+
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if ( resolvedQueries > 0 ) {
|
|
544
|
+
|
|
545
|
+
let startTime = span.t0;
|
|
546
|
+
// Prefer the device envelope whenever any query has a range. Requiring
|
|
547
|
+
// *all* queries to be ranged forced a CPU-t0 + summed-duration fallback
|
|
548
|
+
// that often ended before later GPU pass slices, breaking flame nesting.
|
|
549
|
+
if ( rangedQueries > 0 && gpuStart !== null && gpuEnd !== null && state.gpuTimestampOrigin !== null ) {
|
|
550
|
+
|
|
551
|
+
duration = Number( gpuEnd - gpuStart ) / 1e6;
|
|
552
|
+
startTime = state.gpuTraceOrigin + Number( gpuStart - state.gpuTimestampOrigin ) / 1e6;
|
|
553
|
+
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const isPass = span.label.startsWith( 'GPU pass:' );
|
|
557
|
+
if ( isPass ) {
|
|
558
|
+
|
|
559
|
+
for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
|
|
560
|
+
|
|
561
|
+
for ( const uid of span._queries[ type ] ) labelledQueryUids.add( uid );
|
|
562
|
+
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
samples.push( {
|
|
568
|
+
label: span.label,
|
|
569
|
+
startTime,
|
|
570
|
+
durationMs: duration,
|
|
571
|
+
isPass,
|
|
572
|
+
} );
|
|
573
|
+
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Parent beginGpu spans collect every timestamp query, including renders that
|
|
579
|
+
// had no gpuProfilerLabel. Surface those as pass slices so they are not empty
|
|
580
|
+
// holes under the parent envelope.
|
|
581
|
+
for ( const span of spans ) {
|
|
582
|
+
|
|
583
|
+
if ( span._generation !== this._gpuGeneration ) continue;
|
|
584
|
+
if ( span.label.startsWith( 'GPU pass:' ) ) continue;
|
|
585
|
+
if ( state.gpuTimestampOrigin === null ) continue;
|
|
586
|
+
|
|
587
|
+
for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
|
|
588
|
+
|
|
589
|
+
for ( const uid of span._queries[ type ] ) {
|
|
590
|
+
|
|
591
|
+
if ( labelledQueryUids.has( uid ) ) continue;
|
|
592
|
+
if ( state.renderer.backend.hasTimestampQuery( uid ) === false ) continue;
|
|
593
|
+
|
|
594
|
+
const range = state.renderer.backend.getTimestampRange( uid );
|
|
595
|
+
if ( range === null ) continue;
|
|
596
|
+
|
|
597
|
+
const durationMs = Number( range.end - range.start ) / 1e6;
|
|
598
|
+
if ( durationMs <= 0 ) continue;
|
|
599
|
+
|
|
600
|
+
samples.push( {
|
|
601
|
+
label: 'GPU pass: (unlabeled)',
|
|
602
|
+
startTime: state.gpuTraceOrigin + Number( range.start - state.gpuTimestampOrigin ) / 1e6,
|
|
603
|
+
durationMs,
|
|
604
|
+
isPass: true,
|
|
605
|
+
} );
|
|
606
|
+
labelledQueryUids.add( uid );
|
|
607
|
+
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const passSamples = samples.filter( sample => sample.isPass );
|
|
615
|
+
const otherSamples = samples.filter( sample => sample.isPass === false );
|
|
616
|
+
this._snapHalfOpenGpuPassTraceIntervals( passSamples );
|
|
617
|
+
this._expandGpuParentTraceToContainPasses( otherSamples, passSamples );
|
|
618
|
+
|
|
619
|
+
for ( const sample of otherSamples ) {
|
|
620
|
+
|
|
621
|
+
this._commitGpuDurationSample( sample.label, sample.startTime, sample.durationMs, {
|
|
622
|
+
ts: sample.traceTs,
|
|
623
|
+
dur: sample.traceDur,
|
|
624
|
+
} );
|
|
625
|
+
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
for ( const sample of passSamples ) {
|
|
629
|
+
|
|
630
|
+
this._commitGpuDurationSample( sample.label, sample.startTime, sample.durationMs, {
|
|
631
|
+
ts: sample.traceTs,
|
|
632
|
+
dur: sample.traceDur,
|
|
633
|
+
} );
|
|
634
|
+
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Keep CPU times for queries still referenced by active parent spans.
|
|
638
|
+
for ( const uid of resolvedUids ) {
|
|
639
|
+
|
|
640
|
+
if ( this._isGpuQueryReferencedByActiveSpans( state, uid ) ) continue;
|
|
641
|
+
state.queryCpuTimes.delete( uid );
|
|
642
|
+
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Convert labelled GPU pass samples to non-overlapping half-open µs intervals for
|
|
649
|
+
* Chrome/Speedscope traces. Stats keep the raw measured durations.
|
|
650
|
+
*
|
|
651
|
+
* @param {Array<{ startTime: number, durationMs: number, traceTs?: number, traceDur?: number }>} samples
|
|
652
|
+
*/
|
|
653
|
+
_snapHalfOpenGpuPassTraceIntervals( samples ) {
|
|
654
|
+
|
|
655
|
+
if ( samples.length === 0 ) return;
|
|
656
|
+
|
|
657
|
+
for ( const sample of samples ) {
|
|
658
|
+
|
|
659
|
+
sample.traceTs = Math.round( this._getTraceTimestamp( sample.startTime ) );
|
|
660
|
+
sample.traceDur = Math.max( 1, Math.round( sample.durationMs * 1000 ) );
|
|
661
|
+
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Longer first at the same timestamp so the substantial pass keeps its start.
|
|
665
|
+
samples.sort( ( a, b ) => a.traceTs - b.traceTs || b.traceDur - a.traceDur );
|
|
666
|
+
|
|
667
|
+
let cursor = Number.NEGATIVE_INFINITY;
|
|
668
|
+
for ( const sample of samples ) {
|
|
669
|
+
|
|
670
|
+
if ( sample.traceTs < cursor ) {
|
|
671
|
+
|
|
672
|
+
const end = Math.max( sample.traceTs + sample.traceDur, cursor + 1 );
|
|
673
|
+
sample.traceTs = cursor;
|
|
674
|
+
sample.traceDur = Math.max( 1, end - sample.traceTs );
|
|
675
|
+
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
cursor = sample.traceTs + sample.traceDur;
|
|
679
|
+
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Speedscope nests by time containment on a thread. If a snapped GPU pass
|
|
686
|
+
* sticks out past its beginGpu parent (rounding / half-open adjust), the
|
|
687
|
+
* parent fails containment and is pushed onto a lower lane under renderFrame.
|
|
688
|
+
* Expand overlapping parents so they strictly cover their pass children.
|
|
689
|
+
*
|
|
690
|
+
* @param {Array<{ startTime: number, durationMs: number, traceTs?: number, traceDur?: number }>} parents
|
|
691
|
+
* @param {Array<{ traceTs: number, traceDur: number }>} passes
|
|
692
|
+
*/
|
|
693
|
+
_expandGpuParentTraceToContainPasses( parents, passes ) {
|
|
694
|
+
|
|
695
|
+
if ( parents.length === 0 || passes.length === 0 ) return;
|
|
696
|
+
|
|
697
|
+
for ( const parent of parents ) {
|
|
698
|
+
|
|
699
|
+
parent.traceTs = Math.round( this._getTraceTimestamp( parent.startTime ) );
|
|
700
|
+
parent.traceDur = Math.max( 1, Math.round( parent.durationMs * 1000 ) );
|
|
701
|
+
|
|
702
|
+
const parentEnd = parent.traceTs + parent.traceDur;
|
|
703
|
+
let minTs = parent.traceTs;
|
|
704
|
+
let maxEnd = parentEnd;
|
|
705
|
+
let touched = false;
|
|
706
|
+
|
|
707
|
+
for ( const pass of passes ) {
|
|
708
|
+
|
|
709
|
+
const passEnd = pass.traceTs + pass.traceDur;
|
|
710
|
+
if ( pass.traceTs >= parentEnd || passEnd <= parent.traceTs ) continue;
|
|
711
|
+
|
|
712
|
+
touched = true;
|
|
713
|
+
if ( pass.traceTs < minTs ) minTs = pass.traceTs;
|
|
714
|
+
if ( passEnd > maxEnd ) maxEnd = passEnd;
|
|
715
|
+
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if ( touched === false ) continue;
|
|
719
|
+
|
|
720
|
+
// Start 1µs before the first child so equal-start pairs still nest
|
|
721
|
+
// (Speedscope sorts same-ts by longer-first; a child that begins
|
|
722
|
+
// strictly after the parent is unambiguous).
|
|
723
|
+
parent.traceTs = minTs > 0 ? minTs - 1 : minTs;
|
|
724
|
+
parent.traceDur = Math.max( 1, maxEnd - parent.traceTs );
|
|
725
|
+
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* @param {import('./ProfilerService.js').CommonGpuRendererState} state
|
|
732
|
+
* @param {import('./ProfilerService.js').GpuSpanHandle[]} spans
|
|
733
|
+
* @return {?{ gpuTimestampOrigin: bigint, gpuTraceOrigin: number }}
|
|
734
|
+
*/
|
|
735
|
+
_findEarliestGpuTimestampOrigin( state, spans ) {
|
|
736
|
+
|
|
737
|
+
let earliestRange = null;
|
|
738
|
+
let earliestUid = null;
|
|
739
|
+
const candidates = spans.concat( state.activeSpans );
|
|
740
|
+
|
|
741
|
+
for ( const span of candidates ) {
|
|
742
|
+
|
|
743
|
+
for ( const type of [ TimestampQuery.RENDER, TimestampQuery.COMPUTE ] ) {
|
|
744
|
+
|
|
745
|
+
const queries = span._queries?.[ type ];
|
|
746
|
+
if ( queries === undefined ) continue;
|
|
747
|
+
|
|
748
|
+
for ( const uid of queries ) {
|
|
749
|
+
|
|
750
|
+
const range = state.renderer.backend.getTimestampRange( uid );
|
|
751
|
+
if ( range !== null && ( earliestRange === null || range.start < earliestRange.start ) ) {
|
|
752
|
+
|
|
753
|
+
earliestRange = range;
|
|
754
|
+
earliestUid = uid;
|
|
755
|
+
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if ( earliestRange === null ) return null;
|
|
765
|
+
|
|
766
|
+
return {
|
|
767
|
+
gpuTimestampOrigin: earliestRange.start,
|
|
768
|
+
gpuTraceOrigin: state.queryCpuTimes.get( earliestUid ) ?? spans[ 0 ].t0,
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* @param {import('./ProfilerService.js').CommonGpuRendererState} state
|
|
775
|
+
* @param {string} uid
|
|
776
|
+
* @return {boolean}
|
|
777
|
+
*/
|
|
778
|
+
_isGpuQueryReferencedByActiveSpans( state, uid ) {
|
|
779
|
+
|
|
780
|
+
for ( const span of state.activeSpans ) {
|
|
781
|
+
|
|
782
|
+
if ( span._queries.render.has( uid ) || span._queries.compute.has( uid ) ) return true;
|
|
783
|
+
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return false;
|
|
787
|
+
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* @param {import('./ProfilerService.js').LegacyWebGLGpuRendererState} state
|
|
792
|
+
*/
|
|
793
|
+
async _flushLegacyWebGLState( state ) {
|
|
794
|
+
|
|
795
|
+
const spans = state.pendingSpans.splice( 0 );
|
|
796
|
+
|
|
797
|
+
for ( const span of spans ) {
|
|
798
|
+
|
|
799
|
+
const duration = await this._resolveLegacyWebGLQuery( state, span._query );
|
|
800
|
+
if ( duration !== null && span._generation === this._gpuGeneration && this._enabled ) {
|
|
801
|
+
|
|
802
|
+
this._commitGpuDurationSample( span.label, span.t0, duration );
|
|
803
|
+
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* @param {import('./ProfilerService.js').LegacyWebGLGpuRendererState} state
|
|
812
|
+
* @param {WebGLQuery} query
|
|
813
|
+
* @return {Promise<?number>}
|
|
814
|
+
*/
|
|
815
|
+
_resolveLegacyWebGLQuery( state, query ) {
|
|
816
|
+
|
|
817
|
+
return new Promise( resolve => {
|
|
818
|
+
|
|
819
|
+
const poll = () => {
|
|
820
|
+
|
|
821
|
+
if ( state.gl.isContextLost() ) {
|
|
822
|
+
|
|
823
|
+
state.gl.deleteQuery( query );
|
|
824
|
+
resolve( null );
|
|
825
|
+
return;
|
|
826
|
+
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const disjoint = state.gl.getParameter( state.extension.GPU_DISJOINT_EXT );
|
|
830
|
+
const available = state.gl.getQueryParameter( query, state.gl.QUERY_RESULT_AVAILABLE );
|
|
831
|
+
|
|
832
|
+
if ( available === false ) {
|
|
833
|
+
|
|
834
|
+
setTimeout( poll, 1 );
|
|
835
|
+
return;
|
|
836
|
+
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const result = disjoint
|
|
840
|
+
? null
|
|
841
|
+
: Number( state.gl.getQueryParameter( query, state.gl.QUERY_RESULT ) ) / 1e6;
|
|
842
|
+
state.gl.deleteQuery( query );
|
|
843
|
+
resolve( result );
|
|
844
|
+
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
poll();
|
|
848
|
+
|
|
849
|
+
} );
|
|
850
|
+
|
|
851
|
+
}
|
|
852
|
+
|
|
144
853
|
/**
|
|
145
854
|
* @param {string} label
|
|
146
855
|
*/
|
|
@@ -298,6 +1007,46 @@ class ProfilerServiceClass {
|
|
|
298
1007
|
|
|
299
1008
|
}
|
|
300
1009
|
|
|
1010
|
+
/**
|
|
1011
|
+
* @param {string} label
|
|
1012
|
+
* @param {number} startTime
|
|
1013
|
+
* @param {number} durationMs
|
|
1014
|
+
* @param {{ ts: number, dur: number }} [traceOverride]
|
|
1015
|
+
*/
|
|
1016
|
+
_commitGpuDurationSample( label, startTime, durationMs, traceOverride = null ) {
|
|
1017
|
+
|
|
1018
|
+
let buffer = this.gpuBuffers.get( label );
|
|
1019
|
+
if ( buffer === undefined ) {
|
|
1020
|
+
|
|
1021
|
+
buffer = new Float64Array( RING_SIZE );
|
|
1022
|
+
this.gpuBuffers.set( label, buffer );
|
|
1023
|
+
this.gpuCursors.set( label, 0 );
|
|
1024
|
+
this.gpuCounts.set( label, 0 );
|
|
1025
|
+
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const cursor = this.gpuCursors.get( label );
|
|
1029
|
+
buffer[ cursor ] = durationMs;
|
|
1030
|
+
this.gpuCursors.set( label, ( cursor + 1 ) % RING_SIZE );
|
|
1031
|
+
this.gpuCounts.set( label, Math.min( this.gpuCounts.get( label ) + 1, RING_SIZE ) );
|
|
1032
|
+
this.gpuInvocations.set( label, ( this.gpuInvocations.get( label ) ?? 0 ) + 1 );
|
|
1033
|
+
|
|
1034
|
+
if ( this._profile === 'full' ) {
|
|
1035
|
+
|
|
1036
|
+
this.traceEvents.push( {
|
|
1037
|
+
name: label,
|
|
1038
|
+
ph: 'X',
|
|
1039
|
+
ts: traceOverride?.ts ?? Math.round( this._getTraceTimestamp( startTime ) ),
|
|
1040
|
+
dur: traceOverride?.dur ?? Math.max( 1, Math.round( durationMs * 1000 ) ),
|
|
1041
|
+
pid: 1,
|
|
1042
|
+
tid: TRACE_TID_GPU,
|
|
1043
|
+
cat: 'gnsx-gpu',
|
|
1044
|
+
} );
|
|
1045
|
+
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
}
|
|
1049
|
+
|
|
301
1050
|
/**
|
|
302
1051
|
* @param {string} label
|
|
303
1052
|
* @return {number[]}
|
|
@@ -313,6 +1062,21 @@ class ProfilerServiceClass {
|
|
|
313
1062
|
|
|
314
1063
|
}
|
|
315
1064
|
|
|
1065
|
+
/**
|
|
1066
|
+
* @param {string} label
|
|
1067
|
+
* @return {number[]}
|
|
1068
|
+
*/
|
|
1069
|
+
getValidGpuSamples( label ) {
|
|
1070
|
+
|
|
1071
|
+
const buffer = this.gpuBuffers.get( label );
|
|
1072
|
+
const count = this.gpuCounts.get( label ) ?? 0;
|
|
1073
|
+
if ( buffer === undefined || count === 0 ) return [];
|
|
1074
|
+
return count < RING_SIZE
|
|
1075
|
+
? Array.from( buffer.subarray( 0, count ) )
|
|
1076
|
+
: Array.from( buffer );
|
|
1077
|
+
|
|
1078
|
+
}
|
|
1079
|
+
|
|
316
1080
|
/**
|
|
317
1081
|
* @param {string} label
|
|
318
1082
|
* @return {import('./ProfilerService.js').ProfilerStats|null}
|
|
@@ -360,6 +1124,31 @@ class ProfilerServiceClass {
|
|
|
360
1124
|
|
|
361
1125
|
}
|
|
362
1126
|
|
|
1127
|
+
/**
|
|
1128
|
+
* @param {string} label
|
|
1129
|
+
* @return {import('./ProfilerService.js').GpuProfilerStats|null}
|
|
1130
|
+
*/
|
|
1131
|
+
getGpuStats( label ) {
|
|
1132
|
+
|
|
1133
|
+
const samples = this.getValidGpuSamples( label );
|
|
1134
|
+
if ( samples.length === 0 ) return null;
|
|
1135
|
+
|
|
1136
|
+
const sorted = [ ...samples ].sort( ( a, b ) => a - b );
|
|
1137
|
+
const avg = sorted.reduce( ( a, b ) => a + b, 0 ) / sorted.length;
|
|
1138
|
+
|
|
1139
|
+
return {
|
|
1140
|
+
label,
|
|
1141
|
+
samples: samples.length,
|
|
1142
|
+
avg,
|
|
1143
|
+
min: sorted[ 0 ],
|
|
1144
|
+
max: sorted[ sorted.length - 1 ],
|
|
1145
|
+
p95: sorted[ Math.floor( sorted.length * 0.95 ) ],
|
|
1146
|
+
frameBudget: ( avg / FRAME_BUDGET_MS ) * 100,
|
|
1147
|
+
totalInvocations: this.gpuInvocations.get( label ) ?? 0,
|
|
1148
|
+
};
|
|
1149
|
+
|
|
1150
|
+
}
|
|
1151
|
+
|
|
363
1152
|
/**
|
|
364
1153
|
* @return {import('./ProfilerService.js').ProfilerStats[]}
|
|
365
1154
|
*/
|
|
@@ -371,10 +1160,22 @@ class ProfilerServiceClass {
|
|
|
371
1160
|
|
|
372
1161
|
}
|
|
373
1162
|
|
|
1163
|
+
/**
|
|
1164
|
+
* @return {import('./ProfilerService.js').GpuProfilerStats[]}
|
|
1165
|
+
*/
|
|
1166
|
+
getAllGpuStats() {
|
|
1167
|
+
|
|
1168
|
+
return [ ...this.gpuBuffers.keys() ]
|
|
1169
|
+
.map( label => this.getGpuStats( label ) )
|
|
1170
|
+
.filter( stats => stats !== null );
|
|
1171
|
+
|
|
1172
|
+
}
|
|
1173
|
+
|
|
374
1174
|
report() {
|
|
375
1175
|
|
|
376
1176
|
const stats = this.getAllStats();
|
|
377
|
-
|
|
1177
|
+
const gpuStats = this.getAllGpuStats();
|
|
1178
|
+
if ( stats.length === 0 && gpuStats.length === 0 ) {
|
|
378
1179
|
|
|
379
1180
|
console.log( '[ProfilerService] No data. Enable profiling first and wait a few frames.' );
|
|
380
1181
|
return;
|
|
@@ -394,6 +1195,24 @@ class ProfilerServiceClass {
|
|
|
394
1195
|
} ) )
|
|
395
1196
|
);
|
|
396
1197
|
|
|
1198
|
+
if ( gpuStats.length > 0 ) {
|
|
1199
|
+
|
|
1200
|
+
gpuStats.sort( ( a, b ) => b.avg - a.avg );
|
|
1201
|
+
console.log( '[ProfilerService] GPU timings' );
|
|
1202
|
+
console.table(
|
|
1203
|
+
gpuStats.map( stats => ( {
|
|
1204
|
+
label: stats.label,
|
|
1205
|
+
'avg ms': stats.avg.toFixed( 3 ),
|
|
1206
|
+
'min ms': stats.min.toFixed( 3 ),
|
|
1207
|
+
'max ms': stats.max.toFixed( 3 ),
|
|
1208
|
+
'p95 ms': stats.p95.toFixed( 3 ),
|
|
1209
|
+
'budget %': stats.frameBudget.toFixed( 1 ) + '%',
|
|
1210
|
+
samples: stats.samples,
|
|
1211
|
+
} ) )
|
|
1212
|
+
);
|
|
1213
|
+
|
|
1214
|
+
}
|
|
1215
|
+
|
|
397
1216
|
}
|
|
398
1217
|
|
|
399
1218
|
/**
|
|
@@ -410,6 +1229,7 @@ class ProfilerServiceClass {
|
|
|
410
1229
|
|
|
411
1230
|
} );
|
|
412
1231
|
const hasAsync = slices.some( e => e.tid === TRACE_TID_ASYNC );
|
|
1232
|
+
const hasGpu = slices.some( e => e.tid === TRACE_TID_GPU );
|
|
413
1233
|
/** @type {import('./ProfilerService.js').ChromeTraceMetadataEvent[]} */
|
|
414
1234
|
const prefix = [
|
|
415
1235
|
{
|
|
@@ -445,6 +1265,20 @@ class ProfilerServiceClass {
|
|
|
445
1265
|
|
|
446
1266
|
}
|
|
447
1267
|
|
|
1268
|
+
if ( hasGpu ) {
|
|
1269
|
+
|
|
1270
|
+
prefix.push( {
|
|
1271
|
+
cat: '__metadata',
|
|
1272
|
+
name: 'thread_name',
|
|
1273
|
+
ph: 'M',
|
|
1274
|
+
pid: 1,
|
|
1275
|
+
tid: TRACE_TID_GPU,
|
|
1276
|
+
ts: 0,
|
|
1277
|
+
args: { name: 'GPU (device timestamps)' },
|
|
1278
|
+
} );
|
|
1279
|
+
|
|
1280
|
+
}
|
|
1281
|
+
|
|
448
1282
|
return {
|
|
449
1283
|
displayTimeUnit: 'ms',
|
|
450
1284
|
traceEvents: [ ...prefix, ...slices ],
|
|
@@ -509,8 +1343,24 @@ class ProfilerServiceClass {
|
|
|
509
1343
|
|
|
510
1344
|
}
|
|
511
1345
|
|
|
1346
|
+
/**
|
|
1347
|
+
* @return {import('./ProfilerService.js').GpuProfilerStats[]}
|
|
1348
|
+
*/
|
|
1349
|
+
exportGpuJSON() {
|
|
1350
|
+
|
|
1351
|
+
return this.getAllGpuStats();
|
|
1352
|
+
|
|
1353
|
+
}
|
|
1354
|
+
|
|
512
1355
|
_clearState() {
|
|
513
1356
|
|
|
1357
|
+
this._gpuGeneration ++;
|
|
1358
|
+
for ( const state of this.gpuRendererStates.values() ) {
|
|
1359
|
+
|
|
1360
|
+
this._clearGpuRendererState( state );
|
|
1361
|
+
|
|
1362
|
+
}
|
|
1363
|
+
|
|
514
1364
|
this.buffers.clear();
|
|
515
1365
|
this.cursors.clear();
|
|
516
1366
|
this.counts.clear();
|
|
@@ -523,6 +1373,79 @@ class ProfilerServiceClass {
|
|
|
523
1373
|
this.selfCounts.clear();
|
|
524
1374
|
this.callStack.length = 0;
|
|
525
1375
|
this.invocations.clear();
|
|
1376
|
+
this.gpuBuffers.clear();
|
|
1377
|
+
this.gpuCursors.clear();
|
|
1378
|
+
this.gpuCounts.clear();
|
|
1379
|
+
this.gpuInvocations.clear();
|
|
1380
|
+
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* @param {import('./ProfilerService.js').GpuRendererState} state
|
|
1385
|
+
*/
|
|
1386
|
+
_clearGpuRendererState( state ) {
|
|
1387
|
+
|
|
1388
|
+
if ( state.kind === 'common' ) {
|
|
1389
|
+
|
|
1390
|
+
state.activeSpans.length = 0;
|
|
1391
|
+
state.pendingSpans.length = 0;
|
|
1392
|
+
state.queryCpuTimes.clear();
|
|
1393
|
+
state.gpuTimestampOrigin = null;
|
|
1394
|
+
state.gpuTraceOrigin = null;
|
|
1395
|
+
return;
|
|
1396
|
+
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
if ( state.activeSpan !== null ) {
|
|
1400
|
+
|
|
1401
|
+
try {
|
|
1402
|
+
|
|
1403
|
+
state.gl.endQuery( state.extension.TIME_ELAPSED_EXT );
|
|
1404
|
+
|
|
1405
|
+
} catch {
|
|
1406
|
+
|
|
1407
|
+
// The context may have been lost while the query was active.
|
|
1408
|
+
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
state.gl.deleteQuery( state.activeSpan._query );
|
|
1412
|
+
state.activeSpan = null;
|
|
1413
|
+
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
if ( state.flushPromise === null ) {
|
|
1417
|
+
|
|
1418
|
+
for ( const span of state.pendingSpans ) {
|
|
1419
|
+
|
|
1420
|
+
state.gl.deleteQuery( span._query );
|
|
1421
|
+
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
state.pendingSpans.length = 0;
|
|
1427
|
+
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
_detachGpuRenderers() {
|
|
1431
|
+
|
|
1432
|
+
for ( const state of this.gpuRendererStates.values() ) {
|
|
1433
|
+
|
|
1434
|
+
if ( state.kind === 'common' && state.available ) {
|
|
1435
|
+
|
|
1436
|
+
state.renderer.backend.removeTimestampQueryListener( state.listener );
|
|
1437
|
+
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if ( state.kind === 'common' ) {
|
|
1441
|
+
|
|
1442
|
+
state.renderer.backend.trackTimestamp = state.previousTrackTimestamp;
|
|
1443
|
+
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
this.gpuRendererStates.clear();
|
|
526
1449
|
|
|
527
1450
|
}
|
|
528
1451
|
|
|
@@ -552,6 +1475,18 @@ class ProfilerServiceClass {
|
|
|
552
1475
|
* @property {number|undefined} selfFrameBudget Exclusive time as percentage of a 60 fps frame budget.
|
|
553
1476
|
*/
|
|
554
1477
|
|
|
1478
|
+
/**
|
|
1479
|
+
* @typedef {Object} GpuProfilerStats
|
|
1480
|
+
* @property {string} label
|
|
1481
|
+
* @property {number} samples
|
|
1482
|
+
* @property {number} avg
|
|
1483
|
+
* @property {number} min
|
|
1484
|
+
* @property {number} max
|
|
1485
|
+
* @property {number} p95
|
|
1486
|
+
* @property {number} frameBudget Percentage of a 60 fps frame budget (16.67 ms)
|
|
1487
|
+
* @property {number} totalInvocations
|
|
1488
|
+
*/
|
|
1489
|
+
|
|
555
1490
|
/**
|
|
556
1491
|
* @typedef {Object} SpanHandle
|
|
557
1492
|
* @property {string} label
|
|
@@ -560,6 +1495,51 @@ class ProfilerServiceClass {
|
|
|
560
1495
|
* @property {string} _startMark
|
|
561
1496
|
*/
|
|
562
1497
|
|
|
1498
|
+
/**
|
|
1499
|
+
* @typedef {Object} GpuSpanHandle
|
|
1500
|
+
* @property {string} label
|
|
1501
|
+
* @property {?Object} renderer
|
|
1502
|
+
* @property {number} t0
|
|
1503
|
+
* @property {number} _seq
|
|
1504
|
+
* @property {number} _generation
|
|
1505
|
+
* @property {{render: Set<string>, compute: Set<string>}} [_queries]
|
|
1506
|
+
* @property {WebGLQuery} [_query]
|
|
1507
|
+
* @property {number} [_endTime]
|
|
1508
|
+
*/
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* @typedef {Object} CommonGpuRendererState
|
|
1512
|
+
* @property {'common'} kind
|
|
1513
|
+
* @property {boolean} available
|
|
1514
|
+
* @property {Object} renderer
|
|
1515
|
+
* @property {function('render'|'compute', string, ?string): void} listener
|
|
1516
|
+
* @property {boolean} previousTrackTimestamp
|
|
1517
|
+
* @property {GpuSpanHandle[]} activeSpans
|
|
1518
|
+
* @property {GpuSpanHandle[]} pendingSpans
|
|
1519
|
+
* @property {Map<string, number>} queryCpuTimes
|
|
1520
|
+
* @property {?bigint} gpuTimestampOrigin
|
|
1521
|
+
* @property {?number} gpuTraceOrigin
|
|
1522
|
+
* @property {?Promise<void>} flushPromise
|
|
1523
|
+
* @property {boolean} flushScheduled
|
|
1524
|
+
*/
|
|
1525
|
+
|
|
1526
|
+
/**
|
|
1527
|
+
* @typedef {Object} LegacyWebGLGpuRendererState
|
|
1528
|
+
* @property {'legacy-webgl'} kind
|
|
1529
|
+
* @property {boolean} available
|
|
1530
|
+
* @property {Object} renderer
|
|
1531
|
+
* @property {WebGL2RenderingContext} gl
|
|
1532
|
+
* @property {?EXT_disjoint_timer_query_webgl2} extension
|
|
1533
|
+
* @property {?GpuSpanHandle} activeSpan
|
|
1534
|
+
* @property {GpuSpanHandle[]} pendingSpans
|
|
1535
|
+
* @property {?Promise<void>} flushPromise
|
|
1536
|
+
* @property {boolean} flushScheduled
|
|
1537
|
+
*/
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* @typedef {CommonGpuRendererState|LegacyWebGLGpuRendererState} GpuRendererState
|
|
1541
|
+
*/
|
|
1542
|
+
|
|
563
1543
|
/**
|
|
564
1544
|
* @typedef {Object} EndSpanOptions
|
|
565
1545
|
* @property {boolean} [asyncTimeline] When true, trace slice uses `tid=2` (virtual async row).
|
|
@@ -573,7 +1553,7 @@ class ProfilerServiceClass {
|
|
|
573
1553
|
* @property {number} dur
|
|
574
1554
|
* @property {1} pid
|
|
575
1555
|
* @property {number} tid
|
|
576
|
-
* @property {'gnsx'} cat
|
|
1556
|
+
* @property {'gnsx'|'gnsx-gpu'} cat
|
|
577
1557
|
*/
|
|
578
1558
|
|
|
579
1559
|
/**
|