@gnsx/three 0.184.11 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gnsx/three",
3
- "version": "0.184.11",
3
+ "version": "0.184.12",
4
4
  "description": "JavaScript 3D library",
5
5
  "type": "module",
6
6
  "main": "./build/three.cjs",
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