@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/build/three.cjs CHANGED
@@ -79252,6 +79252,436 @@ class WebGLRenderer {
79252
79252
 
79253
79253
  }
79254
79254
 
79255
+ // WITH_GENESYS
79256
+ /**
79257
+ * ProfilerService — per-label CPU timing with ring-buffer aggregation and DevTools integration.
79258
+ *
79259
+ * Activate via environment variable:
79260
+ * PROFILING=1 stats + trace (full profile)
79261
+ * PROFILING=stats stats only, no trace accumulation (lower overhead)
79262
+ *
79263
+ * Quick start (browser console):
79264
+ * __gnsx_profiler.enable()
79265
+ * // play for a few seconds
79266
+ * __gnsx_profiler.report() // sorted console.table (aggregated stats)
79267
+ * __gnsx_profiler.downloadTrace() // download gnsx-trace.json for Speedscope / Perfetto
79268
+ * __gnsx_profiler.reset() // clear samples and trace
79269
+ * __gnsx_profiler.disable()
79270
+ */
79271
+
79272
+ const RING_SIZE = 120;
79273
+ const FRAME_BUDGET_MS = 1000 / 60;
79274
+ /** Max events in the trace log (~50s at 60fps with 8 labels). */
79275
+ const MAX_TRACE_EVENTS = 50_000;
79276
+ const NOOP = () => {};
79277
+
79278
+ function _readProfilingEnv() {
79279
+
79280
+ const raw =
79281
+ ( typeof process !== 'undefined' ? process.env?.[ 'PROFILING' ] : undefined ) ??
79282
+ ( typeof globalThis !== 'undefined' ? globalThis[ '__gnsx_env__' ]?.[ 'PROFILING' ] : undefined ) ??
79283
+ ( typeof globalThis !== 'undefined' ? globalThis[ '__GNSX_PROFILING__' ] : undefined );
79284
+
79285
+ if ( ! raw || raw === '0' || raw === 'false' ) return false;
79286
+ if ( raw === 'stats' ) return 'stats';
79287
+ return 'full';
79288
+
79289
+ }
79290
+
79291
+ class ProfilerServiceClass {
79292
+
79293
+ constructor() {
79294
+
79295
+ const envProfile = _readProfilingEnv();
79296
+ this._profile = envProfile !== false ? envProfile : 'full';
79297
+ this._enabled = envProfile !== false;
79298
+
79299
+ /** @type {Map<string, Float64Array>} */
79300
+ this.buffers = new Map();
79301
+ /** @type {Map<string, number>} */
79302
+ this.cursors = new Map();
79303
+ /** @type {Map<string, number>} */
79304
+ this.counts = new Map();
79305
+ /** @type {Map<string, number>} */
79306
+ this.marks = new Map();
79307
+ /** @type {Array<{ name: string, ph: 'X', ts: number, dur: number, pid: 1, tid: 1, cat: 'gnsx' }>} */
79308
+ this.traceEvents = [];
79309
+ this.traceStartTime = 0;
79310
+
79311
+ if ( this._enabled ) {
79312
+
79313
+ this.traceStartTime = performance.now();
79314
+ this.begin = this._beginImpl.bind( this );
79315
+ this.end = this._endImpl.bind( this );
79316
+ this._exposeGlobal();
79317
+ console.log( `[ProfilerService] auto-started from env (profile: ${this._profile})` );
79318
+
79319
+ } else {
79320
+
79321
+ this.begin = NOOP;
79322
+ this.end = NOOP;
79323
+
79324
+ }
79325
+
79326
+ }
79327
+
79328
+ /**
79329
+ * Set the active profile. Must be called before enable() to take effect on the current session.
79330
+ *
79331
+ * @param {'full'|'stats'} profile
79332
+ */
79333
+ setProfile( profile ) {
79334
+
79335
+ this._profile = profile;
79336
+
79337
+ }
79338
+
79339
+ /**
79340
+ * @return {'full'|'stats'}
79341
+ */
79342
+ getProfile() {
79343
+
79344
+ return this._profile;
79345
+
79346
+ }
79347
+
79348
+ enable() {
79349
+
79350
+ this._clearState();
79351
+ this._enabled = true;
79352
+ this.begin = this._beginImpl.bind( this );
79353
+ this.end = this._endImpl.bind( this );
79354
+ this._exposeGlobal();
79355
+ console.log( `[ProfilerService] enabled (profile: ${this._profile}) — call __gnsx_profiler.report() or downloadTrace() from the console` );
79356
+
79357
+ }
79358
+
79359
+ disable() {
79360
+
79361
+ this._clearState();
79362
+ this._enabled = false;
79363
+ this.begin = NOOP;
79364
+ this.end = NOOP;
79365
+ console.log( '[ProfilerService] disabled' );
79366
+
79367
+ }
79368
+
79369
+ isEnabled() {
79370
+
79371
+ return this._enabled;
79372
+
79373
+ }
79374
+
79375
+ _exposeGlobal() {
79376
+
79377
+ if ( typeof window !== 'undefined' ) {
79378
+
79379
+ window.__gnsx_profiler = this;
79380
+
79381
+ }
79382
+
79383
+ }
79384
+
79385
+ /**
79386
+ * @param {string} label
79387
+ */
79388
+ _beginImpl( label ) {
79389
+
79390
+ performance.mark( `gnsx:${label}:start` );
79391
+ this.marks.set( label, performance.now() );
79392
+
79393
+ }
79394
+
79395
+ /**
79396
+ * @param {string} label
79397
+ */
79398
+ _endImpl( label ) {
79399
+
79400
+ const startTime = this.marks.get( label );
79401
+ if ( startTime === undefined ) return;
79402
+ this.marks.delete( label );
79403
+
79404
+ const now = performance.now();
79405
+ const duration = now - startTime;
79406
+
79407
+ performance.mark( `gnsx:${label}:end` );
79408
+ performance.measure( `gnsx:${label}`, `gnsx:${label}:start`, `gnsx:${label}:end` );
79409
+
79410
+ let buffer = this.buffers.get( label );
79411
+ if ( ! buffer ) {
79412
+
79413
+ buffer = new Float64Array( RING_SIZE );
79414
+ this.buffers.set( label, buffer );
79415
+ this.cursors.set( label, 0 );
79416
+ this.counts.set( label, 0 );
79417
+
79418
+ }
79419
+
79420
+ const cursor = this.cursors.get( label );
79421
+ buffer[ cursor ] = duration;
79422
+ this.cursors.set( label, ( cursor + 1 ) % RING_SIZE );
79423
+ this.counts.set( label, Math.min( ( this.counts.get( label ) + 1 ), RING_SIZE ) );
79424
+
79425
+ if ( this._profile === 'full' && this.traceEvents.length < MAX_TRACE_EVENTS ) {
79426
+
79427
+ this.traceEvents.push( {
79428
+ name: label,
79429
+ ph: 'X',
79430
+ ts: Math.round( ( startTime - this.traceStartTime ) * 1000 ),
79431
+ dur: Math.max( 1, Math.round( duration * 1000 ) ),
79432
+ pid: 1,
79433
+ tid: 1,
79434
+ cat: 'gnsx',
79435
+ } );
79436
+
79437
+ }
79438
+
79439
+ }
79440
+
79441
+ /**
79442
+ * @param {string} label
79443
+ * @return {number[]}
79444
+ */
79445
+ getValidSamples( label ) {
79446
+
79447
+ const buffer = this.buffers.get( label );
79448
+ const count = this.counts.get( label ) ?? 0;
79449
+ if ( ! buffer || count === 0 ) return [];
79450
+ return count < RING_SIZE
79451
+ ? Array.from( buffer.subarray( 0, count ) )
79452
+ : Array.from( buffer );
79453
+
79454
+ }
79455
+
79456
+ /**
79457
+ * @param {string} label
79458
+ * @return {import('./ProfilerService.js').ProfilerStats|null}
79459
+ */
79460
+ getStats( label ) {
79461
+
79462
+ const samples = this.getValidSamples( label );
79463
+ if ( samples.length === 0 ) return null;
79464
+
79465
+ const sorted = [ ...samples ].sort( ( a, b ) => a - b );
79466
+ const avg = sorted.reduce( ( a, b ) => a + b, 0 ) / sorted.length;
79467
+
79468
+ return {
79469
+ label,
79470
+ samples: samples.length,
79471
+ avg,
79472
+ min: sorted[ 0 ],
79473
+ max: sorted[ sorted.length - 1 ],
79474
+ p95: sorted[ Math.floor( sorted.length * 0.95 ) ],
79475
+ frameBudget: ( avg / FRAME_BUDGET_MS ) * 100,
79476
+ };
79477
+
79478
+ }
79479
+
79480
+ /**
79481
+ * @return {import('./ProfilerService.js').ProfilerStats[]}
79482
+ */
79483
+ getAllStats() {
79484
+
79485
+ return [ ...this.buffers.keys() ]
79486
+ .map( label => this.getStats( label ) )
79487
+ .filter( s => s !== null );
79488
+
79489
+ }
79490
+
79491
+ report() {
79492
+
79493
+ const stats = this.getAllStats();
79494
+ if ( stats.length === 0 ) {
79495
+
79496
+ console.log( '[ProfilerService] No data. Enable profiling first and wait a few frames.' );
79497
+ return;
79498
+
79499
+ }
79500
+
79501
+ stats.sort( ( a, b ) => b.avg - a.avg );
79502
+ console.table(
79503
+ stats.map( s => ( {
79504
+ label: s.label,
79505
+ 'avg ms': s.avg.toFixed( 3 ),
79506
+ 'min ms': s.min.toFixed( 3 ),
79507
+ 'max ms': s.max.toFixed( 3 ),
79508
+ 'p95 ms': s.p95.toFixed( 3 ),
79509
+ 'budget %': s.frameBudget.toFixed( 1 ) + '%',
79510
+ samples: s.samples,
79511
+ } ) )
79512
+ );
79513
+
79514
+ }
79515
+
79516
+ /**
79517
+ * @return {import('./ProfilerService.js').ChromeTrace}
79518
+ */
79519
+ exportChromeTrace() {
79520
+
79521
+ return { traceEvents: [ ...this.traceEvents ] };
79522
+
79523
+ }
79524
+
79525
+ /**
79526
+ * @param {string} [filename='gnsx-trace.json']
79527
+ */
79528
+ downloadTrace( filename = 'gnsx-trace.json' ) {
79529
+
79530
+ if ( typeof document === 'undefined' ) {
79531
+
79532
+ console.log( '[ProfilerService] downloadTrace() only works in the browser. Use exportChromeTrace() in Node.js.' );
79533
+ return;
79534
+
79535
+ }
79536
+
79537
+ if ( this._profile !== 'full' ) {
79538
+
79539
+ console.log( `[ProfilerService] No trace — current profile is '${this._profile}'. Use setProfile('full') before enabling.` );
79540
+ return;
79541
+
79542
+ }
79543
+
79544
+ if ( this.traceEvents.length === 0 ) {
79545
+
79546
+ console.log( '[ProfilerService] No trace events. Enable the profiler and play for a few seconds first.' );
79547
+ return;
79548
+
79549
+ }
79550
+
79551
+ const json = JSON.stringify( this.exportChromeTrace() );
79552
+ const blob = new Blob( [ json ], { type: 'application/json' } );
79553
+ const url = URL.createObjectURL( blob );
79554
+ const a = document.createElement( 'a' );
79555
+ a.href = url;
79556
+ a.download = filename;
79557
+ a.click();
79558
+ URL.revokeObjectURL( url );
79559
+ console.log( `[ProfilerService] Trace downloaded: ${filename} (${this.traceEvents.length} events)` );
79560
+
79561
+ }
79562
+
79563
+ /**
79564
+ * @return {import('./ProfilerService.js').ProfilerStats[]}
79565
+ */
79566
+ exportJSON() {
79567
+
79568
+ return this.getAllStats();
79569
+
79570
+ }
79571
+
79572
+ _clearState() {
79573
+
79574
+ this.buffers.clear();
79575
+ this.cursors.clear();
79576
+ this.counts.clear();
79577
+ this.marks.clear();
79578
+ this.traceEvents = [];
79579
+ this.traceStartTime = performance.now();
79580
+
79581
+ }
79582
+
79583
+ reset() {
79584
+
79585
+ this._clearState();
79586
+ console.log( '[ProfilerService] data reset' );
79587
+
79588
+ }
79589
+
79590
+ }
79591
+
79592
+ /**
79593
+ * @typedef {Object} ProfilerStats
79594
+ * @property {string} label
79595
+ * @property {number} samples
79596
+ * @property {number} avg
79597
+ * @property {number} min
79598
+ * @property {number} max
79599
+ * @property {number} p95
79600
+ * @property {number} frameBudget Percentage of a 60 fps frame budget (16.67 ms)
79601
+ */
79602
+
79603
+ /**
79604
+ * @typedef {Object} ChromeTrace
79605
+ * @property {Array<{ name: string, ph: 'X', ts: number, dur: number, pid: 1, tid: 1, cat: 'gnsx' }>} traceEvents
79606
+ * @property {{ 'clock-offset-since-epoch-ns'?: number }|undefined} [metadata]
79607
+ */
79608
+
79609
+ /**
79610
+ * @typedef {'full'|'stats'} ProfilingProfile
79611
+ */
79612
+
79613
+ const ProfilerService = new ProfilerServiceClass();
79614
+
79615
+ function applyProfileToMethod( label, descriptor ) {
79616
+
79617
+ if ( _readProfilingEnv() === false ) return descriptor;
79618
+
79619
+ const original = descriptor.value;
79620
+
79621
+ function profiled( ...args ) {
79622
+
79623
+ ProfilerService.begin( label );
79624
+ const result = original.apply( this, args );
79625
+ if ( result instanceof Promise ) {
79626
+
79627
+ return result.finally( () => ProfilerService.end( label ) );
79628
+
79629
+ }
79630
+
79631
+ ProfilerService.end( label );
79632
+ return result;
79633
+
79634
+ }
79635
+
79636
+ descriptor.value = profiled;
79637
+ return descriptor;
79638
+
79639
+ }
79640
+
79641
+ /**
79642
+ * Method decorator that profiles the decorated method.
79643
+ * The label is automatically set to `ClassName.methodName`.
79644
+ *
79645
+ * @param {Object} target
79646
+ * @param {string|symbol} propertyKey
79647
+ * @param {PropertyDescriptor} descriptor
79648
+ * @return {PropertyDescriptor}
79649
+ */
79650
+ function profile( target, propertyKey, descriptor ) {
79651
+
79652
+ const className = target.constructor?.name ?? 'Unknown';
79653
+ return applyProfileToMethod( `${className}.${String( propertyKey )}`, descriptor );
79654
+
79655
+ }
79656
+
79657
+ /**
79658
+ * Class decorator that profiles every method of the decorated class.
79659
+ *
79660
+ * @template T
79661
+ * @param {T} constructor
79662
+ * @return {T}
79663
+ */
79664
+ function profileClass( constructor ) {
79665
+
79666
+ const proto = constructor.prototype;
79667
+ for ( const key of Object.getOwnPropertyNames( proto ) ) {
79668
+
79669
+ if ( key === 'constructor' ) continue;
79670
+ const descriptor = Object.getOwnPropertyDescriptor( proto, key );
79671
+ if ( ! descriptor || typeof descriptor.value !== 'function' ) continue;
79672
+ Object.defineProperty(
79673
+ proto,
79674
+ key,
79675
+ applyProfileToMethod( `${constructor.name}.${key}`, { ...descriptor } )
79676
+ );
79677
+
79678
+ }
79679
+
79680
+ return constructor;
79681
+
79682
+ }
79683
+ // !WITH_GENESYS
79684
+
79255
79685
  exports.ACESFilmicToneMapping = ACESFilmicToneMapping;
79256
79686
  exports.AddEquation = AddEquation;
79257
79687
  exports.AddOperation = AddOperation;
@@ -79528,6 +79958,7 @@ exports.PointsMaterial = PointsMaterial;
79528
79958
  exports.PolarGridHelper = PolarGridHelper;
79529
79959
  exports.PolyhedronGeometry = PolyhedronGeometry;
79530
79960
  exports.PositionalAudio = PositionalAudio;
79961
+ exports.ProfilerService = ProfilerService;
79531
79962
  exports.PropertyBinding = PropertyBinding;
79532
79963
  exports.PropertyMixer = PropertyMixer;
79533
79964
  exports.QuadraticBezierCurve = QuadraticBezierCurve;
@@ -79690,6 +80121,8 @@ exports.createCanvasElement = createCanvasElement;
79690
80121
  exports.error = error;
79691
80122
  exports.getConsoleFunction = getConsoleFunction;
79692
80123
  exports.log = log;
80124
+ exports.profile = profile;
80125
+ exports.profileClass = profileClass;
79693
80126
  exports.setConsoleFunction = setConsoleFunction;
79694
80127
  exports.warn = warn;
79695
80128
  exports.warnOnce = warnOnce;