@gnsx/three 0.184.11 → 0.184.13
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 +509 -0
- package/build/three.core.js +507 -1
- package/build/three.core.min.js +1 -1
- package/build/three.module.js +1 -1
- package/build/three.module.min.js +1 -1
- package/build/three.webgpu.js +104 -3
- package/build/three.webgpu.min.js +1 -1
- package/build/three.webgpu.nodes.js +104 -3
- package/build/three.webgpu.nodes.min.js +1 -1
- package/package.json +1 -1
- package/src/Three.Core.js +5 -0
- package/src/nodes/core/NodeFrame.js +22 -0
- package/src/nodes/lighting/ShadowNode.js +20 -0
- package/src/profiler/ProfilerService.js +504 -0
- package/src/renderers/common/Renderer.js +64 -0
package/package.json
CHANGED
package/src/Three.Core.js
CHANGED
|
@@ -164,6 +164,11 @@ export { createCanvasElement, setConsoleFunction, getConsoleFunction, log, warn,
|
|
|
164
164
|
export * from './constants.js';
|
|
165
165
|
export * from './Three.Legacy.js';
|
|
166
166
|
|
|
167
|
+
// WITH_GENESYS
|
|
168
|
+
export { ProfilerService, profile, profileClass } from './profiler/ProfilerService.js';
|
|
169
|
+
// !WITH_GENESYS
|
|
170
|
+
|
|
171
|
+
|
|
167
172
|
if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {
|
|
168
173
|
|
|
169
174
|
__THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'register', { detail: {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { NodeUpdateType } from './constants.js';
|
|
2
2
|
|
|
3
|
+
// WITH_GENESYS
|
|
4
|
+
import { ProfilerService } from '../../profiler/ProfilerService.js';
|
|
5
|
+
// !WITH_GENESYS
|
|
6
|
+
|
|
3
7
|
/**
|
|
4
8
|
* Management class for updating nodes. The module tracks metrics like
|
|
5
9
|
* the elapsed time, delta time, the render and frame ID to correctly
|
|
@@ -146,6 +150,11 @@ class NodeFrame {
|
|
|
146
150
|
*/
|
|
147
151
|
updateBeforeNode( node ) {
|
|
148
152
|
|
|
153
|
+
// WITH_GENESYS
|
|
154
|
+
const label = `${node.type}.updateBefore`;
|
|
155
|
+
ProfilerService.begin( label );
|
|
156
|
+
// !WITH_GENESYS
|
|
157
|
+
|
|
149
158
|
const updateType = node.getUpdateBeforeType();
|
|
150
159
|
const reference = node.updateReference( this );
|
|
151
160
|
|
|
@@ -191,6 +200,10 @@ class NodeFrame {
|
|
|
191
200
|
|
|
192
201
|
}
|
|
193
202
|
|
|
203
|
+
// WITH_GENESYS
|
|
204
|
+
ProfilerService.end( label );
|
|
205
|
+
// !WITH_GENESYS
|
|
206
|
+
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
/**
|
|
@@ -203,6 +216,11 @@ class NodeFrame {
|
|
|
203
216
|
*/
|
|
204
217
|
updateAfterNode( node ) {
|
|
205
218
|
|
|
219
|
+
// WITH_GENESYS
|
|
220
|
+
const label = `${node.type}.updateAfter`;
|
|
221
|
+
ProfilerService.begin( label );
|
|
222
|
+
// !WITH_GENESYS
|
|
223
|
+
|
|
206
224
|
const updateType = node.getUpdateAfterType();
|
|
207
225
|
const reference = node.updateReference( this );
|
|
208
226
|
|
|
@@ -240,6 +258,10 @@ class NodeFrame {
|
|
|
240
258
|
|
|
241
259
|
}
|
|
242
260
|
|
|
261
|
+
// WITH_GENESYS
|
|
262
|
+
ProfilerService.end( label );
|
|
263
|
+
// !WITH_GENESYS
|
|
264
|
+
|
|
243
265
|
}
|
|
244
266
|
|
|
245
267
|
/**
|
|
@@ -24,6 +24,10 @@ import { uv } from '../accessors/UV.js';
|
|
|
24
24
|
import { positionLocal } from '../accessors/Position.js';
|
|
25
25
|
import { uniform } from '../core/UniformNode.js';
|
|
26
26
|
|
|
27
|
+
// WITH_GENESYS
|
|
28
|
+
import { ProfilerService } from '../../profiler/ProfilerService.js';
|
|
29
|
+
// !WITH_GENESYS
|
|
30
|
+
|
|
27
31
|
//
|
|
28
32
|
|
|
29
33
|
const _shadowRenderObjectLibrary = /*@__PURE__*/ new ChainMap();
|
|
@@ -676,6 +680,10 @@ class ShadowNode extends ShadowBaseNode {
|
|
|
676
680
|
*/
|
|
677
681
|
renderShadow( frame ) {
|
|
678
682
|
|
|
683
|
+
// WITH_GENESYS
|
|
684
|
+
ProfilerService.begin( 'ShadowNode.renderShadow' );
|
|
685
|
+
// !WITH_GENESYS
|
|
686
|
+
|
|
679
687
|
const { shadow, shadowMap, light } = this;
|
|
680
688
|
const { renderer, scene } = frame;
|
|
681
689
|
|
|
@@ -691,6 +699,10 @@ class ShadowNode extends ShadowBaseNode {
|
|
|
691
699
|
|
|
692
700
|
scene.name = currentSceneName;
|
|
693
701
|
|
|
702
|
+
// WITH_GENESYS
|
|
703
|
+
ProfilerService.end( 'ShadowNode.renderShadow' );
|
|
704
|
+
// !WITH_GENESYS
|
|
705
|
+
|
|
694
706
|
}
|
|
695
707
|
|
|
696
708
|
/**
|
|
@@ -756,6 +768,10 @@ class ShadowNode extends ShadowBaseNode {
|
|
|
756
768
|
*/
|
|
757
769
|
vsmPass( renderer ) {
|
|
758
770
|
|
|
771
|
+
// WITH_GENESYS
|
|
772
|
+
ProfilerService.begin( 'ShadowNode.vsmPass' );
|
|
773
|
+
// !WITH_GENESYS
|
|
774
|
+
|
|
759
775
|
const { shadow } = this;
|
|
760
776
|
|
|
761
777
|
const depth = this.shadowMap.depth;
|
|
@@ -770,6 +786,10 @@ class ShadowNode extends ShadowBaseNode {
|
|
|
770
786
|
_quadMesh.material = this.vsmMaterialHorizontal;
|
|
771
787
|
_quadMesh.render( renderer );
|
|
772
788
|
|
|
789
|
+
// WITH_GENESYS
|
|
790
|
+
ProfilerService.end( 'ShadowNode.vsmPass' );
|
|
791
|
+
// !WITH_GENESYS
|
|
792
|
+
|
|
773
793
|
}
|
|
774
794
|
|
|
775
795
|
/**
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// WITH_GENESYS
|
|
2
|
+
/**
|
|
3
|
+
* ProfilerService — per-label CPU timing with ring-buffer aggregation and DevTools integration.
|
|
4
|
+
*
|
|
5
|
+
* Activate via environment variable:
|
|
6
|
+
* PROFILING=1 stats + trace (full profile)
|
|
7
|
+
* PROFILING=stats stats only, no trace accumulation (lower overhead)
|
|
8
|
+
*
|
|
9
|
+
* Quick start (browser console):
|
|
10
|
+
* __gnsx_profiler.enable()
|
|
11
|
+
* // play for a few seconds
|
|
12
|
+
* __gnsx_profiler.report() // sorted console.table (aggregated stats)
|
|
13
|
+
* __gnsx_profiler.downloadTrace() // download gnsx-trace.json for Speedscope / Perfetto
|
|
14
|
+
* __gnsx_profiler.reset() // clear samples and trace
|
|
15
|
+
* __gnsx_profiler.disable()
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const RING_SIZE = 120;
|
|
19
|
+
const FRAME_BUDGET_MS = 1000 / 60;
|
|
20
|
+
/** Max events in the trace log (~50s at 60fps with 8 labels). */
|
|
21
|
+
const MAX_TRACE_EVENTS = 50_000;
|
|
22
|
+
const NOOP = () => {};
|
|
23
|
+
|
|
24
|
+
function _readProfilingEnv() {
|
|
25
|
+
|
|
26
|
+
const raw =
|
|
27
|
+
( typeof process !== 'undefined' ? process.env?.[ 'PROFILING' ] : undefined ) ??
|
|
28
|
+
( typeof globalThis !== 'undefined' ? globalThis[ '__gnsx_env__' ]?.[ 'PROFILING' ] : undefined ) ??
|
|
29
|
+
( typeof globalThis !== 'undefined' ? globalThis[ '__GNSX_PROFILING__' ] : undefined );
|
|
30
|
+
|
|
31
|
+
if ( ! raw || raw === '0' || raw === 'false' ) return false;
|
|
32
|
+
if ( raw === 'stats' ) return 'stats';
|
|
33
|
+
return 'full';
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class ProfilerServiceClass {
|
|
38
|
+
|
|
39
|
+
constructor() {
|
|
40
|
+
|
|
41
|
+
const envProfile = _readProfilingEnv();
|
|
42
|
+
this._profile = envProfile !== false ? envProfile : 'full';
|
|
43
|
+
this._enabled = envProfile !== false;
|
|
44
|
+
|
|
45
|
+
/** @type {Map<string, Float64Array>} */
|
|
46
|
+
this.buffers = new Map();
|
|
47
|
+
/** @type {Map<string, number>} */
|
|
48
|
+
this.cursors = new Map();
|
|
49
|
+
/** @type {Map<string, number>} */
|
|
50
|
+
this.counts = new Map();
|
|
51
|
+
/** @type {Map<string, Array<{ startTime: number, startMark: string, traced: boolean }>>} */
|
|
52
|
+
this.marks = new Map();
|
|
53
|
+
/** @type {ChromeTraceEvent[]} */
|
|
54
|
+
this.traceEvents = [];
|
|
55
|
+
this.traceStartTime = 0;
|
|
56
|
+
this._markId = 0;
|
|
57
|
+
|
|
58
|
+
if ( this._enabled ) {
|
|
59
|
+
|
|
60
|
+
this.traceStartTime = performance.now();
|
|
61
|
+
this.begin = this._beginImpl.bind( this );
|
|
62
|
+
this.end = this._endImpl.bind( this );
|
|
63
|
+
this._exposeGlobal();
|
|
64
|
+
console.log( `[ProfilerService] auto-started from env (profile: ${this._profile})` );
|
|
65
|
+
|
|
66
|
+
} else {
|
|
67
|
+
|
|
68
|
+
this.begin = NOOP;
|
|
69
|
+
this.end = NOOP;
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Set the active profile. Must be called before enable() to take effect on the current session.
|
|
77
|
+
*
|
|
78
|
+
* @param {'full'|'stats'} profile
|
|
79
|
+
*/
|
|
80
|
+
setProfile( profile ) {
|
|
81
|
+
|
|
82
|
+
this._profile = profile;
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @return {'full'|'stats'}
|
|
88
|
+
*/
|
|
89
|
+
getProfile() {
|
|
90
|
+
|
|
91
|
+
return this._profile;
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
enable() {
|
|
96
|
+
|
|
97
|
+
this._clearState();
|
|
98
|
+
this._enabled = true;
|
|
99
|
+
this.begin = this._beginImpl.bind( this );
|
|
100
|
+
this.end = this._endImpl.bind( this );
|
|
101
|
+
this._exposeGlobal();
|
|
102
|
+
console.log( `[ProfilerService] enabled (profile: ${this._profile}) — call __gnsx_profiler.report() or downloadTrace() from the console` );
|
|
103
|
+
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
disable() {
|
|
107
|
+
|
|
108
|
+
this._clearState();
|
|
109
|
+
this._enabled = false;
|
|
110
|
+
this.begin = NOOP;
|
|
111
|
+
this.end = NOOP;
|
|
112
|
+
console.log( '[ProfilerService] disabled' );
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
isEnabled() {
|
|
117
|
+
|
|
118
|
+
return this._enabled;
|
|
119
|
+
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_exposeGlobal() {
|
|
123
|
+
|
|
124
|
+
if ( typeof window !== 'undefined' ) {
|
|
125
|
+
|
|
126
|
+
window.__gnsx_profiler = this;
|
|
127
|
+
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string} label
|
|
134
|
+
*/
|
|
135
|
+
_beginImpl( label ) {
|
|
136
|
+
|
|
137
|
+
const startTime = performance.now();
|
|
138
|
+
const startMark = `gnsx:${label}:start:${++ this._markId}`;
|
|
139
|
+
let stack = this.marks.get( label );
|
|
140
|
+
if ( stack === undefined ) {
|
|
141
|
+
|
|
142
|
+
stack = [];
|
|
143
|
+
this.marks.set( label, stack );
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const traced = this._profile === 'full' && this.traceEvents.length < MAX_TRACE_EVENTS;
|
|
148
|
+
if ( traced ) {
|
|
149
|
+
|
|
150
|
+
this.traceEvents.push( {
|
|
151
|
+
name: label,
|
|
152
|
+
ph: 'B',
|
|
153
|
+
ts: this._getTraceTimestamp( startTime ),
|
|
154
|
+
pid: 1,
|
|
155
|
+
tid: 1,
|
|
156
|
+
cat: 'gnsx',
|
|
157
|
+
} );
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
performance.mark( startMark );
|
|
162
|
+
stack.push( { startTime, startMark, traced } );
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {string} label
|
|
168
|
+
*/
|
|
169
|
+
_endImpl( label ) {
|
|
170
|
+
|
|
171
|
+
const stack = this.marks.get( label );
|
|
172
|
+
const mark = stack?.pop();
|
|
173
|
+
if ( mark === undefined ) return;
|
|
174
|
+
if ( stack.length === 0 ) this.marks.delete( label );
|
|
175
|
+
|
|
176
|
+
const now = performance.now();
|
|
177
|
+
const { startTime, startMark, traced } = mark;
|
|
178
|
+
const duration = now - startTime;
|
|
179
|
+
const endMark = `gnsx:${label}:end:${++ this._markId}`;
|
|
180
|
+
|
|
181
|
+
performance.mark( endMark );
|
|
182
|
+
performance.measure( `gnsx:${label}`, startMark, endMark );
|
|
183
|
+
|
|
184
|
+
let buffer = this.buffers.get( label );
|
|
185
|
+
if ( ! buffer ) {
|
|
186
|
+
|
|
187
|
+
buffer = new Float64Array( RING_SIZE );
|
|
188
|
+
this.buffers.set( label, buffer );
|
|
189
|
+
this.cursors.set( label, 0 );
|
|
190
|
+
this.counts.set( label, 0 );
|
|
191
|
+
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const cursor = this.cursors.get( label );
|
|
195
|
+
buffer[ cursor ] = duration;
|
|
196
|
+
this.cursors.set( label, ( cursor + 1 ) % RING_SIZE );
|
|
197
|
+
this.counts.set( label, Math.min( ( this.counts.get( label ) + 1 ), RING_SIZE ) );
|
|
198
|
+
|
|
199
|
+
if ( traced ) {
|
|
200
|
+
|
|
201
|
+
this.traceEvents.push( {
|
|
202
|
+
name: label,
|
|
203
|
+
ph: 'E',
|
|
204
|
+
ts: this._getTraceTimestamp( now ),
|
|
205
|
+
pid: 1,
|
|
206
|
+
tid: 1,
|
|
207
|
+
cat: 'gnsx',
|
|
208
|
+
} );
|
|
209
|
+
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {string} label
|
|
216
|
+
* @return {number[]}
|
|
217
|
+
*/
|
|
218
|
+
getValidSamples( label ) {
|
|
219
|
+
|
|
220
|
+
const buffer = this.buffers.get( label );
|
|
221
|
+
const count = this.counts.get( label ) ?? 0;
|
|
222
|
+
if ( ! buffer || count === 0 ) return [];
|
|
223
|
+
return count < RING_SIZE
|
|
224
|
+
? Array.from( buffer.subarray( 0, count ) )
|
|
225
|
+
: Array.from( buffer );
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {string} label
|
|
231
|
+
* @return {import('./ProfilerService.js').ProfilerStats|null}
|
|
232
|
+
*/
|
|
233
|
+
getStats( label ) {
|
|
234
|
+
|
|
235
|
+
const samples = this.getValidSamples( label );
|
|
236
|
+
if ( samples.length === 0 ) return null;
|
|
237
|
+
|
|
238
|
+
const sorted = [ ...samples ].sort( ( a, b ) => a - b );
|
|
239
|
+
const avg = sorted.reduce( ( a, b ) => a + b, 0 ) / sorted.length;
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
label,
|
|
243
|
+
samples: samples.length,
|
|
244
|
+
avg,
|
|
245
|
+
min: sorted[ 0 ],
|
|
246
|
+
max: sorted[ sorted.length - 1 ],
|
|
247
|
+
p95: sorted[ Math.floor( sorted.length * 0.95 ) ],
|
|
248
|
+
frameBudget: ( avg / FRAME_BUDGET_MS ) * 100,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @return {import('./ProfilerService.js').ProfilerStats[]}
|
|
255
|
+
*/
|
|
256
|
+
getAllStats() {
|
|
257
|
+
|
|
258
|
+
return [ ...this.buffers.keys() ]
|
|
259
|
+
.map( label => this.getStats( label ) )
|
|
260
|
+
.filter( s => s !== null );
|
|
261
|
+
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
report() {
|
|
265
|
+
|
|
266
|
+
const stats = this.getAllStats();
|
|
267
|
+
if ( stats.length === 0 ) {
|
|
268
|
+
|
|
269
|
+
console.log( '[ProfilerService] No data. Enable profiling first and wait a few frames.' );
|
|
270
|
+
return;
|
|
271
|
+
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
stats.sort( ( a, b ) => b.avg - a.avg );
|
|
275
|
+
console.table(
|
|
276
|
+
stats.map( s => ( {
|
|
277
|
+
label: s.label,
|
|
278
|
+
'avg ms': s.avg.toFixed( 3 ),
|
|
279
|
+
'min ms': s.min.toFixed( 3 ),
|
|
280
|
+
'max ms': s.max.toFixed( 3 ),
|
|
281
|
+
'p95 ms': s.p95.toFixed( 3 ),
|
|
282
|
+
'budget %': s.frameBudget.toFixed( 1 ) + '%',
|
|
283
|
+
samples: s.samples,
|
|
284
|
+
} ) )
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* @return {import('./ProfilerService.js').ChromeTrace}
|
|
291
|
+
*/
|
|
292
|
+
exportChromeTrace() {
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
displayTimeUnit: 'ms',
|
|
296
|
+
traceEvents: [
|
|
297
|
+
{ name: 'process_name', ph: 'M', pid: 1, args: { name: 'Genesys Profiler' } },
|
|
298
|
+
{ name: 'thread_name', ph: 'M', pid: 1, tid: 1, args: { name: 'Main Thread' } },
|
|
299
|
+
...this.traceEvents,
|
|
300
|
+
],
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @param {number} time
|
|
307
|
+
* @return {number}
|
|
308
|
+
*/
|
|
309
|
+
_getTraceTimestamp( time ) {
|
|
310
|
+
|
|
311
|
+
return ( time - this.traceStartTime ) * 1000;
|
|
312
|
+
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @param {string} [filename='gnsx-trace.json']
|
|
317
|
+
*/
|
|
318
|
+
downloadTrace( filename = 'gnsx-trace.json' ) {
|
|
319
|
+
|
|
320
|
+
if ( typeof document === 'undefined' ) {
|
|
321
|
+
|
|
322
|
+
console.log( '[ProfilerService] downloadTrace() only works in the browser. Use exportChromeTrace() in Node.js.' );
|
|
323
|
+
return;
|
|
324
|
+
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if ( this._profile !== 'full' ) {
|
|
328
|
+
|
|
329
|
+
console.log( `[ProfilerService] No trace — current profile is '${this._profile}'. Use setProfile('full') before enabling.` );
|
|
330
|
+
return;
|
|
331
|
+
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if ( this.traceEvents.length === 0 ) {
|
|
335
|
+
|
|
336
|
+
console.log( '[ProfilerService] No trace events. Enable the profiler and play for a few seconds first.' );
|
|
337
|
+
return;
|
|
338
|
+
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const json = JSON.stringify( this.exportChromeTrace() );
|
|
342
|
+
const blob = new Blob( [ json ], { type: 'application/json' } );
|
|
343
|
+
const url = URL.createObjectURL( blob );
|
|
344
|
+
const a = document.createElement( 'a' );
|
|
345
|
+
a.href = url;
|
|
346
|
+
a.download = filename;
|
|
347
|
+
a.click();
|
|
348
|
+
URL.revokeObjectURL( url );
|
|
349
|
+
console.log( `[ProfilerService] Trace downloaded: ${filename} (${this.traceEvents.length} events)` );
|
|
350
|
+
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* @return {import('./ProfilerService.js').ProfilerStats[]}
|
|
355
|
+
*/
|
|
356
|
+
exportJSON() {
|
|
357
|
+
|
|
358
|
+
return this.getAllStats();
|
|
359
|
+
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
_clearState() {
|
|
363
|
+
|
|
364
|
+
this.buffers.clear();
|
|
365
|
+
this.cursors.clear();
|
|
366
|
+
this.counts.clear();
|
|
367
|
+
this.marks.clear();
|
|
368
|
+
this.traceEvents = [];
|
|
369
|
+
this.traceStartTime = performance.now();
|
|
370
|
+
this._markId = 0;
|
|
371
|
+
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
reset() {
|
|
375
|
+
|
|
376
|
+
this._clearState();
|
|
377
|
+
console.log( '[ProfilerService] data reset' );
|
|
378
|
+
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* @typedef {Object} ProfilerStats
|
|
385
|
+
* @property {string} label
|
|
386
|
+
* @property {number} samples
|
|
387
|
+
* @property {number} avg
|
|
388
|
+
* @property {number} min
|
|
389
|
+
* @property {number} max
|
|
390
|
+
* @property {number} p95
|
|
391
|
+
* @property {number} frameBudget Percentage of a 60 fps frame budget (16.67 ms)
|
|
392
|
+
*/
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* @typedef {Object} ChromeTraceEvent
|
|
396
|
+
* @property {string} name
|
|
397
|
+
* @property {'B'|'E'} ph
|
|
398
|
+
* @property {number} ts
|
|
399
|
+
* @property {1} pid
|
|
400
|
+
* @property {1} tid
|
|
401
|
+
* @property {'gnsx'} cat
|
|
402
|
+
*/
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* @typedef {Object} ChromeTraceMetadataEvent
|
|
406
|
+
* @property {'process_name'|'thread_name'} name
|
|
407
|
+
* @property {'M'} ph
|
|
408
|
+
* @property {1} pid
|
|
409
|
+
* @property {1} [tid]
|
|
410
|
+
* @property {{ name: string }} args
|
|
411
|
+
*/
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* @typedef {Object} ChromeTrace
|
|
415
|
+
* @property {'ms'} displayTimeUnit
|
|
416
|
+
* @property {Array<ChromeTraceEvent|ChromeTraceMetadataEvent>} traceEvents
|
|
417
|
+
*/
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* @typedef {'full'|'stats'} ProfilingProfile
|
|
421
|
+
*/
|
|
422
|
+
|
|
423
|
+
const ProfilerService = new ProfilerServiceClass();
|
|
424
|
+
|
|
425
|
+
function applyProfileToMethod( label, descriptor ) {
|
|
426
|
+
|
|
427
|
+
if ( _readProfilingEnv() === false ) return descriptor;
|
|
428
|
+
|
|
429
|
+
const original = descriptor.value;
|
|
430
|
+
|
|
431
|
+
function profiled( ...args ) {
|
|
432
|
+
|
|
433
|
+
ProfilerService.begin( label );
|
|
434
|
+
try {
|
|
435
|
+
|
|
436
|
+
const result = original.apply( this, args );
|
|
437
|
+
if ( result instanceof Promise ) {
|
|
438
|
+
|
|
439
|
+
return result.finally( () => ProfilerService.end( label ) );
|
|
440
|
+
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
ProfilerService.end( label );
|
|
444
|
+
return result;
|
|
445
|
+
|
|
446
|
+
} catch ( error ) {
|
|
447
|
+
|
|
448
|
+
ProfilerService.end( label );
|
|
449
|
+
throw error;
|
|
450
|
+
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
descriptor.value = profiled;
|
|
456
|
+
return descriptor;
|
|
457
|
+
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Method decorator that profiles the decorated method.
|
|
462
|
+
* The label is automatically set to `ClassName.methodName`.
|
|
463
|
+
*
|
|
464
|
+
* @param {Object} target
|
|
465
|
+
* @param {string|symbol} propertyKey
|
|
466
|
+
* @param {PropertyDescriptor} descriptor
|
|
467
|
+
* @return {PropertyDescriptor}
|
|
468
|
+
*/
|
|
469
|
+
function profile( target, propertyKey, descriptor ) {
|
|
470
|
+
|
|
471
|
+
const className = target.constructor?.name ?? 'Unknown';
|
|
472
|
+
return applyProfileToMethod( `${className}.${String( propertyKey )}`, descriptor );
|
|
473
|
+
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Class decorator that profiles every method of the decorated class.
|
|
478
|
+
*
|
|
479
|
+
* @template T
|
|
480
|
+
* @param {T} constructor
|
|
481
|
+
* @return {T}
|
|
482
|
+
*/
|
|
483
|
+
function profileClass( constructor ) {
|
|
484
|
+
|
|
485
|
+
const proto = constructor.prototype;
|
|
486
|
+
for ( const key of Object.getOwnPropertyNames( proto ) ) {
|
|
487
|
+
|
|
488
|
+
if ( key === 'constructor' ) continue;
|
|
489
|
+
const descriptor = Object.getOwnPropertyDescriptor( proto, key );
|
|
490
|
+
if ( ! descriptor || typeof descriptor.value !== 'function' ) continue;
|
|
491
|
+
Object.defineProperty(
|
|
492
|
+
proto,
|
|
493
|
+
key,
|
|
494
|
+
applyProfileToMethod( `${constructor.name}.${key}`, { ...descriptor } )
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return constructor;
|
|
500
|
+
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export { ProfilerService, profile, profileClass };
|
|
504
|
+
// !WITH_GENESYS
|