@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.
- package/build/three.cjs +759 -2
- package/build/three.core.js +759 -2
- package/build/three.core.min.js +1 -1
- package/build/three.tsl.js +1 -1
- package/build/three.tsl.min.js +1 -1
- package/build/three.webgpu.js +138 -9
- package/build/three.webgpu.min.js +1 -1
- package/build/three.webgpu.nodes.js +138 -9
- package/build/three.webgpu.nodes.min.js +1 -1
- package/package.json +1 -1
- package/src/profiler/ProfilerService.js +761 -2
- package/src/renderers/common/Backend.js +60 -0
- package/src/renderers/common/RenderContext.js +10 -0
- package/src/renderers/common/Renderer.js +5 -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 +13 -2
- package/src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js +6 -0
|
@@ -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
|
-
|
|
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
|
/**
|