@gnsx/three 0.184.18 → 0.184.20

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
@@ -59718,14 +59718,22 @@ class TextureUtils {
59718
59718
  * __gnsx_profiler.downloadTrace() // download gnsx-trace.json for Speedscope / Perfetto
59719
59719
  * __gnsx_profiler.reset() // clear samples and trace
59720
59720
  * __gnsx_profiler.disable()
59721
+ *
59722
+ * Chrome trace: `tid=1` is synchronous work; `@profile` on async methods records the
59723
+ * promise lifetime on `tid=2` so long async does not flatten the main row. For manual
59724
+ * spans, use `beginSpan` / `endSpan` with `{ asyncTimeline: true }` in a `.finally()`.
59721
59725
  */
59722
59726
 
59723
59727
  const RING_SIZE = 120;
59724
59728
  const FRAME_BUDGET_MS = 1000 / 60;
59725
- /** Max events in the trace log (~50s at 60fps with 8 labels). */
59726
- const MAX_TRACE_EVENTS = 50_000;
59729
+ const TRACE_TID_MAIN = 1;
59730
+ const TRACE_TID_ASYNC = 2;
59727
59731
  const NOOP = () => {};
59728
59732
 
59733
+ const DUMMY_SPAN = Object.freeze( { label: '', t0: 0, _seq: 0 } );
59734
+ const NOOP_BEGIN_SPAN = () => DUMMY_SPAN;
59735
+ const NOOP_END_SPAN = () => {};
59736
+
59729
59737
  class ProfilerServiceClass {
59730
59738
 
59731
59739
  constructor() {
@@ -59733,24 +59741,40 @@ class ProfilerServiceClass {
59733
59741
  this._profile = 'full';
59734
59742
  this._enabled = false;
59735
59743
 
59736
- /** @type {Map<string, Float64Array>} */
59744
+ /** @type {Map<string, Float64Array>} Inclusive time ring buffers (one per label). */
59737
59745
  this.buffers = new Map();
59738
59746
  /** @type {Map<string, number>} */
59739
59747
  this.cursors = new Map();
59740
59748
  /** @type {Map<string, number>} */
59741
59749
  this.counts = new Map();
59742
- /** @type {Map<string, Array<{ startTime: number, startMark: string, traced: boolean }>>} */
59750
+ /** @type {Map<string, Array<{ startTime: number, startMark: string }>>} */
59743
59751
  this.marks = new Map();
59744
59752
  /** @type {ChromeTraceEvent[]} */
59745
59753
  this.traceEvents = [];
59746
59754
  this.traceStartTime = 0;
59747
59755
  this._markId = 0;
59748
59756
 
59757
+ /** @type {Map<string, Float64Array>} Exclusive (self) time ring buffers (one per label). */
59758
+ this.selfBuffers = new Map();
59759
+ /** @type {Map<string, number>} */
59760
+ this.selfCursors = new Map();
59761
+ /** @type {Map<string, number>} */
59762
+ this.selfCounts = new Map();
59763
+ /**
59764
+ * Global call stack tracking nesting across all labels for exclusive-time computation.
59765
+ * @type {Array<{ label: string, childTime: number }>}
59766
+ */
59767
+ this.callStack = [];
59768
+ /** @type {Map<string, number>} Total (uncapped) invocation count since last reset, for calls-per-frame. */
59769
+ this.invocations = new Map();
59770
+
59749
59771
  if ( this._enabled ) {
59750
59772
 
59751
59773
  this.traceStartTime = performance.now();
59752
59774
  this.begin = this._beginImpl.bind( this );
59753
59775
  this.end = this._endImpl.bind( this );
59776
+ this.beginSpan = this._beginSpanImpl.bind( this );
59777
+ this.endSpan = this._endSpanImpl.bind( this );
59754
59778
  this._exposeGlobal();
59755
59779
  console.log( `[ProfilerService] auto-started from env (profile: ${this._profile})` );
59756
59780
 
@@ -59758,6 +59782,8 @@ class ProfilerServiceClass {
59758
59782
 
59759
59783
  this.begin = NOOP;
59760
59784
  this.end = NOOP;
59785
+ this.beginSpan = NOOP_BEGIN_SPAN;
59786
+ this.endSpan = NOOP_END_SPAN;
59761
59787
 
59762
59788
  }
59763
59789
 
@@ -59789,6 +59815,8 @@ class ProfilerServiceClass {
59789
59815
  this._enabled = true;
59790
59816
  this.begin = this._beginImpl.bind( this );
59791
59817
  this.end = this._endImpl.bind( this );
59818
+ this.beginSpan = this._beginSpanImpl.bind( this );
59819
+ this.endSpan = this._endSpanImpl.bind( this );
59792
59820
  this._exposeGlobal();
59793
59821
  console.log( `[ProfilerService] enabled (profile: ${this._profile}) — call __gnsx_profiler.report() or downloadTrace() from the console` );
59794
59822
 
@@ -59800,6 +59828,8 @@ class ProfilerServiceClass {
59800
59828
  this._enabled = false;
59801
59829
  this.begin = NOOP;
59802
59830
  this.end = NOOP;
59831
+ this.beginSpan = NOOP_BEGIN_SPAN;
59832
+ this.endSpan = NOOP_END_SPAN;
59803
59833
  console.log( '[ProfilerService] disabled' );
59804
59834
 
59805
59835
  }
@@ -59835,22 +59865,11 @@ class ProfilerServiceClass {
59835
59865
 
59836
59866
  }
59837
59867
 
59838
- const traced = this._profile === 'full' && this.traceEvents.length < MAX_TRACE_EVENTS;
59839
- if ( traced ) {
59840
-
59841
- this.traceEvents.push( {
59842
- name: label,
59843
- ph: 'B',
59844
- ts: this._getTraceTimestamp( startTime ),
59845
- pid: 1,
59846
- tid: 1,
59847
- cat: 'gnsx',
59848
- } );
59849
-
59850
- }
59851
-
59852
59868
  performance.mark( startMark );
59853
- stack.push( { startTime, startMark, traced } );
59869
+ stack.push( { startTime, startMark } );
59870
+
59871
+ // Push onto the global call stack for exclusive-time tracking.
59872
+ this.callStack.push( { label, childTime: 0 } );
59854
59873
 
59855
59874
  }
59856
59875
 
@@ -59865,13 +59884,61 @@ class ProfilerServiceClass {
59865
59884
  if ( stack.length === 0 ) this.marks.delete( label );
59866
59885
 
59867
59886
  const now = performance.now();
59868
- const { startTime, startMark, traced } = mark;
59887
+ const { startTime, startMark } = mark;
59869
59888
  const duration = now - startTime;
59870
59889
  const endMark = `gnsx:${label}:end:${++ this._markId}`;
59871
59890
 
59872
59891
  performance.mark( endMark );
59873
59892
  performance.measure( `gnsx:${label}`, startMark, endMark );
59874
59893
 
59894
+ this._commitDurationSample( label, startTime, duration, TRACE_TID_MAIN );
59895
+
59896
+ }
59897
+
59898
+ /**
59899
+ * Per-invocation span start (pair with {@link ProfilerServiceClass#endSpan}).
59900
+ * Safe for concurrent async with the same label.
59901
+ *
59902
+ * @param {string} label
59903
+ * @return {import('./ProfilerService.js').SpanHandle}
59904
+ */
59905
+ _beginSpanImpl( label ) {
59906
+
59907
+ const seq = ++ this._markId;
59908
+ const t0 = performance.now();
59909
+ const startMark = `gnsx:${label}:s${seq}:start`;
59910
+ performance.mark( startMark );
59911
+ return { label, t0, _seq: seq, _startMark: startMark };
59912
+
59913
+ }
59914
+
59915
+ /**
59916
+ * @param {import('./ProfilerService.js').SpanHandle} handle
59917
+ * @param {import('./ProfilerService.js').EndSpanOptions} [opts]
59918
+ */
59919
+ _endSpanImpl( handle, opts ) {
59920
+
59921
+ if ( handle._seq === 0 ) return;
59922
+
59923
+ const now = performance.now();
59924
+ const duration = now - handle.t0;
59925
+ const endMark = `gnsx:${handle.label}:s${handle._seq}:end`;
59926
+ performance.mark( endMark );
59927
+ performance.measure( `gnsx:${handle.label}#${handle._seq}`, handle._startMark, endMark );
59928
+
59929
+ const traceTid = opts?.asyncTimeline === true ? TRACE_TID_ASYNC : TRACE_TID_MAIN;
59930
+ this._commitDurationSample( handle.label, handle.t0, duration, traceTid );
59931
+
59932
+ }
59933
+
59934
+ /**
59935
+ * @param {string} label
59936
+ * @param {number} startTime
59937
+ * @param {number} durationMs
59938
+ * @param {typeof TRACE_TID_MAIN|typeof TRACE_TID_ASYNC} traceTid
59939
+ */
59940
+ _commitDurationSample( label, startTime, durationMs, traceTid ) {
59941
+
59875
59942
  let buffer = this.buffers.get( label );
59876
59943
  if ( ! buffer ) {
59877
59944
 
@@ -59883,18 +59950,56 @@ class ProfilerServiceClass {
59883
59950
  }
59884
59951
 
59885
59952
  const cursor = this.cursors.get( label );
59886
- buffer[ cursor ] = duration;
59953
+ buffer[ cursor ] = durationMs;
59887
59954
  this.cursors.set( label, ( cursor + 1 ) % RING_SIZE );
59888
59955
  this.counts.set( label, Math.min( ( this.counts.get( label ) + 1 ), RING_SIZE ) );
59889
59956
 
59890
- if ( traced ) {
59957
+ // Track total invocations (uncapped) for calls-per-frame computation.
59958
+ this.invocations.set( label, ( this.invocations.get( label ) ?? 0 ) + 1 );
59959
+
59960
+ // Exclusive (self) time via the global call stack.
59961
+ const top = this.callStack.length > 0 ? this.callStack[ this.callStack.length - 1 ] : undefined;
59962
+ if ( top !== undefined && top.label === label ) {
59963
+
59964
+ this.callStack.pop();
59965
+ const selfTime = Math.max( 0, durationMs - top.childTime );
59966
+
59967
+ let selfBuffer = this.selfBuffers.get( label );
59968
+ if ( ! selfBuffer ) {
59969
+
59970
+ selfBuffer = new Float64Array( RING_SIZE );
59971
+ this.selfBuffers.set( label, selfBuffer );
59972
+ this.selfCursors.set( label, 0 );
59973
+ this.selfCounts.set( label, 0 );
59974
+
59975
+ }
59976
+
59977
+ const selfCursor = this.selfCursors.get( label );
59978
+ selfBuffer[ selfCursor ] = selfTime;
59979
+ this.selfCursors.set( label, ( selfCursor + 1 ) % RING_SIZE );
59980
+ this.selfCounts.set( label, Math.min( ( this.selfCounts.get( label ) + 1 ), RING_SIZE ) );
59981
+
59982
+ // Propagate inclusive duration to the parent scope's child accumulator.
59983
+ const parent = this.callStack.length > 0 ? this.callStack[ this.callStack.length - 1 ] : undefined;
59984
+ if ( parent !== undefined ) parent.childTime += durationMs;
59985
+
59986
+ } else {
59987
+
59988
+ // Label mismatch — likely async interleaving. Reset to avoid corruption.
59989
+ this.callStack.length = 0;
59990
+
59991
+ }
59891
59992
 
59993
+ if ( traceTid ) {
59994
+
59995
+ const traceName = traceTid === TRACE_TID_ASYNC ? `${label} (promise)` : label;
59892
59996
  this.traceEvents.push( {
59893
- name: label,
59894
- ph: 'E',
59895
- ts: this._getTraceTimestamp( now ),
59997
+ name: traceName,
59998
+ ph: 'X',
59999
+ ts: Math.round( this._getTraceTimestamp( startTime ) ),
60000
+ dur: Math.max( 1, Math.round( durationMs * 1000 ) ),
59896
60001
  pid: 1,
59897
- tid: 1,
60002
+ tid: traceTid,
59898
60003
  cat: 'gnsx',
59899
60004
  } );
59900
60005
 
@@ -59929,6 +60034,23 @@ class ProfilerServiceClass {
59929
60034
  const sorted = [ ...samples ].sort( ( a, b ) => a - b );
59930
60035
  const avg = sorted.reduce( ( a, b ) => a + b, 0 ) / sorted.length;
59931
60036
 
60037
+ // Exclusive (self) time stats.
60038
+ const selfCount = this.selfCounts.get( label ) ?? 0;
60039
+ let selfAvg, selfMin, selfMax, selfP95, selfFrameBudget;
60040
+ if ( selfCount > 0 ) {
60041
+
60042
+ const selfBuffer = this.selfBuffers.get( label );
60043
+ const selfSamples = [ ...( selfCount < RING_SIZE
60044
+ ? selfBuffer.subarray( 0, selfCount )
60045
+ : selfBuffer ) ].sort( ( a, b ) => a - b );
60046
+ selfAvg = selfSamples.reduce( ( a, b ) => a + b, 0 ) / selfSamples.length;
60047
+ selfMin = selfSamples[ 0 ];
60048
+ selfMax = selfSamples[ selfSamples.length - 1 ];
60049
+ selfP95 = selfSamples[ Math.floor( selfSamples.length * 0.95 ) ];
60050
+ selfFrameBudget = ( selfAvg / FRAME_BUDGET_MS ) * 100;
60051
+
60052
+ }
60053
+
59932
60054
  return {
59933
60055
  label,
59934
60056
  samples: samples.length,
@@ -59937,6 +60059,12 @@ class ProfilerServiceClass {
59937
60059
  max: sorted[ sorted.length - 1 ],
59938
60060
  p95: sorted[ Math.floor( sorted.length * 0.95 ) ],
59939
60061
  frameBudget: ( avg / FRAME_BUDGET_MS ) * 100,
60062
+ totalInvocations: this.invocations.get( label ) ?? 0,
60063
+ selfAvg,
60064
+ selfMin,
60065
+ selfMax,
60066
+ selfP95,
60067
+ selfFrameBudget,
59940
60068
  };
59941
60069
 
59942
60070
  }
@@ -59982,13 +60110,53 @@ class ProfilerServiceClass {
59982
60110
  */
59983
60111
  exportChromeTrace() {
59984
60112
 
60113
+ const slices = [ ...this.traceEvents ];
60114
+ slices.sort( ( a, b ) => {
60115
+
60116
+ if ( a.ts !== b.ts ) return a.ts - b.ts;
60117
+ if ( a.tid !== b.tid ) return a.tid - b.tid;
60118
+ return a.name.localeCompare( b.name );
60119
+
60120
+ } );
60121
+ const hasAsync = slices.some( e => e.tid === TRACE_TID_ASYNC );
60122
+ /** @type {import('./ProfilerService.js').ChromeTraceMetadataEvent[]} */
60123
+ const prefix = [
60124
+ {
60125
+ cat: '__metadata',
60126
+ name: 'process_name',
60127
+ ph: 'M',
60128
+ pid: 1,
60129
+ tid: 0,
60130
+ ts: 0,
60131
+ args: { name: 'Genesys Profiler' },
60132
+ },
60133
+ {
60134
+ cat: '__metadata',
60135
+ name: 'thread_name',
60136
+ ph: 'M',
60137
+ pid: 1,
60138
+ tid: TRACE_TID_MAIN,
60139
+ ts: 0,
60140
+ args: { name: 'Main thread' },
60141
+ },
60142
+ ];
60143
+ if ( hasAsync ) {
60144
+
60145
+ prefix.push( {
60146
+ cat: '__metadata',
60147
+ name: 'thread_name',
60148
+ ph: 'M',
60149
+ pid: 1,
60150
+ tid: TRACE_TID_ASYNC,
60151
+ ts: 0,
60152
+ args: { name: 'Async (promise lifetime)' },
60153
+ } );
60154
+
60155
+ }
60156
+
59985
60157
  return {
59986
60158
  displayTimeUnit: 'ms',
59987
- traceEvents: [
59988
- { name: 'process_name', ph: 'M', pid: 1, args: { name: 'Genesys Profiler' } },
59989
- { name: 'thread_name', ph: 'M', pid: 1, tid: 1, args: { name: 'Main Thread' } },
59990
- ...this.traceEvents,
59991
- ],
60159
+ traceEvents: [ ...prefix, ...slices ],
59992
60160
  };
59993
60161
 
59994
60162
  }
@@ -60059,6 +60227,11 @@ class ProfilerServiceClass {
60059
60227
  this.traceEvents = [];
60060
60228
  this.traceStartTime = performance.now();
60061
60229
  this._markId = 0;
60230
+ this.selfBuffers.clear();
60231
+ this.selfCursors.clear();
60232
+ this.selfCounts.clear();
60233
+ this.callStack.length = 0;
60234
+ this.invocations.clear();
60062
60235
 
60063
60236
  }
60064
60237
 
@@ -60080,24 +60253,46 @@ class ProfilerServiceClass {
60080
60253
  * @property {number} max
60081
60254
  * @property {number} p95
60082
60255
  * @property {number} frameBudget Percentage of a 60 fps frame budget (16.67 ms)
60256
+ * @property {number} totalInvocations Total (uncapped) call count since last reset, for calls-per-frame computation.
60257
+ * @property {number|undefined} selfAvg Exclusive (self) avg ms — inclusive time minus child scope time.
60258
+ * @property {number|undefined} selfMin
60259
+ * @property {number|undefined} selfMax
60260
+ * @property {number|undefined} selfP95
60261
+ * @property {number|undefined} selfFrameBudget Exclusive time as percentage of a 60 fps frame budget.
60262
+ */
60263
+
60264
+ /**
60265
+ * @typedef {Object} SpanHandle
60266
+ * @property {string} label
60267
+ * @property {number} t0
60268
+ * @property {number} _seq
60269
+ * @property {string} _startMark
60270
+ */
60271
+
60272
+ /**
60273
+ * @typedef {Object} EndSpanOptions
60274
+ * @property {boolean} [asyncTimeline] When true, trace slice uses `tid=2` (virtual async row).
60083
60275
  */
60084
60276
 
60085
60277
  /**
60086
60278
  * @typedef {Object} ChromeTraceEvent
60087
60279
  * @property {string} name
60088
- * @property {'B'|'E'} ph
60280
+ * @property {'X'} ph
60089
60281
  * @property {number} ts
60282
+ * @property {number} dur
60090
60283
  * @property {1} pid
60091
- * @property {1} tid
60284
+ * @property {number} tid
60092
60285
  * @property {'gnsx'} cat
60093
60286
  */
60094
60287
 
60095
60288
  /**
60096
60289
  * @typedef {Object} ChromeTraceMetadataEvent
60290
+ * @property {'__metadata'} cat
60097
60291
  * @property {'process_name'|'thread_name'} name
60098
60292
  * @property {'M'} ph
60099
60293
  * @property {1} pid
60100
- * @property {1} [tid]
60294
+ * @property {number} [tid]
60295
+ * @property {0} ts
60101
60296
  * @property {{ name: string }} args
60102
60297
  */
60103
60298
 
@@ -60113,28 +60308,38 @@ class ProfilerServiceClass {
60113
60308
 
60114
60309
  const ProfilerService = new ProfilerServiceClass();
60115
60310
 
60311
+ function isThenable( x ) {
60312
+
60313
+ return (
60314
+ ( typeof x === 'object' || typeof x === 'function' ) &&
60315
+ x !== null &&
60316
+ typeof x.then === 'function'
60317
+ );
60318
+
60319
+ }
60320
+
60116
60321
  function applyProfileToMethod( label, descriptor ) {
60117
60322
 
60118
60323
  const original = descriptor.value;
60119
60324
 
60120
60325
  function profiled( ...args ) {
60121
60326
 
60122
- ProfilerService.begin( label );
60327
+ const span = ProfilerService.beginSpan( label );
60123
60328
  try {
60124
60329
 
60125
60330
  const result = original.apply( this, args );
60126
- if ( result instanceof Promise ) {
60331
+ if ( isThenable( result ) ) {
60127
60332
 
60128
- return result.finally( () => ProfilerService.end( label ) );
60333
+ return Promise.resolve( result ).finally( () => ProfilerService.endSpan( span, { asyncTimeline: true } ) );
60129
60334
 
60130
60335
  }
60131
60336
 
60132
- ProfilerService.end( label );
60337
+ ProfilerService.endSpan( span );
60133
60338
  return result;
60134
60339
 
60135
60340
  } catch ( error ) {
60136
60341
 
60137
- ProfilerService.end( label );
60342
+ ProfilerService.endSpan( span );
60138
60343
  throw error;
60139
60344
 
60140
60345
  }
@@ -60148,14 +60353,39 @@ function applyProfileToMethod( label, descriptor ) {
60148
60353
 
60149
60354
  /**
60150
60355
  * Method decorator that profiles the decorated method.
60151
- * The label is automatically set to `ClassName.methodName`.
60356
+ * Default label: `ClassName.methodName`. Pass a custom tag via `@profile('My tag')`.
60152
60357
  *
60358
+ * @param {Object|string} [targetOrTag]
60359
+ * @param {string|symbol} [propertyKey]
60360
+ * @param {PropertyDescriptor} [descriptor]
60361
+ * @return {PropertyDescriptor|function(Object, string|symbol, PropertyDescriptor): PropertyDescriptor}
60362
+ */
60363
+ function profile( targetOrTag, propertyKey, descriptor ) {
60364
+
60365
+ if ( arguments.length === 0 ) {
60366
+
60367
+ return defaultProfileDecorator;
60368
+
60369
+ }
60370
+
60371
+ if ( arguments.length === 1 && typeof targetOrTag === 'string' ) {
60372
+
60373
+ const customTag = targetOrTag;
60374
+ return ( target, key, desc ) => applyProfileToMethod( customTag, desc );
60375
+
60376
+ }
60377
+
60378
+ return defaultProfileDecorator( targetOrTag, propertyKey, descriptor );
60379
+
60380
+ }
60381
+
60382
+ /**
60153
60383
  * @param {Object} target
60154
60384
  * @param {string|symbol} propertyKey
60155
60385
  * @param {PropertyDescriptor} descriptor
60156
60386
  * @return {PropertyDescriptor}
60157
60387
  */
60158
- function profile( target, propertyKey, descriptor ) {
60388
+ function defaultProfileDecorator( target, propertyKey, descriptor ) {
60159
60389
 
60160
60390
  const className = target.constructor?.name ?? 'Unknown';
60161
60391
  return applyProfileToMethod( `${className}.${String( propertyKey )}`, descriptor );