@gnsx/three 0.184.19 → 0.184.20
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 +185 -42
- package/build/three.core.js +185 -42
- package/build/three.core.min.js +1 -1
- package/package.json +1 -1
- package/src/profiler/ProfilerService.js +185 -42
package/package.json
CHANGED
|
@@ -9,14 +9,22 @@
|
|
|
9
9
|
* __gnsx_profiler.downloadTrace() // download gnsx-trace.json for Speedscope / Perfetto
|
|
10
10
|
* __gnsx_profiler.reset() // clear samples and trace
|
|
11
11
|
* __gnsx_profiler.disable()
|
|
12
|
+
*
|
|
13
|
+
* Chrome trace: `tid=1` is synchronous work; `@profile` on async methods records the
|
|
14
|
+
* promise lifetime on `tid=2` so long async does not flatten the main row. For manual
|
|
15
|
+
* spans, use `beginSpan` / `endSpan` with `{ asyncTimeline: true }` in a `.finally()`.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
const RING_SIZE = 120;
|
|
15
19
|
const FRAME_BUDGET_MS = 1000 / 60;
|
|
16
|
-
|
|
17
|
-
const
|
|
20
|
+
const TRACE_TID_MAIN = 1;
|
|
21
|
+
const TRACE_TID_ASYNC = 2;
|
|
18
22
|
const NOOP = () => {};
|
|
19
23
|
|
|
24
|
+
const DUMMY_SPAN = Object.freeze( { label: '', t0: 0, _seq: 0 } );
|
|
25
|
+
const NOOP_BEGIN_SPAN = () => DUMMY_SPAN;
|
|
26
|
+
const NOOP_END_SPAN = () => {};
|
|
27
|
+
|
|
20
28
|
class ProfilerServiceClass {
|
|
21
29
|
|
|
22
30
|
constructor() {
|
|
@@ -30,7 +38,7 @@ class ProfilerServiceClass {
|
|
|
30
38
|
this.cursors = new Map();
|
|
31
39
|
/** @type {Map<string, number>} */
|
|
32
40
|
this.counts = new Map();
|
|
33
|
-
/** @type {Map<string, Array<{ startTime: number, startMark: string
|
|
41
|
+
/** @type {Map<string, Array<{ startTime: number, startMark: string }>>} */
|
|
34
42
|
this.marks = new Map();
|
|
35
43
|
/** @type {ChromeTraceEvent[]} */
|
|
36
44
|
this.traceEvents = [];
|
|
@@ -56,6 +64,8 @@ class ProfilerServiceClass {
|
|
|
56
64
|
this.traceStartTime = performance.now();
|
|
57
65
|
this.begin = this._beginImpl.bind( this );
|
|
58
66
|
this.end = this._endImpl.bind( this );
|
|
67
|
+
this.beginSpan = this._beginSpanImpl.bind( this );
|
|
68
|
+
this.endSpan = this._endSpanImpl.bind( this );
|
|
59
69
|
this._exposeGlobal();
|
|
60
70
|
console.log( `[ProfilerService] auto-started from env (profile: ${this._profile})` );
|
|
61
71
|
|
|
@@ -63,6 +73,8 @@ class ProfilerServiceClass {
|
|
|
63
73
|
|
|
64
74
|
this.begin = NOOP;
|
|
65
75
|
this.end = NOOP;
|
|
76
|
+
this.beginSpan = NOOP_BEGIN_SPAN;
|
|
77
|
+
this.endSpan = NOOP_END_SPAN;
|
|
66
78
|
|
|
67
79
|
}
|
|
68
80
|
|
|
@@ -94,6 +106,8 @@ class ProfilerServiceClass {
|
|
|
94
106
|
this._enabled = true;
|
|
95
107
|
this.begin = this._beginImpl.bind( this );
|
|
96
108
|
this.end = this._endImpl.bind( this );
|
|
109
|
+
this.beginSpan = this._beginSpanImpl.bind( this );
|
|
110
|
+
this.endSpan = this._endSpanImpl.bind( this );
|
|
97
111
|
this._exposeGlobal();
|
|
98
112
|
console.log( `[ProfilerService] enabled (profile: ${this._profile}) — call __gnsx_profiler.report() or downloadTrace() from the console` );
|
|
99
113
|
|
|
@@ -105,6 +119,8 @@ class ProfilerServiceClass {
|
|
|
105
119
|
this._enabled = false;
|
|
106
120
|
this.begin = NOOP;
|
|
107
121
|
this.end = NOOP;
|
|
122
|
+
this.beginSpan = NOOP_BEGIN_SPAN;
|
|
123
|
+
this.endSpan = NOOP_END_SPAN;
|
|
108
124
|
console.log( '[ProfilerService] disabled' );
|
|
109
125
|
|
|
110
126
|
}
|
|
@@ -140,22 +156,8 @@ class ProfilerServiceClass {
|
|
|
140
156
|
|
|
141
157
|
}
|
|
142
158
|
|
|
143
|
-
const traced = this._profile === 'full' && this.traceEvents.length < MAX_TRACE_EVENTS;
|
|
144
|
-
if ( traced ) {
|
|
145
|
-
|
|
146
|
-
this.traceEvents.push( {
|
|
147
|
-
name: label,
|
|
148
|
-
ph: 'B',
|
|
149
|
-
ts: this._getTraceTimestamp( startTime ),
|
|
150
|
-
pid: 1,
|
|
151
|
-
tid: 1,
|
|
152
|
-
cat: 'gnsx',
|
|
153
|
-
} );
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
159
|
performance.mark( startMark );
|
|
158
|
-
stack.push( { startTime, startMark
|
|
160
|
+
stack.push( { startTime, startMark } );
|
|
159
161
|
|
|
160
162
|
// Push onto the global call stack for exclusive-time tracking.
|
|
161
163
|
this.callStack.push( { label, childTime: 0 } );
|
|
@@ -173,13 +175,61 @@ class ProfilerServiceClass {
|
|
|
173
175
|
if ( stack.length === 0 ) this.marks.delete( label );
|
|
174
176
|
|
|
175
177
|
const now = performance.now();
|
|
176
|
-
const { startTime, startMark
|
|
178
|
+
const { startTime, startMark } = mark;
|
|
177
179
|
const duration = now - startTime;
|
|
178
180
|
const endMark = `gnsx:${label}:end:${++ this._markId}`;
|
|
179
181
|
|
|
180
182
|
performance.mark( endMark );
|
|
181
183
|
performance.measure( `gnsx:${label}`, startMark, endMark );
|
|
182
184
|
|
|
185
|
+
this._commitDurationSample( label, startTime, duration, TRACE_TID_MAIN );
|
|
186
|
+
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Per-invocation span start (pair with {@link ProfilerServiceClass#endSpan}).
|
|
191
|
+
* Safe for concurrent async with the same label.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} label
|
|
194
|
+
* @return {import('./ProfilerService.js').SpanHandle}
|
|
195
|
+
*/
|
|
196
|
+
_beginSpanImpl( label ) {
|
|
197
|
+
|
|
198
|
+
const seq = ++ this._markId;
|
|
199
|
+
const t0 = performance.now();
|
|
200
|
+
const startMark = `gnsx:${label}:s${seq}:start`;
|
|
201
|
+
performance.mark( startMark );
|
|
202
|
+
return { label, t0, _seq: seq, _startMark: startMark };
|
|
203
|
+
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {import('./ProfilerService.js').SpanHandle} handle
|
|
208
|
+
* @param {import('./ProfilerService.js').EndSpanOptions} [opts]
|
|
209
|
+
*/
|
|
210
|
+
_endSpanImpl( handle, opts ) {
|
|
211
|
+
|
|
212
|
+
if ( handle._seq === 0 ) return;
|
|
213
|
+
|
|
214
|
+
const now = performance.now();
|
|
215
|
+
const duration = now - handle.t0;
|
|
216
|
+
const endMark = `gnsx:${handle.label}:s${handle._seq}:end`;
|
|
217
|
+
performance.mark( endMark );
|
|
218
|
+
performance.measure( `gnsx:${handle.label}#${handle._seq}`, handle._startMark, endMark );
|
|
219
|
+
|
|
220
|
+
const traceTid = opts?.asyncTimeline === true ? TRACE_TID_ASYNC : TRACE_TID_MAIN;
|
|
221
|
+
this._commitDurationSample( handle.label, handle.t0, duration, traceTid );
|
|
222
|
+
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {string} label
|
|
227
|
+
* @param {number} startTime
|
|
228
|
+
* @param {number} durationMs
|
|
229
|
+
* @param {typeof TRACE_TID_MAIN|typeof TRACE_TID_ASYNC} traceTid
|
|
230
|
+
*/
|
|
231
|
+
_commitDurationSample( label, startTime, durationMs, traceTid ) {
|
|
232
|
+
|
|
183
233
|
let buffer = this.buffers.get( label );
|
|
184
234
|
if ( ! buffer ) {
|
|
185
235
|
|
|
@@ -191,7 +241,7 @@ class ProfilerServiceClass {
|
|
|
191
241
|
}
|
|
192
242
|
|
|
193
243
|
const cursor = this.cursors.get( label );
|
|
194
|
-
buffer[ cursor ] =
|
|
244
|
+
buffer[ cursor ] = durationMs;
|
|
195
245
|
this.cursors.set( label, ( cursor + 1 ) % RING_SIZE );
|
|
196
246
|
this.counts.set( label, Math.min( ( this.counts.get( label ) + 1 ), RING_SIZE ) );
|
|
197
247
|
|
|
@@ -203,7 +253,7 @@ class ProfilerServiceClass {
|
|
|
203
253
|
if ( top !== undefined && top.label === label ) {
|
|
204
254
|
|
|
205
255
|
this.callStack.pop();
|
|
206
|
-
const selfTime = Math.max( 0,
|
|
256
|
+
const selfTime = Math.max( 0, durationMs - top.childTime );
|
|
207
257
|
|
|
208
258
|
let selfBuffer = this.selfBuffers.get( label );
|
|
209
259
|
if ( ! selfBuffer ) {
|
|
@@ -222,7 +272,7 @@ class ProfilerServiceClass {
|
|
|
222
272
|
|
|
223
273
|
// Propagate inclusive duration to the parent scope's child accumulator.
|
|
224
274
|
const parent = this.callStack.length > 0 ? this.callStack[ this.callStack.length - 1 ] : undefined;
|
|
225
|
-
if ( parent !== undefined ) parent.childTime +=
|
|
275
|
+
if ( parent !== undefined ) parent.childTime += durationMs;
|
|
226
276
|
|
|
227
277
|
} else {
|
|
228
278
|
|
|
@@ -231,14 +281,16 @@ class ProfilerServiceClass {
|
|
|
231
281
|
|
|
232
282
|
}
|
|
233
283
|
|
|
234
|
-
if (
|
|
284
|
+
if ( traceTid ) {
|
|
235
285
|
|
|
286
|
+
const traceName = traceTid === TRACE_TID_ASYNC ? `${label} (promise)` : label;
|
|
236
287
|
this.traceEvents.push( {
|
|
237
|
-
name:
|
|
238
|
-
ph: '
|
|
239
|
-
ts: this._getTraceTimestamp(
|
|
288
|
+
name: traceName,
|
|
289
|
+
ph: 'X',
|
|
290
|
+
ts: Math.round( this._getTraceTimestamp( startTime ) ),
|
|
291
|
+
dur: Math.max( 1, Math.round( durationMs * 1000 ) ),
|
|
240
292
|
pid: 1,
|
|
241
|
-
tid:
|
|
293
|
+
tid: traceTid,
|
|
242
294
|
cat: 'gnsx',
|
|
243
295
|
} );
|
|
244
296
|
|
|
@@ -349,13 +401,53 @@ class ProfilerServiceClass {
|
|
|
349
401
|
*/
|
|
350
402
|
exportChromeTrace() {
|
|
351
403
|
|
|
404
|
+
const slices = [ ...this.traceEvents ];
|
|
405
|
+
slices.sort( ( a, b ) => {
|
|
406
|
+
|
|
407
|
+
if ( a.ts !== b.ts ) return a.ts - b.ts;
|
|
408
|
+
if ( a.tid !== b.tid ) return a.tid - b.tid;
|
|
409
|
+
return a.name.localeCompare( b.name );
|
|
410
|
+
|
|
411
|
+
} );
|
|
412
|
+
const hasAsync = slices.some( e => e.tid === TRACE_TID_ASYNC );
|
|
413
|
+
/** @type {import('./ProfilerService.js').ChromeTraceMetadataEvent[]} */
|
|
414
|
+
const prefix = [
|
|
415
|
+
{
|
|
416
|
+
cat: '__metadata',
|
|
417
|
+
name: 'process_name',
|
|
418
|
+
ph: 'M',
|
|
419
|
+
pid: 1,
|
|
420
|
+
tid: 0,
|
|
421
|
+
ts: 0,
|
|
422
|
+
args: { name: 'Genesys Profiler' },
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
cat: '__metadata',
|
|
426
|
+
name: 'thread_name',
|
|
427
|
+
ph: 'M',
|
|
428
|
+
pid: 1,
|
|
429
|
+
tid: TRACE_TID_MAIN,
|
|
430
|
+
ts: 0,
|
|
431
|
+
args: { name: 'Main thread' },
|
|
432
|
+
},
|
|
433
|
+
];
|
|
434
|
+
if ( hasAsync ) {
|
|
435
|
+
|
|
436
|
+
prefix.push( {
|
|
437
|
+
cat: '__metadata',
|
|
438
|
+
name: 'thread_name',
|
|
439
|
+
ph: 'M',
|
|
440
|
+
pid: 1,
|
|
441
|
+
tid: TRACE_TID_ASYNC,
|
|
442
|
+
ts: 0,
|
|
443
|
+
args: { name: 'Async (promise lifetime)' },
|
|
444
|
+
} );
|
|
445
|
+
|
|
446
|
+
}
|
|
447
|
+
|
|
352
448
|
return {
|
|
353
449
|
displayTimeUnit: 'ms',
|
|
354
|
-
traceEvents: [
|
|
355
|
-
{ name: 'process_name', ph: 'M', pid: 1, args: { name: 'Genesys Profiler' } },
|
|
356
|
-
{ name: 'thread_name', ph: 'M', pid: 1, tid: 1, args: { name: 'Main Thread' } },
|
|
357
|
-
...this.traceEvents,
|
|
358
|
-
],
|
|
450
|
+
traceEvents: [ ...prefix, ...slices ],
|
|
359
451
|
};
|
|
360
452
|
|
|
361
453
|
}
|
|
@@ -460,22 +552,38 @@ class ProfilerServiceClass {
|
|
|
460
552
|
* @property {number|undefined} selfFrameBudget Exclusive time as percentage of a 60 fps frame budget.
|
|
461
553
|
*/
|
|
462
554
|
|
|
555
|
+
/**
|
|
556
|
+
* @typedef {Object} SpanHandle
|
|
557
|
+
* @property {string} label
|
|
558
|
+
* @property {number} t0
|
|
559
|
+
* @property {number} _seq
|
|
560
|
+
* @property {string} _startMark
|
|
561
|
+
*/
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* @typedef {Object} EndSpanOptions
|
|
565
|
+
* @property {boolean} [asyncTimeline] When true, trace slice uses `tid=2` (virtual async row).
|
|
566
|
+
*/
|
|
567
|
+
|
|
463
568
|
/**
|
|
464
569
|
* @typedef {Object} ChromeTraceEvent
|
|
465
570
|
* @property {string} name
|
|
466
|
-
* @property {'
|
|
571
|
+
* @property {'X'} ph
|
|
467
572
|
* @property {number} ts
|
|
573
|
+
* @property {number} dur
|
|
468
574
|
* @property {1} pid
|
|
469
|
-
* @property {
|
|
575
|
+
* @property {number} tid
|
|
470
576
|
* @property {'gnsx'} cat
|
|
471
577
|
*/
|
|
472
578
|
|
|
473
579
|
/**
|
|
474
580
|
* @typedef {Object} ChromeTraceMetadataEvent
|
|
581
|
+
* @property {'__metadata'} cat
|
|
475
582
|
* @property {'process_name'|'thread_name'} name
|
|
476
583
|
* @property {'M'} ph
|
|
477
584
|
* @property {1} pid
|
|
478
|
-
* @property {
|
|
585
|
+
* @property {number} [tid]
|
|
586
|
+
* @property {0} ts
|
|
479
587
|
* @property {{ name: string }} args
|
|
480
588
|
*/
|
|
481
589
|
|
|
@@ -491,28 +599,38 @@ class ProfilerServiceClass {
|
|
|
491
599
|
|
|
492
600
|
const ProfilerService = new ProfilerServiceClass();
|
|
493
601
|
|
|
602
|
+
function isThenable( x ) {
|
|
603
|
+
|
|
604
|
+
return (
|
|
605
|
+
( typeof x === 'object' || typeof x === 'function' ) &&
|
|
606
|
+
x !== null &&
|
|
607
|
+
typeof x.then === 'function'
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
}
|
|
611
|
+
|
|
494
612
|
function applyProfileToMethod( label, descriptor ) {
|
|
495
613
|
|
|
496
614
|
const original = descriptor.value;
|
|
497
615
|
|
|
498
616
|
function profiled( ...args ) {
|
|
499
617
|
|
|
500
|
-
ProfilerService.
|
|
618
|
+
const span = ProfilerService.beginSpan( label );
|
|
501
619
|
try {
|
|
502
620
|
|
|
503
621
|
const result = original.apply( this, args );
|
|
504
|
-
if ( result
|
|
622
|
+
if ( isThenable( result ) ) {
|
|
505
623
|
|
|
506
|
-
return result.finally( () => ProfilerService.
|
|
624
|
+
return Promise.resolve( result ).finally( () => ProfilerService.endSpan( span, { asyncTimeline: true } ) );
|
|
507
625
|
|
|
508
626
|
}
|
|
509
627
|
|
|
510
|
-
ProfilerService.
|
|
628
|
+
ProfilerService.endSpan( span );
|
|
511
629
|
return result;
|
|
512
630
|
|
|
513
631
|
} catch ( error ) {
|
|
514
632
|
|
|
515
|
-
ProfilerService.
|
|
633
|
+
ProfilerService.endSpan( span );
|
|
516
634
|
throw error;
|
|
517
635
|
|
|
518
636
|
}
|
|
@@ -526,14 +644,39 @@ function applyProfileToMethod( label, descriptor ) {
|
|
|
526
644
|
|
|
527
645
|
/**
|
|
528
646
|
* Method decorator that profiles the decorated method.
|
|
529
|
-
*
|
|
647
|
+
* Default label: `ClassName.methodName`. Pass a custom tag via `@profile('My tag')`.
|
|
530
648
|
*
|
|
649
|
+
* @param {Object|string} [targetOrTag]
|
|
650
|
+
* @param {string|symbol} [propertyKey]
|
|
651
|
+
* @param {PropertyDescriptor} [descriptor]
|
|
652
|
+
* @return {PropertyDescriptor|function(Object, string|symbol, PropertyDescriptor): PropertyDescriptor}
|
|
653
|
+
*/
|
|
654
|
+
function profile( targetOrTag, propertyKey, descriptor ) {
|
|
655
|
+
|
|
656
|
+
if ( arguments.length === 0 ) {
|
|
657
|
+
|
|
658
|
+
return defaultProfileDecorator;
|
|
659
|
+
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if ( arguments.length === 1 && typeof targetOrTag === 'string' ) {
|
|
663
|
+
|
|
664
|
+
const customTag = targetOrTag;
|
|
665
|
+
return ( target, key, desc ) => applyProfileToMethod( customTag, desc );
|
|
666
|
+
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return defaultProfileDecorator( targetOrTag, propertyKey, descriptor );
|
|
670
|
+
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
531
674
|
* @param {Object} target
|
|
532
675
|
* @param {string|symbol} propertyKey
|
|
533
676
|
* @param {PropertyDescriptor} descriptor
|
|
534
677
|
* @return {PropertyDescriptor}
|
|
535
678
|
*/
|
|
536
|
-
function
|
|
679
|
+
function defaultProfileDecorator( target, propertyKey, descriptor ) {
|
|
537
680
|
|
|
538
681
|
const className = target.constructor?.name ?? 'Unknown';
|
|
539
682
|
return applyProfileToMethod( `${className}.${String( propertyKey )}`, descriptor );
|