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