@gnsx/three 0.184.10 → 0.184.12
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 +433 -0
- package/build/three.module.js +431 -1
- package/build/three.module.min.js +1 -1
- package/examples/jsm/helpers/LightProbeGridCubeMapHelper.js +92 -7
- package/examples/jsm/lighting/LightProbeGrid.js +12 -1
- package/package.json +1 -1
- package/src/Three.js +3 -0
- package/src/profiler/ProfilerService.js +431 -0
|
@@ -7,9 +7,14 @@ import {
|
|
|
7
7
|
} from 'three';
|
|
8
8
|
import { NodeMaterial } from 'three/webgpu';
|
|
9
9
|
import {
|
|
10
|
+
abs,
|
|
10
11
|
cubeTexture,
|
|
11
12
|
Fn,
|
|
12
|
-
|
|
13
|
+
max,
|
|
14
|
+
normalLocal,
|
|
15
|
+
positionLocal,
|
|
16
|
+
select,
|
|
17
|
+
vec3,
|
|
13
18
|
vec4
|
|
14
19
|
} from 'three/tsl';
|
|
15
20
|
|
|
@@ -17,7 +22,41 @@ function createNodeMaterial( texture ) {
|
|
|
17
22
|
|
|
18
23
|
const fragmentNode = Fn( () => {
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
// Get local position and normal
|
|
26
|
+
const localPos = positionLocal;
|
|
27
|
+
const localNormal = normalLocal;
|
|
28
|
+
|
|
29
|
+
// Get absolute normal components to determine dominant axis
|
|
30
|
+
const absNormal = abs( localNormal );
|
|
31
|
+
const maxComp = max( max( absNormal.x, absNormal.y ), absNormal.z );
|
|
32
|
+
|
|
33
|
+
// Determine which face we're on and compute sample direction
|
|
34
|
+
const isX = maxComp.equal( absNormal.x );
|
|
35
|
+
const isY = maxComp.equal( absNormal.y ).and( isX.not() );
|
|
36
|
+
|
|
37
|
+
// Match LightProbeGrid / WebGL cubemap face conventions (see LightProbeGrid.js SH projection).
|
|
38
|
+
const xPositive = localNormal.x.greaterThan( 0.0 );
|
|
39
|
+
const xDir = select( xPositive,
|
|
40
|
+
vec3( 1.0, localPos.y, localPos.z.negate() ).normalize(),
|
|
41
|
+
vec3( - 1.0, localPos.y, localPos.z ).normalize()
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const yPositive = localNormal.y.greaterThan( 0.0 );
|
|
45
|
+
const yDir = select( yPositive,
|
|
46
|
+
vec3( localPos.x, 1.0, localPos.z.negate() ).normalize(),
|
|
47
|
+
vec3( localPos.x, - 1.0, localPos.z ).normalize()
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const zPositive = localNormal.z.greaterThan( 0.0 );
|
|
51
|
+
const zDir = select( zPositive,
|
|
52
|
+
vec3( localPos.x, localPos.y, 1.0 ).normalize(),
|
|
53
|
+
vec3( localPos.x, localPos.y, - 1.0 ).normalize()
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Select the appropriate direction based on which axis is dominant
|
|
57
|
+
const sampleDir = select( isX, xDir, select( isY, yDir, zDir ) );
|
|
58
|
+
|
|
59
|
+
return vec4( cubeTexture( texture, sampleDir ).rgb, 1.0 );
|
|
21
60
|
|
|
22
61
|
} )();
|
|
23
62
|
|
|
@@ -40,11 +79,13 @@ function createShaderMaterial( texture ) {
|
|
|
40
79
|
|
|
41
80
|
vertexShader: /* glsl */`
|
|
42
81
|
|
|
43
|
-
varying vec3
|
|
82
|
+
varying vec3 vLocalPosition;
|
|
83
|
+
varying vec3 vNormal;
|
|
44
84
|
|
|
45
85
|
void main() {
|
|
46
86
|
|
|
47
|
-
|
|
87
|
+
vLocalPosition = position;
|
|
88
|
+
vNormal = normal;
|
|
48
89
|
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
|
|
49
90
|
|
|
50
91
|
}
|
|
@@ -57,12 +98,56 @@ function createShaderMaterial( texture ) {
|
|
|
57
98
|
|
|
58
99
|
uniform samplerCube cubeTexture;
|
|
59
100
|
|
|
60
|
-
varying vec3
|
|
101
|
+
varying vec3 vLocalPosition;
|
|
102
|
+
varying vec3 vNormal;
|
|
61
103
|
|
|
62
104
|
void main() {
|
|
105
|
+
// Get the dominant axis from the normal to determine which face we're on
|
|
106
|
+
vec3 absNormal = abs( vNormal );
|
|
107
|
+
float maxComp = max( max( absNormal.x, absNormal.y ), absNormal.z );
|
|
108
|
+
|
|
109
|
+
vec3 sampleDir;
|
|
110
|
+
|
|
111
|
+
// Match LightProbeGrid / WebGL cubemap face conventions (see LightProbeGrid.js SH projection).
|
|
112
|
+
if ( maxComp == absNormal.x ) {
|
|
113
|
+
|
|
114
|
+
if ( vNormal.x > 0.0 ) {
|
|
115
|
+
|
|
116
|
+
sampleDir = normalize( vec3( 1.0, vLocalPosition.y, -vLocalPosition.z ) );
|
|
117
|
+
|
|
118
|
+
} else {
|
|
119
|
+
|
|
120
|
+
sampleDir = normalize( vec3( -1.0, vLocalPosition.y, vLocalPosition.z ) );
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
} else if ( maxComp == absNormal.y ) {
|
|
125
|
+
|
|
126
|
+
if ( vNormal.y > 0.0 ) {
|
|
127
|
+
|
|
128
|
+
sampleDir = normalize( vec3( vLocalPosition.x, 1.0, -vLocalPosition.z ) );
|
|
129
|
+
|
|
130
|
+
} else {
|
|
131
|
+
|
|
132
|
+
sampleDir = normalize( vec3( vLocalPosition.x, -1.0, vLocalPosition.z ) );
|
|
133
|
+
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
} else {
|
|
137
|
+
|
|
138
|
+
if ( vNormal.z > 0.0 ) {
|
|
139
|
+
|
|
140
|
+
sampleDir = normalize( vec3( vLocalPosition.x, vLocalPosition.y, 1.0 ) );
|
|
141
|
+
|
|
142
|
+
} else {
|
|
143
|
+
|
|
144
|
+
sampleDir = normalize( vec3( vLocalPosition.x, vLocalPosition.y, -1.0 ) );
|
|
145
|
+
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
}
|
|
63
149
|
|
|
64
|
-
vec3
|
|
65
|
-
vec3 color = textureCube( cubeTexture, direction ).rgb;
|
|
150
|
+
vec3 color = textureCube( cubeTexture, sampleDir ).rgb;
|
|
66
151
|
|
|
67
152
|
gl_FragColor = vec4( color, 1.0 );
|
|
68
153
|
|
|
@@ -238,7 +238,11 @@ class LightProbeGrid extends Object3D {
|
|
|
238
238
|
*/
|
|
239
239
|
getProbePosition( ix, iy, iz, target ) {
|
|
240
240
|
|
|
241
|
-
const pos = this.position;
|
|
241
|
+
// const pos = this.position;
|
|
242
|
+
// WITH_GENESYS
|
|
243
|
+
const pos = _position;
|
|
244
|
+
this.getWorldPosition( pos );
|
|
245
|
+
// !WITH_GENESYS
|
|
242
246
|
const res = this.resolution;
|
|
243
247
|
const w = this.width, h = this.height, d = this.depth;
|
|
244
248
|
|
|
@@ -257,8 +261,15 @@ class LightProbeGrid extends Object3D {
|
|
|
257
261
|
*/
|
|
258
262
|
updateBoundingBox() {
|
|
259
263
|
|
|
264
|
+
// WITH_GENESYS
|
|
265
|
+
this.getWorldPosition( _position );
|
|
266
|
+
_size.set( this.width, this.height, this.depth );
|
|
267
|
+
this.boundingBox.setFromCenterAndSize( _position, _size );
|
|
268
|
+
// !WITH_GENESYS
|
|
269
|
+
/*
|
|
260
270
|
_size.set( this.width, this.height, this.depth );
|
|
261
271
|
this.boundingBox.setFromCenterAndSize( this.position, _size );
|
|
272
|
+
*/
|
|
262
273
|
|
|
263
274
|
}
|
|
264
275
|
|
package/package.json
CHANGED
package/src/Three.js
CHANGED
|
@@ -8,3 +8,6 @@ export { UniformsUtils } from './renderers/shaders/UniformsUtils.js';
|
|
|
8
8
|
export { ShaderChunk } from './renderers/shaders/ShaderChunk.js';
|
|
9
9
|
export { PMREMGenerator } from './extras/PMREMGenerator.js';
|
|
10
10
|
export { WebGLUtils } from './renderers/webgl/WebGLUtils.js';
|
|
11
|
+
// WITH_GENESYS
|
|
12
|
+
export { ProfilerService, profile, profileClass } from './profiler/ProfilerService.js';
|
|
13
|
+
// !WITH_GENESYS
|
|
@@ -0,0 +1,431 @@
|
|
|
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, number>} */
|
|
52
|
+
this.marks = new Map();
|
|
53
|
+
/** @type {Array<{ name: string, ph: 'X', ts: number, dur: number, pid: 1, tid: 1, cat: 'gnsx' }>} */
|
|
54
|
+
this.traceEvents = [];
|
|
55
|
+
this.traceStartTime = 0;
|
|
56
|
+
|
|
57
|
+
if ( this._enabled ) {
|
|
58
|
+
|
|
59
|
+
this.traceStartTime = performance.now();
|
|
60
|
+
this.begin = this._beginImpl.bind( this );
|
|
61
|
+
this.end = this._endImpl.bind( this );
|
|
62
|
+
this._exposeGlobal();
|
|
63
|
+
console.log( `[ProfilerService] auto-started from env (profile: ${this._profile})` );
|
|
64
|
+
|
|
65
|
+
} else {
|
|
66
|
+
|
|
67
|
+
this.begin = NOOP;
|
|
68
|
+
this.end = NOOP;
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set the active profile. Must be called before enable() to take effect on the current session.
|
|
76
|
+
*
|
|
77
|
+
* @param {'full'|'stats'} profile
|
|
78
|
+
*/
|
|
79
|
+
setProfile( profile ) {
|
|
80
|
+
|
|
81
|
+
this._profile = profile;
|
|
82
|
+
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @return {'full'|'stats'}
|
|
87
|
+
*/
|
|
88
|
+
getProfile() {
|
|
89
|
+
|
|
90
|
+
return this._profile;
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
enable() {
|
|
95
|
+
|
|
96
|
+
this._clearState();
|
|
97
|
+
this._enabled = true;
|
|
98
|
+
this.begin = this._beginImpl.bind( this );
|
|
99
|
+
this.end = this._endImpl.bind( this );
|
|
100
|
+
this._exposeGlobal();
|
|
101
|
+
console.log( `[ProfilerService] enabled (profile: ${this._profile}) — call __gnsx_profiler.report() or downloadTrace() from the console` );
|
|
102
|
+
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
disable() {
|
|
106
|
+
|
|
107
|
+
this._clearState();
|
|
108
|
+
this._enabled = false;
|
|
109
|
+
this.begin = NOOP;
|
|
110
|
+
this.end = NOOP;
|
|
111
|
+
console.log( '[ProfilerService] disabled' );
|
|
112
|
+
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
isEnabled() {
|
|
116
|
+
|
|
117
|
+
return this._enabled;
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_exposeGlobal() {
|
|
122
|
+
|
|
123
|
+
if ( typeof window !== 'undefined' ) {
|
|
124
|
+
|
|
125
|
+
window.__gnsx_profiler = this;
|
|
126
|
+
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} label
|
|
133
|
+
*/
|
|
134
|
+
_beginImpl( label ) {
|
|
135
|
+
|
|
136
|
+
performance.mark( `gnsx:${label}:start` );
|
|
137
|
+
this.marks.set( label, performance.now() );
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} label
|
|
143
|
+
*/
|
|
144
|
+
_endImpl( label ) {
|
|
145
|
+
|
|
146
|
+
const startTime = this.marks.get( label );
|
|
147
|
+
if ( startTime === undefined ) return;
|
|
148
|
+
this.marks.delete( label );
|
|
149
|
+
|
|
150
|
+
const now = performance.now();
|
|
151
|
+
const duration = now - startTime;
|
|
152
|
+
|
|
153
|
+
performance.mark( `gnsx:${label}:end` );
|
|
154
|
+
performance.measure( `gnsx:${label}`, `gnsx:${label}:start`, `gnsx:${label}:end` );
|
|
155
|
+
|
|
156
|
+
let buffer = this.buffers.get( label );
|
|
157
|
+
if ( ! buffer ) {
|
|
158
|
+
|
|
159
|
+
buffer = new Float64Array( RING_SIZE );
|
|
160
|
+
this.buffers.set( label, buffer );
|
|
161
|
+
this.cursors.set( label, 0 );
|
|
162
|
+
this.counts.set( label, 0 );
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const cursor = this.cursors.get( label );
|
|
167
|
+
buffer[ cursor ] = duration;
|
|
168
|
+
this.cursors.set( label, ( cursor + 1 ) % RING_SIZE );
|
|
169
|
+
this.counts.set( label, Math.min( ( this.counts.get( label ) + 1 ), RING_SIZE ) );
|
|
170
|
+
|
|
171
|
+
if ( this._profile === 'full' && this.traceEvents.length < MAX_TRACE_EVENTS ) {
|
|
172
|
+
|
|
173
|
+
this.traceEvents.push( {
|
|
174
|
+
name: label,
|
|
175
|
+
ph: 'X',
|
|
176
|
+
ts: Math.round( ( startTime - this.traceStartTime ) * 1000 ),
|
|
177
|
+
dur: Math.max( 1, Math.round( duration * 1000 ) ),
|
|
178
|
+
pid: 1,
|
|
179
|
+
tid: 1,
|
|
180
|
+
cat: 'gnsx',
|
|
181
|
+
} );
|
|
182
|
+
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {string} label
|
|
189
|
+
* @return {number[]}
|
|
190
|
+
*/
|
|
191
|
+
getValidSamples( label ) {
|
|
192
|
+
|
|
193
|
+
const buffer = this.buffers.get( label );
|
|
194
|
+
const count = this.counts.get( label ) ?? 0;
|
|
195
|
+
if ( ! buffer || count === 0 ) return [];
|
|
196
|
+
return count < RING_SIZE
|
|
197
|
+
? Array.from( buffer.subarray( 0, count ) )
|
|
198
|
+
: Array.from( buffer );
|
|
199
|
+
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {string} label
|
|
204
|
+
* @return {import('./ProfilerService.js').ProfilerStats|null}
|
|
205
|
+
*/
|
|
206
|
+
getStats( label ) {
|
|
207
|
+
|
|
208
|
+
const samples = this.getValidSamples( label );
|
|
209
|
+
if ( samples.length === 0 ) return null;
|
|
210
|
+
|
|
211
|
+
const sorted = [ ...samples ].sort( ( a, b ) => a - b );
|
|
212
|
+
const avg = sorted.reduce( ( a, b ) => a + b, 0 ) / sorted.length;
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
label,
|
|
216
|
+
samples: samples.length,
|
|
217
|
+
avg,
|
|
218
|
+
min: sorted[ 0 ],
|
|
219
|
+
max: sorted[ sorted.length - 1 ],
|
|
220
|
+
p95: sorted[ Math.floor( sorted.length * 0.95 ) ],
|
|
221
|
+
frameBudget: ( avg / FRAME_BUDGET_MS ) * 100,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @return {import('./ProfilerService.js').ProfilerStats[]}
|
|
228
|
+
*/
|
|
229
|
+
getAllStats() {
|
|
230
|
+
|
|
231
|
+
return [ ...this.buffers.keys() ]
|
|
232
|
+
.map( label => this.getStats( label ) )
|
|
233
|
+
.filter( s => s !== null );
|
|
234
|
+
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
report() {
|
|
238
|
+
|
|
239
|
+
const stats = this.getAllStats();
|
|
240
|
+
if ( stats.length === 0 ) {
|
|
241
|
+
|
|
242
|
+
console.log( '[ProfilerService] No data. Enable profiling first and wait a few frames.' );
|
|
243
|
+
return;
|
|
244
|
+
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
stats.sort( ( a, b ) => b.avg - a.avg );
|
|
248
|
+
console.table(
|
|
249
|
+
stats.map( s => ( {
|
|
250
|
+
label: s.label,
|
|
251
|
+
'avg ms': s.avg.toFixed( 3 ),
|
|
252
|
+
'min ms': s.min.toFixed( 3 ),
|
|
253
|
+
'max ms': s.max.toFixed( 3 ),
|
|
254
|
+
'p95 ms': s.p95.toFixed( 3 ),
|
|
255
|
+
'budget %': s.frameBudget.toFixed( 1 ) + '%',
|
|
256
|
+
samples: s.samples,
|
|
257
|
+
} ) )
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @return {import('./ProfilerService.js').ChromeTrace}
|
|
264
|
+
*/
|
|
265
|
+
exportChromeTrace() {
|
|
266
|
+
|
|
267
|
+
return { traceEvents: [ ...this.traceEvents ] };
|
|
268
|
+
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @param {string} [filename='gnsx-trace.json']
|
|
273
|
+
*/
|
|
274
|
+
downloadTrace( filename = 'gnsx-trace.json' ) {
|
|
275
|
+
|
|
276
|
+
if ( typeof document === 'undefined' ) {
|
|
277
|
+
|
|
278
|
+
console.log( '[ProfilerService] downloadTrace() only works in the browser. Use exportChromeTrace() in Node.js.' );
|
|
279
|
+
return;
|
|
280
|
+
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if ( this._profile !== 'full' ) {
|
|
284
|
+
|
|
285
|
+
console.log( `[ProfilerService] No trace — current profile is '${this._profile}'. Use setProfile('full') before enabling.` );
|
|
286
|
+
return;
|
|
287
|
+
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if ( this.traceEvents.length === 0 ) {
|
|
291
|
+
|
|
292
|
+
console.log( '[ProfilerService] No trace events. Enable the profiler and play for a few seconds first.' );
|
|
293
|
+
return;
|
|
294
|
+
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const json = JSON.stringify( this.exportChromeTrace() );
|
|
298
|
+
const blob = new Blob( [ json ], { type: 'application/json' } );
|
|
299
|
+
const url = URL.createObjectURL( blob );
|
|
300
|
+
const a = document.createElement( 'a' );
|
|
301
|
+
a.href = url;
|
|
302
|
+
a.download = filename;
|
|
303
|
+
a.click();
|
|
304
|
+
URL.revokeObjectURL( url );
|
|
305
|
+
console.log( `[ProfilerService] Trace downloaded: ${filename} (${this.traceEvents.length} events)` );
|
|
306
|
+
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @return {import('./ProfilerService.js').ProfilerStats[]}
|
|
311
|
+
*/
|
|
312
|
+
exportJSON() {
|
|
313
|
+
|
|
314
|
+
return this.getAllStats();
|
|
315
|
+
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
_clearState() {
|
|
319
|
+
|
|
320
|
+
this.buffers.clear();
|
|
321
|
+
this.cursors.clear();
|
|
322
|
+
this.counts.clear();
|
|
323
|
+
this.marks.clear();
|
|
324
|
+
this.traceEvents = [];
|
|
325
|
+
this.traceStartTime = performance.now();
|
|
326
|
+
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
reset() {
|
|
330
|
+
|
|
331
|
+
this._clearState();
|
|
332
|
+
console.log( '[ProfilerService] data reset' );
|
|
333
|
+
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* @typedef {Object} ProfilerStats
|
|
340
|
+
* @property {string} label
|
|
341
|
+
* @property {number} samples
|
|
342
|
+
* @property {number} avg
|
|
343
|
+
* @property {number} min
|
|
344
|
+
* @property {number} max
|
|
345
|
+
* @property {number} p95
|
|
346
|
+
* @property {number} frameBudget Percentage of a 60 fps frame budget (16.67 ms)
|
|
347
|
+
*/
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* @typedef {Object} ChromeTrace
|
|
351
|
+
* @property {Array<{ name: string, ph: 'X', ts: number, dur: number, pid: 1, tid: 1, cat: 'gnsx' }>} traceEvents
|
|
352
|
+
* @property {{ 'clock-offset-since-epoch-ns'?: number }|undefined} [metadata]
|
|
353
|
+
*/
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* @typedef {'full'|'stats'} ProfilingProfile
|
|
357
|
+
*/
|
|
358
|
+
|
|
359
|
+
const ProfilerService = new ProfilerServiceClass();
|
|
360
|
+
|
|
361
|
+
function applyProfileToMethod( label, descriptor ) {
|
|
362
|
+
|
|
363
|
+
if ( _readProfilingEnv() === false ) return descriptor;
|
|
364
|
+
|
|
365
|
+
const original = descriptor.value;
|
|
366
|
+
|
|
367
|
+
function profiled( ...args ) {
|
|
368
|
+
|
|
369
|
+
ProfilerService.begin( label );
|
|
370
|
+
const result = original.apply( this, args );
|
|
371
|
+
if ( result instanceof Promise ) {
|
|
372
|
+
|
|
373
|
+
return result.finally( () => ProfilerService.end( label ) );
|
|
374
|
+
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
ProfilerService.end( label );
|
|
378
|
+
return result;
|
|
379
|
+
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
descriptor.value = profiled;
|
|
383
|
+
return descriptor;
|
|
384
|
+
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Method decorator that profiles the decorated method.
|
|
389
|
+
* The label is automatically set to `ClassName.methodName`.
|
|
390
|
+
*
|
|
391
|
+
* @param {Object} target
|
|
392
|
+
* @param {string|symbol} propertyKey
|
|
393
|
+
* @param {PropertyDescriptor} descriptor
|
|
394
|
+
* @return {PropertyDescriptor}
|
|
395
|
+
*/
|
|
396
|
+
function profile( target, propertyKey, descriptor ) {
|
|
397
|
+
|
|
398
|
+
const className = target.constructor?.name ?? 'Unknown';
|
|
399
|
+
return applyProfileToMethod( `${className}.${String( propertyKey )}`, descriptor );
|
|
400
|
+
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Class decorator that profiles every method of the decorated class.
|
|
405
|
+
*
|
|
406
|
+
* @template T
|
|
407
|
+
* @param {T} constructor
|
|
408
|
+
* @return {T}
|
|
409
|
+
*/
|
|
410
|
+
function profileClass( constructor ) {
|
|
411
|
+
|
|
412
|
+
const proto = constructor.prototype;
|
|
413
|
+
for ( const key of Object.getOwnPropertyNames( proto ) ) {
|
|
414
|
+
|
|
415
|
+
if ( key === 'constructor' ) continue;
|
|
416
|
+
const descriptor = Object.getOwnPropertyDescriptor( proto, key );
|
|
417
|
+
if ( ! descriptor || typeof descriptor.value !== 'function' ) continue;
|
|
418
|
+
Object.defineProperty(
|
|
419
|
+
proto,
|
|
420
|
+
key,
|
|
421
|
+
applyProfileToMethod( `${constructor.name}.${key}`, { ...descriptor } )
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return constructor;
|
|
427
|
+
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export { ProfilerService, profile, profileClass };
|
|
431
|
+
// !WITH_GENESYS
|