@gnsx/three 0.184.19 → 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() {
@@ -59759,7 +59767,7 @@ class ProfilerServiceClass {
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 = [];
@@ -59785,6 +59793,8 @@ class ProfilerServiceClass {
59785
59793
  this.traceStartTime = performance.now();
59786
59794
  this.begin = this._beginImpl.bind( this );
59787
59795
  this.end = this._endImpl.bind( this );
59796
+ this.beginSpan = this._beginSpanImpl.bind( this );
59797
+ this.endSpan = this._endSpanImpl.bind( this );
59788
59798
  this._exposeGlobal();
59789
59799
  console.log( `[ProfilerService] auto-started from env (profile: ${this._profile})` );
59790
59800
 
@@ -59792,6 +59802,8 @@ class ProfilerServiceClass {
59792
59802
 
59793
59803
  this.begin = NOOP;
59794
59804
  this.end = NOOP;
59805
+ this.beginSpan = NOOP_BEGIN_SPAN;
59806
+ this.endSpan = NOOP_END_SPAN;
59795
59807
 
59796
59808
  }
59797
59809
 
@@ -59823,6 +59835,8 @@ class ProfilerServiceClass {
59823
59835
  this._enabled = true;
59824
59836
  this.begin = this._beginImpl.bind( this );
59825
59837
  this.end = this._endImpl.bind( this );
59838
+ this.beginSpan = this._beginSpanImpl.bind( this );
59839
+ this.endSpan = this._endSpanImpl.bind( this );
59826
59840
  this._exposeGlobal();
59827
59841
  console.log( `[ProfilerService] enabled (profile: ${this._profile}) — call __gnsx_profiler.report() or downloadTrace() from the console` );
59828
59842
 
@@ -59834,6 +59848,8 @@ class ProfilerServiceClass {
59834
59848
  this._enabled = false;
59835
59849
  this.begin = NOOP;
59836
59850
  this.end = NOOP;
59851
+ this.beginSpan = NOOP_BEGIN_SPAN;
59852
+ this.endSpan = NOOP_END_SPAN;
59837
59853
  console.log( '[ProfilerService] disabled' );
59838
59854
 
59839
59855
  }
@@ -59869,22 +59885,8 @@ class ProfilerServiceClass {
59869
59885
 
59870
59886
  }
59871
59887
 
59872
- const traced = this._profile === 'full' && this.traceEvents.length < MAX_TRACE_EVENTS;
59873
- if ( traced ) {
59874
-
59875
- this.traceEvents.push( {
59876
- name: label,
59877
- ph: 'B',
59878
- ts: this._getTraceTimestamp( startTime ),
59879
- pid: 1,
59880
- tid: 1,
59881
- cat: 'gnsx',
59882
- } );
59883
-
59884
- }
59885
-
59886
59888
  performance.mark( startMark );
59887
- stack.push( { startTime, startMark, traced } );
59889
+ stack.push( { startTime, startMark } );
59888
59890
 
59889
59891
  // Push onto the global call stack for exclusive-time tracking.
59890
59892
  this.callStack.push( { label, childTime: 0 } );
@@ -59902,13 +59904,61 @@ class ProfilerServiceClass {
59902
59904
  if ( stack.length === 0 ) this.marks.delete( label );
59903
59905
 
59904
59906
  const now = performance.now();
59905
- const { startTime, startMark, traced } = mark;
59907
+ const { startTime, startMark } = mark;
59906
59908
  const duration = now - startTime;
59907
59909
  const endMark = `gnsx:${label}:end:${++ this._markId}`;
59908
59910
 
59909
59911
  performance.mark( endMark );
59910
59912
  performance.measure( `gnsx:${label}`, startMark, endMark );
59911
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
+
59912
59962
  let buffer = this.buffers.get( label );
59913
59963
  if ( ! buffer ) {
59914
59964
 
@@ -59920,7 +59970,7 @@ class ProfilerServiceClass {
59920
59970
  }
59921
59971
 
59922
59972
  const cursor = this.cursors.get( label );
59923
- buffer[ cursor ] = duration;
59973
+ buffer[ cursor ] = durationMs;
59924
59974
  this.cursors.set( label, ( cursor + 1 ) % RING_SIZE );
59925
59975
  this.counts.set( label, Math.min( ( this.counts.get( label ) + 1 ), RING_SIZE ) );
59926
59976
 
@@ -59932,7 +59982,7 @@ class ProfilerServiceClass {
59932
59982
  if ( top !== undefined && top.label === label ) {
59933
59983
 
59934
59984
  this.callStack.pop();
59935
- const selfTime = Math.max( 0, duration - top.childTime );
59985
+ const selfTime = Math.max( 0, durationMs - top.childTime );
59936
59986
 
59937
59987
  let selfBuffer = this.selfBuffers.get( label );
59938
59988
  if ( ! selfBuffer ) {
@@ -59951,7 +60001,7 @@ class ProfilerServiceClass {
59951
60001
 
59952
60002
  // Propagate inclusive duration to the parent scope's child accumulator.
59953
60003
  const parent = this.callStack.length > 0 ? this.callStack[ this.callStack.length - 1 ] : undefined;
59954
- if ( parent !== undefined ) parent.childTime += duration;
60004
+ if ( parent !== undefined ) parent.childTime += durationMs;
59955
60005
 
59956
60006
  } else {
59957
60007
 
@@ -59960,14 +60010,16 @@ class ProfilerServiceClass {
59960
60010
 
59961
60011
  }
59962
60012
 
59963
- if ( traced ) {
60013
+ if ( traceTid ) {
59964
60014
 
60015
+ const traceName = traceTid === TRACE_TID_ASYNC ? `${label} (promise)` : label;
59965
60016
  this.traceEvents.push( {
59966
- name: label,
59967
- ph: 'E',
59968
- 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 ) ),
59969
60021
  pid: 1,
59970
- tid: 1,
60022
+ tid: traceTid,
59971
60023
  cat: 'gnsx',
59972
60024
  } );
59973
60025
 
@@ -60078,13 +60130,53 @@ class ProfilerServiceClass {
60078
60130
  */
60079
60131
  exportChromeTrace() {
60080
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
+
60081
60177
  return {
60082
60178
  displayTimeUnit: 'ms',
60083
- traceEvents: [
60084
- { name: 'process_name', ph: 'M', pid: 1, args: { name: 'Genesys Profiler' } },
60085
- { name: 'thread_name', ph: 'M', pid: 1, tid: 1, args: { name: 'Main Thread' } },
60086
- ...this.traceEvents,
60087
- ],
60179
+ traceEvents: [ ...prefix, ...slices ],
60088
60180
  };
60089
60181
 
60090
60182
  }
@@ -60189,22 +60281,38 @@ class ProfilerServiceClass {
60189
60281
  * @property {number|undefined} selfFrameBudget Exclusive time as percentage of a 60 fps frame budget.
60190
60282
  */
60191
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).
60295
+ */
60296
+
60192
60297
  /**
60193
60298
  * @typedef {Object} ChromeTraceEvent
60194
60299
  * @property {string} name
60195
- * @property {'B'|'E'} ph
60300
+ * @property {'X'} ph
60196
60301
  * @property {number} ts
60302
+ * @property {number} dur
60197
60303
  * @property {1} pid
60198
- * @property {1} tid
60304
+ * @property {number} tid
60199
60305
  * @property {'gnsx'} cat
60200
60306
  */
60201
60307
 
60202
60308
  /**
60203
60309
  * @typedef {Object} ChromeTraceMetadataEvent
60310
+ * @property {'__metadata'} cat
60204
60311
  * @property {'process_name'|'thread_name'} name
60205
60312
  * @property {'M'} ph
60206
60313
  * @property {1} pid
60207
- * @property {1} [tid]
60314
+ * @property {number} [tid]
60315
+ * @property {0} ts
60208
60316
  * @property {{ name: string }} args
60209
60317
  */
60210
60318
 
@@ -60220,28 +60328,38 @@ class ProfilerServiceClass {
60220
60328
 
60221
60329
  const ProfilerService = new ProfilerServiceClass();
60222
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
+
60223
60341
  function applyProfileToMethod( label, descriptor ) {
60224
60342
 
60225
60343
  const original = descriptor.value;
60226
60344
 
60227
60345
  function profiled( ...args ) {
60228
60346
 
60229
- ProfilerService.begin( label );
60347
+ const span = ProfilerService.beginSpan( label );
60230
60348
  try {
60231
60349
 
60232
60350
  const result = original.apply( this, args );
60233
- if ( result instanceof Promise ) {
60351
+ if ( isThenable( result ) ) {
60234
60352
 
60235
- return result.finally( () => ProfilerService.end( label ) );
60353
+ return Promise.resolve( result ).finally( () => ProfilerService.endSpan( span, { asyncTimeline: true } ) );
60236
60354
 
60237
60355
  }
60238
60356
 
60239
- ProfilerService.end( label );
60357
+ ProfilerService.endSpan( span );
60240
60358
  return result;
60241
60359
 
60242
60360
  } catch ( error ) {
60243
60361
 
60244
- ProfilerService.end( label );
60362
+ ProfilerService.endSpan( span );
60245
60363
  throw error;
60246
60364
 
60247
60365
  }
@@ -60255,14 +60373,39 @@ function applyProfileToMethod( label, descriptor ) {
60255
60373
 
60256
60374
  /**
60257
60375
  * Method decorator that profiles the decorated method.
60258
- * The label is automatically set to `ClassName.methodName`.
60376
+ * Default label: `ClassName.methodName`. Pass a custom tag via `@profile('My tag')`.
60259
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
+ /**
60260
60403
  * @param {Object} target
60261
60404
  * @param {string|symbol} propertyKey
60262
60405
  * @param {PropertyDescriptor} descriptor
60263
60406
  * @return {PropertyDescriptor}
60264
60407
  */
60265
- function profile( target, propertyKey, descriptor ) {
60408
+ function defaultProfileDecorator( target, propertyKey, descriptor ) {
60266
60409
 
60267
60410
  const className = target.constructor?.name ?? 'Unknown';
60268
60411
  return applyProfileToMethod( `${className}.${String( propertyKey )}`, descriptor );