@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.
@@ -59738,14 +59738,22 @@ class TextureUtils {
59738
59738
  * __gnsx_profiler.downloadTrace() // download gnsx-trace.json for Speedscope / Perfetto
59739
59739
  * __gnsx_profiler.reset() // clear samples and trace
59740
59740
  * __gnsx_profiler.disable()
59741
+ *
59742
+ * Chrome trace: `tid=1` is synchronous work; `@profile` on async methods records the
59743
+ * promise lifetime on `tid=2` so long async does not flatten the main row. For manual
59744
+ * spans, use `beginSpan` / `endSpan` with `{ asyncTimeline: true }` in a `.finally()`.
59741
59745
  */
59742
59746
 
59743
59747
  const RING_SIZE = 120;
59744
59748
  const FRAME_BUDGET_MS = 1000 / 60;
59745
- /** Max events in the trace log (~50s at 60fps with 8 labels). */
59746
- const MAX_TRACE_EVENTS = 50_000;
59749
+ const TRACE_TID_MAIN = 1;
59750
+ const TRACE_TID_ASYNC = 2;
59747
59751
  const NOOP = () => {};
59748
59752
 
59753
+ const DUMMY_SPAN = Object.freeze( { label: '', t0: 0, _seq: 0 } );
59754
+ const NOOP_BEGIN_SPAN = () => DUMMY_SPAN;
59755
+ const NOOP_END_SPAN = () => {};
59756
+
59749
59757
  class ProfilerServiceClass {
59750
59758
 
59751
59759
  constructor() {
@@ -59753,24 +59761,40 @@ class ProfilerServiceClass {
59753
59761
  this._profile = 'full';
59754
59762
  this._enabled = false;
59755
59763
 
59756
- /** @type {Map<string, Float64Array>} */
59764
+ /** @type {Map<string, Float64Array>} Inclusive time ring buffers (one per label). */
59757
59765
  this.buffers = new Map();
59758
59766
  /** @type {Map<string, number>} */
59759
59767
  this.cursors = new Map();
59760
59768
  /** @type {Map<string, number>} */
59761
59769
  this.counts = new Map();
59762
- /** @type {Map<string, Array<{ startTime: number, startMark: string, traced: boolean }>>} */
59770
+ /** @type {Map<string, Array<{ startTime: number, startMark: string }>>} */
59763
59771
  this.marks = new Map();
59764
59772
  /** @type {ChromeTraceEvent[]} */
59765
59773
  this.traceEvents = [];
59766
59774
  this.traceStartTime = 0;
59767
59775
  this._markId = 0;
59768
59776
 
59777
+ /** @type {Map<string, Float64Array>} Exclusive (self) time ring buffers (one per label). */
59778
+ this.selfBuffers = new Map();
59779
+ /** @type {Map<string, number>} */
59780
+ this.selfCursors = new Map();
59781
+ /** @type {Map<string, number>} */
59782
+ this.selfCounts = new Map();
59783
+ /**
59784
+ * Global call stack tracking nesting across all labels for exclusive-time computation.
59785
+ * @type {Array<{ label: string, childTime: number }>}
59786
+ */
59787
+ this.callStack = [];
59788
+ /** @type {Map<string, number>} Total (uncapped) invocation count since last reset, for calls-per-frame. */
59789
+ this.invocations = new Map();
59790
+
59769
59791
  if ( this._enabled ) {
59770
59792
 
59771
59793
  this.traceStartTime = performance.now();
59772
59794
  this.begin = this._beginImpl.bind( this );
59773
59795
  this.end = this._endImpl.bind( this );
59796
+ this.beginSpan = this._beginSpanImpl.bind( this );
59797
+ this.endSpan = this._endSpanImpl.bind( this );
59774
59798
  this._exposeGlobal();
59775
59799
  console.log( `[ProfilerService] auto-started from env (profile: ${this._profile})` );
59776
59800
 
@@ -59778,6 +59802,8 @@ class ProfilerServiceClass {
59778
59802
 
59779
59803
  this.begin = NOOP;
59780
59804
  this.end = NOOP;
59805
+ this.beginSpan = NOOP_BEGIN_SPAN;
59806
+ this.endSpan = NOOP_END_SPAN;
59781
59807
 
59782
59808
  }
59783
59809
 
@@ -59809,6 +59835,8 @@ class ProfilerServiceClass {
59809
59835
  this._enabled = true;
59810
59836
  this.begin = this._beginImpl.bind( this );
59811
59837
  this.end = this._endImpl.bind( this );
59838
+ this.beginSpan = this._beginSpanImpl.bind( this );
59839
+ this.endSpan = this._endSpanImpl.bind( this );
59812
59840
  this._exposeGlobal();
59813
59841
  console.log( `[ProfilerService] enabled (profile: ${this._profile}) — call __gnsx_profiler.report() or downloadTrace() from the console` );
59814
59842
 
@@ -59820,6 +59848,8 @@ class ProfilerServiceClass {
59820
59848
  this._enabled = false;
59821
59849
  this.begin = NOOP;
59822
59850
  this.end = NOOP;
59851
+ this.beginSpan = NOOP_BEGIN_SPAN;
59852
+ this.endSpan = NOOP_END_SPAN;
59823
59853
  console.log( '[ProfilerService] disabled' );
59824
59854
 
59825
59855
  }
@@ -59855,22 +59885,11 @@ class ProfilerServiceClass {
59855
59885
 
59856
59886
  }
59857
59887
 
59858
- const traced = this._profile === 'full' && this.traceEvents.length < MAX_TRACE_EVENTS;
59859
- if ( traced ) {
59860
-
59861
- this.traceEvents.push( {
59862
- name: label,
59863
- ph: 'B',
59864
- ts: this._getTraceTimestamp( startTime ),
59865
- pid: 1,
59866
- tid: 1,
59867
- cat: 'gnsx',
59868
- } );
59869
-
59870
- }
59871
-
59872
59888
  performance.mark( startMark );
59873
- stack.push( { startTime, startMark, traced } );
59889
+ stack.push( { startTime, startMark } );
59890
+
59891
+ // Push onto the global call stack for exclusive-time tracking.
59892
+ this.callStack.push( { label, childTime: 0 } );
59874
59893
 
59875
59894
  }
59876
59895
 
@@ -59885,13 +59904,61 @@ class ProfilerServiceClass {
59885
59904
  if ( stack.length === 0 ) this.marks.delete( label );
59886
59905
 
59887
59906
  const now = performance.now();
59888
- const { startTime, startMark, traced } = mark;
59907
+ const { startTime, startMark } = mark;
59889
59908
  const duration = now - startTime;
59890
59909
  const endMark = `gnsx:${label}:end:${++ this._markId}`;
59891
59910
 
59892
59911
  performance.mark( endMark );
59893
59912
  performance.measure( `gnsx:${label}`, startMark, endMark );
59894
59913
 
59914
+ this._commitDurationSample( label, startTime, duration, TRACE_TID_MAIN );
59915
+
59916
+ }
59917
+
59918
+ /**
59919
+ * Per-invocation span start (pair with {@link ProfilerServiceClass#endSpan}).
59920
+ * Safe for concurrent async with the same label.
59921
+ *
59922
+ * @param {string} label
59923
+ * @return {import('./ProfilerService.js').SpanHandle}
59924
+ */
59925
+ _beginSpanImpl( label ) {
59926
+
59927
+ const seq = ++ this._markId;
59928
+ const t0 = performance.now();
59929
+ const startMark = `gnsx:${label}:s${seq}:start`;
59930
+ performance.mark( startMark );
59931
+ return { label, t0, _seq: seq, _startMark: startMark };
59932
+
59933
+ }
59934
+
59935
+ /**
59936
+ * @param {import('./ProfilerService.js').SpanHandle} handle
59937
+ * @param {import('./ProfilerService.js').EndSpanOptions} [opts]
59938
+ */
59939
+ _endSpanImpl( handle, opts ) {
59940
+
59941
+ if ( handle._seq === 0 ) return;
59942
+
59943
+ const now = performance.now();
59944
+ const duration = now - handle.t0;
59945
+ const endMark = `gnsx:${handle.label}:s${handle._seq}:end`;
59946
+ performance.mark( endMark );
59947
+ performance.measure( `gnsx:${handle.label}#${handle._seq}`, handle._startMark, endMark );
59948
+
59949
+ const traceTid = opts?.asyncTimeline === true ? TRACE_TID_ASYNC : TRACE_TID_MAIN;
59950
+ this._commitDurationSample( handle.label, handle.t0, duration, traceTid );
59951
+
59952
+ }
59953
+
59954
+ /**
59955
+ * @param {string} label
59956
+ * @param {number} startTime
59957
+ * @param {number} durationMs
59958
+ * @param {typeof TRACE_TID_MAIN|typeof TRACE_TID_ASYNC} traceTid
59959
+ */
59960
+ _commitDurationSample( label, startTime, durationMs, traceTid ) {
59961
+
59895
59962
  let buffer = this.buffers.get( label );
59896
59963
  if ( ! buffer ) {
59897
59964
 
@@ -59903,18 +59970,56 @@ class ProfilerServiceClass {
59903
59970
  }
59904
59971
 
59905
59972
  const cursor = this.cursors.get( label );
59906
- buffer[ cursor ] = duration;
59973
+ buffer[ cursor ] = durationMs;
59907
59974
  this.cursors.set( label, ( cursor + 1 ) % RING_SIZE );
59908
59975
  this.counts.set( label, Math.min( ( this.counts.get( label ) + 1 ), RING_SIZE ) );
59909
59976
 
59910
- if ( traced ) {
59977
+ // Track total invocations (uncapped) for calls-per-frame computation.
59978
+ this.invocations.set( label, ( this.invocations.get( label ) ?? 0 ) + 1 );
59979
+
59980
+ // Exclusive (self) time via the global call stack.
59981
+ const top = this.callStack.length > 0 ? this.callStack[ this.callStack.length - 1 ] : undefined;
59982
+ if ( top !== undefined && top.label === label ) {
59983
+
59984
+ this.callStack.pop();
59985
+ const selfTime = Math.max( 0, durationMs - top.childTime );
59986
+
59987
+ let selfBuffer = this.selfBuffers.get( label );
59988
+ if ( ! selfBuffer ) {
59989
+
59990
+ selfBuffer = new Float64Array( RING_SIZE );
59991
+ this.selfBuffers.set( label, selfBuffer );
59992
+ this.selfCursors.set( label, 0 );
59993
+ this.selfCounts.set( label, 0 );
59994
+
59995
+ }
59996
+
59997
+ const selfCursor = this.selfCursors.get( label );
59998
+ selfBuffer[ selfCursor ] = selfTime;
59999
+ this.selfCursors.set( label, ( selfCursor + 1 ) % RING_SIZE );
60000
+ this.selfCounts.set( label, Math.min( ( this.selfCounts.get( label ) + 1 ), RING_SIZE ) );
60001
+
60002
+ // Propagate inclusive duration to the parent scope's child accumulator.
60003
+ const parent = this.callStack.length > 0 ? this.callStack[ this.callStack.length - 1 ] : undefined;
60004
+ if ( parent !== undefined ) parent.childTime += durationMs;
59911
60005
 
60006
+ } else {
60007
+
60008
+ // Label mismatch — likely async interleaving. Reset to avoid corruption.
60009
+ this.callStack.length = 0;
60010
+
60011
+ }
60012
+
60013
+ if ( traceTid ) {
60014
+
60015
+ const traceName = traceTid === TRACE_TID_ASYNC ? `${label} (promise)` : label;
59912
60016
  this.traceEvents.push( {
59913
- name: label,
59914
- ph: 'E',
59915
- ts: this._getTraceTimestamp( now ),
60017
+ name: traceName,
60018
+ ph: 'X',
60019
+ ts: Math.round( this._getTraceTimestamp( startTime ) ),
60020
+ dur: Math.max( 1, Math.round( durationMs * 1000 ) ),
59916
60021
  pid: 1,
59917
- tid: 1,
60022
+ tid: traceTid,
59918
60023
  cat: 'gnsx',
59919
60024
  } );
59920
60025
 
@@ -59949,6 +60054,23 @@ class ProfilerServiceClass {
59949
60054
  const sorted = [ ...samples ].sort( ( a, b ) => a - b );
59950
60055
  const avg = sorted.reduce( ( a, b ) => a + b, 0 ) / sorted.length;
59951
60056
 
60057
+ // Exclusive (self) time stats.
60058
+ const selfCount = this.selfCounts.get( label ) ?? 0;
60059
+ let selfAvg, selfMin, selfMax, selfP95, selfFrameBudget;
60060
+ if ( selfCount > 0 ) {
60061
+
60062
+ const selfBuffer = this.selfBuffers.get( label );
60063
+ const selfSamples = [ ...( selfCount < RING_SIZE
60064
+ ? selfBuffer.subarray( 0, selfCount )
60065
+ : selfBuffer ) ].sort( ( a, b ) => a - b );
60066
+ selfAvg = selfSamples.reduce( ( a, b ) => a + b, 0 ) / selfSamples.length;
60067
+ selfMin = selfSamples[ 0 ];
60068
+ selfMax = selfSamples[ selfSamples.length - 1 ];
60069
+ selfP95 = selfSamples[ Math.floor( selfSamples.length * 0.95 ) ];
60070
+ selfFrameBudget = ( selfAvg / FRAME_BUDGET_MS ) * 100;
60071
+
60072
+ }
60073
+
59952
60074
  return {
59953
60075
  label,
59954
60076
  samples: samples.length,
@@ -59957,6 +60079,12 @@ class ProfilerServiceClass {
59957
60079
  max: sorted[ sorted.length - 1 ],
59958
60080
  p95: sorted[ Math.floor( sorted.length * 0.95 ) ],
59959
60081
  frameBudget: ( avg / FRAME_BUDGET_MS ) * 100,
60082
+ totalInvocations: this.invocations.get( label ) ?? 0,
60083
+ selfAvg,
60084
+ selfMin,
60085
+ selfMax,
60086
+ selfP95,
60087
+ selfFrameBudget,
59960
60088
  };
59961
60089
 
59962
60090
  }
@@ -60002,13 +60130,53 @@ class ProfilerServiceClass {
60002
60130
  */
60003
60131
  exportChromeTrace() {
60004
60132
 
60133
+ const slices = [ ...this.traceEvents ];
60134
+ slices.sort( ( a, b ) => {
60135
+
60136
+ if ( a.ts !== b.ts ) return a.ts - b.ts;
60137
+ if ( a.tid !== b.tid ) return a.tid - b.tid;
60138
+ return a.name.localeCompare( b.name );
60139
+
60140
+ } );
60141
+ const hasAsync = slices.some( e => e.tid === TRACE_TID_ASYNC );
60142
+ /** @type {import('./ProfilerService.js').ChromeTraceMetadataEvent[]} */
60143
+ const prefix = [
60144
+ {
60145
+ cat: '__metadata',
60146
+ name: 'process_name',
60147
+ ph: 'M',
60148
+ pid: 1,
60149
+ tid: 0,
60150
+ ts: 0,
60151
+ args: { name: 'Genesys Profiler' },
60152
+ },
60153
+ {
60154
+ cat: '__metadata',
60155
+ name: 'thread_name',
60156
+ ph: 'M',
60157
+ pid: 1,
60158
+ tid: TRACE_TID_MAIN,
60159
+ ts: 0,
60160
+ args: { name: 'Main thread' },
60161
+ },
60162
+ ];
60163
+ if ( hasAsync ) {
60164
+
60165
+ prefix.push( {
60166
+ cat: '__metadata',
60167
+ name: 'thread_name',
60168
+ ph: 'M',
60169
+ pid: 1,
60170
+ tid: TRACE_TID_ASYNC,
60171
+ ts: 0,
60172
+ args: { name: 'Async (promise lifetime)' },
60173
+ } );
60174
+
60175
+ }
60176
+
60005
60177
  return {
60006
60178
  displayTimeUnit: 'ms',
60007
- traceEvents: [
60008
- { name: 'process_name', ph: 'M', pid: 1, args: { name: 'Genesys Profiler' } },
60009
- { name: 'thread_name', ph: 'M', pid: 1, tid: 1, args: { name: 'Main Thread' } },
60010
- ...this.traceEvents,
60011
- ],
60179
+ traceEvents: [ ...prefix, ...slices ],
60012
60180
  };
60013
60181
 
60014
60182
  }
@@ -60079,6 +60247,11 @@ class ProfilerServiceClass {
60079
60247
  this.traceEvents = [];
60080
60248
  this.traceStartTime = performance.now();
60081
60249
  this._markId = 0;
60250
+ this.selfBuffers.clear();
60251
+ this.selfCursors.clear();
60252
+ this.selfCounts.clear();
60253
+ this.callStack.length = 0;
60254
+ this.invocations.clear();
60082
60255
 
60083
60256
  }
60084
60257
 
@@ -60100,24 +60273,46 @@ class ProfilerServiceClass {
60100
60273
  * @property {number} max
60101
60274
  * @property {number} p95
60102
60275
  * @property {number} frameBudget Percentage of a 60 fps frame budget (16.67 ms)
60276
+ * @property {number} totalInvocations Total (uncapped) call count since last reset, for calls-per-frame computation.
60277
+ * @property {number|undefined} selfAvg Exclusive (self) avg ms — inclusive time minus child scope time.
60278
+ * @property {number|undefined} selfMin
60279
+ * @property {number|undefined} selfMax
60280
+ * @property {number|undefined} selfP95
60281
+ * @property {number|undefined} selfFrameBudget Exclusive time as percentage of a 60 fps frame budget.
60282
+ */
60283
+
60284
+ /**
60285
+ * @typedef {Object} SpanHandle
60286
+ * @property {string} label
60287
+ * @property {number} t0
60288
+ * @property {number} _seq
60289
+ * @property {string} _startMark
60290
+ */
60291
+
60292
+ /**
60293
+ * @typedef {Object} EndSpanOptions
60294
+ * @property {boolean} [asyncTimeline] When true, trace slice uses `tid=2` (virtual async row).
60103
60295
  */
60104
60296
 
60105
60297
  /**
60106
60298
  * @typedef {Object} ChromeTraceEvent
60107
60299
  * @property {string} name
60108
- * @property {'B'|'E'} ph
60300
+ * @property {'X'} ph
60109
60301
  * @property {number} ts
60302
+ * @property {number} dur
60110
60303
  * @property {1} pid
60111
- * @property {1} tid
60304
+ * @property {number} tid
60112
60305
  * @property {'gnsx'} cat
60113
60306
  */
60114
60307
 
60115
60308
  /**
60116
60309
  * @typedef {Object} ChromeTraceMetadataEvent
60310
+ * @property {'__metadata'} cat
60117
60311
  * @property {'process_name'|'thread_name'} name
60118
60312
  * @property {'M'} ph
60119
60313
  * @property {1} pid
60120
- * @property {1} [tid]
60314
+ * @property {number} [tid]
60315
+ * @property {0} ts
60121
60316
  * @property {{ name: string }} args
60122
60317
  */
60123
60318
 
@@ -60133,28 +60328,38 @@ class ProfilerServiceClass {
60133
60328
 
60134
60329
  const ProfilerService = new ProfilerServiceClass();
60135
60330
 
60331
+ function isThenable( x ) {
60332
+
60333
+ return (
60334
+ ( typeof x === 'object' || typeof x === 'function' ) &&
60335
+ x !== null &&
60336
+ typeof x.then === 'function'
60337
+ );
60338
+
60339
+ }
60340
+
60136
60341
  function applyProfileToMethod( label, descriptor ) {
60137
60342
 
60138
60343
  const original = descriptor.value;
60139
60344
 
60140
60345
  function profiled( ...args ) {
60141
60346
 
60142
- ProfilerService.begin( label );
60347
+ const span = ProfilerService.beginSpan( label );
60143
60348
  try {
60144
60349
 
60145
60350
  const result = original.apply( this, args );
60146
- if ( result instanceof Promise ) {
60351
+ if ( isThenable( result ) ) {
60147
60352
 
60148
- return result.finally( () => ProfilerService.end( label ) );
60353
+ return Promise.resolve( result ).finally( () => ProfilerService.endSpan( span, { asyncTimeline: true } ) );
60149
60354
 
60150
60355
  }
60151
60356
 
60152
- ProfilerService.end( label );
60357
+ ProfilerService.endSpan( span );
60153
60358
  return result;
60154
60359
 
60155
60360
  } catch ( error ) {
60156
60361
 
60157
- ProfilerService.end( label );
60362
+ ProfilerService.endSpan( span );
60158
60363
  throw error;
60159
60364
 
60160
60365
  }
@@ -60168,14 +60373,39 @@ function applyProfileToMethod( label, descriptor ) {
60168
60373
 
60169
60374
  /**
60170
60375
  * Method decorator that profiles the decorated method.
60171
- * The label is automatically set to `ClassName.methodName`.
60376
+ * Default label: `ClassName.methodName`. Pass a custom tag via `@profile('My tag')`.
60172
60377
  *
60378
+ * @param {Object|string} [targetOrTag]
60379
+ * @param {string|symbol} [propertyKey]
60380
+ * @param {PropertyDescriptor} [descriptor]
60381
+ * @return {PropertyDescriptor|function(Object, string|symbol, PropertyDescriptor): PropertyDescriptor}
60382
+ */
60383
+ function profile( targetOrTag, propertyKey, descriptor ) {
60384
+
60385
+ if ( arguments.length === 0 ) {
60386
+
60387
+ return defaultProfileDecorator;
60388
+
60389
+ }
60390
+
60391
+ if ( arguments.length === 1 && typeof targetOrTag === 'string' ) {
60392
+
60393
+ const customTag = targetOrTag;
60394
+ return ( target, key, desc ) => applyProfileToMethod( customTag, desc );
60395
+
60396
+ }
60397
+
60398
+ return defaultProfileDecorator( targetOrTag, propertyKey, descriptor );
60399
+
60400
+ }
60401
+
60402
+ /**
60173
60403
  * @param {Object} target
60174
60404
  * @param {string|symbol} propertyKey
60175
60405
  * @param {PropertyDescriptor} descriptor
60176
60406
  * @return {PropertyDescriptor}
60177
60407
  */
60178
- function profile( target, propertyKey, descriptor ) {
60408
+ function defaultProfileDecorator( target, propertyKey, descriptor ) {
60179
60409
 
60180
60410
  const className = target.constructor?.name ?? 'Unknown';
60181
60411
  return applyProfileToMethod( `${className}.${String( propertyKey )}`, descriptor );