@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/build/three.cjs CHANGED
@@ -59687,6 +59687,512 @@ class TextureUtils {
59687
59687
 
59688
59688
  }
59689
59689
 
59690
+ // WITH_GENESYS
59691
+ /**
59692
+ * ProfilerService — per-label CPU timing with ring-buffer aggregation and DevTools integration.
59693
+ *
59694
+ * Activate via environment variable:
59695
+ * PROFILING=1 stats + trace (full profile)
59696
+ * PROFILING=stats stats only, no trace accumulation (lower overhead)
59697
+ *
59698
+ * Quick start (browser console):
59699
+ * __gnsx_profiler.enable()
59700
+ * // play for a few seconds
59701
+ * __gnsx_profiler.report() // sorted console.table (aggregated stats)
59702
+ * __gnsx_profiler.downloadTrace() // download gnsx-trace.json for Speedscope / Perfetto
59703
+ * __gnsx_profiler.reset() // clear samples and trace
59704
+ * __gnsx_profiler.disable()
59705
+ */
59706
+
59707
+ const RING_SIZE = 120;
59708
+ const FRAME_BUDGET_MS = 1000 / 60;
59709
+ /** Max events in the trace log (~50s at 60fps with 8 labels). */
59710
+ const MAX_TRACE_EVENTS = 50_000;
59711
+ const NOOP = () => {};
59712
+
59713
+ function _readProfilingEnv() {
59714
+
59715
+ const raw =
59716
+ ( typeof process !== 'undefined' ? process.env?.[ 'PROFILING' ] : undefined ) ??
59717
+ ( typeof globalThis !== 'undefined' ? globalThis[ '__gnsx_env__' ]?.[ 'PROFILING' ] : undefined ) ??
59718
+ ( typeof globalThis !== 'undefined' ? globalThis[ '__GNSX_PROFILING__' ] : undefined );
59719
+
59720
+ if ( ! raw || raw === '0' || raw === 'false' ) return false;
59721
+ if ( raw === 'stats' ) return 'stats';
59722
+ return 'full';
59723
+
59724
+ }
59725
+
59726
+ class ProfilerServiceClass {
59727
+
59728
+ constructor() {
59729
+
59730
+ const envProfile = _readProfilingEnv();
59731
+ this._profile = envProfile !== false ? envProfile : 'full';
59732
+ this._enabled = envProfile !== false;
59733
+
59734
+ /** @type {Map<string, Float64Array>} */
59735
+ this.buffers = new Map();
59736
+ /** @type {Map<string, number>} */
59737
+ this.cursors = new Map();
59738
+ /** @type {Map<string, number>} */
59739
+ this.counts = new Map();
59740
+ /** @type {Map<string, Array<{ startTime: number, startMark: string, traced: boolean }>>} */
59741
+ this.marks = new Map();
59742
+ /** @type {ChromeTraceEvent[]} */
59743
+ this.traceEvents = [];
59744
+ this.traceStartTime = 0;
59745
+ this._markId = 0;
59746
+
59747
+ if ( this._enabled ) {
59748
+
59749
+ this.traceStartTime = performance.now();
59750
+ this.begin = this._beginImpl.bind( this );
59751
+ this.end = this._endImpl.bind( this );
59752
+ this._exposeGlobal();
59753
+ console.log( `[ProfilerService] auto-started from env (profile: ${this._profile})` );
59754
+
59755
+ } else {
59756
+
59757
+ this.begin = NOOP;
59758
+ this.end = NOOP;
59759
+
59760
+ }
59761
+
59762
+ }
59763
+
59764
+ /**
59765
+ * Set the active profile. Must be called before enable() to take effect on the current session.
59766
+ *
59767
+ * @param {'full'|'stats'} profile
59768
+ */
59769
+ setProfile( profile ) {
59770
+
59771
+ this._profile = profile;
59772
+
59773
+ }
59774
+
59775
+ /**
59776
+ * @return {'full'|'stats'}
59777
+ */
59778
+ getProfile() {
59779
+
59780
+ return this._profile;
59781
+
59782
+ }
59783
+
59784
+ enable() {
59785
+
59786
+ this._clearState();
59787
+ this._enabled = true;
59788
+ this.begin = this._beginImpl.bind( this );
59789
+ this.end = this._endImpl.bind( this );
59790
+ this._exposeGlobal();
59791
+ console.log( `[ProfilerService] enabled (profile: ${this._profile}) — call __gnsx_profiler.report() or downloadTrace() from the console` );
59792
+
59793
+ }
59794
+
59795
+ disable() {
59796
+
59797
+ this._clearState();
59798
+ this._enabled = false;
59799
+ this.begin = NOOP;
59800
+ this.end = NOOP;
59801
+ console.log( '[ProfilerService] disabled' );
59802
+
59803
+ }
59804
+
59805
+ isEnabled() {
59806
+
59807
+ return this._enabled;
59808
+
59809
+ }
59810
+
59811
+ _exposeGlobal() {
59812
+
59813
+ if ( typeof window !== 'undefined' ) {
59814
+
59815
+ window.__gnsx_profiler = this;
59816
+
59817
+ }
59818
+
59819
+ }
59820
+
59821
+ /**
59822
+ * @param {string} label
59823
+ */
59824
+ _beginImpl( label ) {
59825
+
59826
+ const startTime = performance.now();
59827
+ const startMark = `gnsx:${label}:start:${++ this._markId}`;
59828
+ let stack = this.marks.get( label );
59829
+ if ( stack === undefined ) {
59830
+
59831
+ stack = [];
59832
+ this.marks.set( label, stack );
59833
+
59834
+ }
59835
+
59836
+ const traced = this._profile === 'full' && this.traceEvents.length < MAX_TRACE_EVENTS;
59837
+ if ( traced ) {
59838
+
59839
+ this.traceEvents.push( {
59840
+ name: label,
59841
+ ph: 'B',
59842
+ ts: this._getTraceTimestamp( startTime ),
59843
+ pid: 1,
59844
+ tid: 1,
59845
+ cat: 'gnsx',
59846
+ } );
59847
+
59848
+ }
59849
+
59850
+ performance.mark( startMark );
59851
+ stack.push( { startTime, startMark, traced } );
59852
+
59853
+ }
59854
+
59855
+ /**
59856
+ * @param {string} label
59857
+ */
59858
+ _endImpl( label ) {
59859
+
59860
+ const stack = this.marks.get( label );
59861
+ const mark = stack?.pop();
59862
+ if ( mark === undefined ) return;
59863
+ if ( stack.length === 0 ) this.marks.delete( label );
59864
+
59865
+ const now = performance.now();
59866
+ const { startTime, startMark, traced } = mark;
59867
+ const duration = now - startTime;
59868
+ const endMark = `gnsx:${label}:end:${++ this._markId}`;
59869
+
59870
+ performance.mark( endMark );
59871
+ performance.measure( `gnsx:${label}`, startMark, endMark );
59872
+
59873
+ let buffer = this.buffers.get( label );
59874
+ if ( ! buffer ) {
59875
+
59876
+ buffer = new Float64Array( RING_SIZE );
59877
+ this.buffers.set( label, buffer );
59878
+ this.cursors.set( label, 0 );
59879
+ this.counts.set( label, 0 );
59880
+
59881
+ }
59882
+
59883
+ const cursor = this.cursors.get( label );
59884
+ buffer[ cursor ] = duration;
59885
+ this.cursors.set( label, ( cursor + 1 ) % RING_SIZE );
59886
+ this.counts.set( label, Math.min( ( this.counts.get( label ) + 1 ), RING_SIZE ) );
59887
+
59888
+ if ( traced ) {
59889
+
59890
+ this.traceEvents.push( {
59891
+ name: label,
59892
+ ph: 'E',
59893
+ ts: this._getTraceTimestamp( now ),
59894
+ pid: 1,
59895
+ tid: 1,
59896
+ cat: 'gnsx',
59897
+ } );
59898
+
59899
+ }
59900
+
59901
+ }
59902
+
59903
+ /**
59904
+ * @param {string} label
59905
+ * @return {number[]}
59906
+ */
59907
+ getValidSamples( label ) {
59908
+
59909
+ const buffer = this.buffers.get( label );
59910
+ const count = this.counts.get( label ) ?? 0;
59911
+ if ( ! buffer || count === 0 ) return [];
59912
+ return count < RING_SIZE
59913
+ ? Array.from( buffer.subarray( 0, count ) )
59914
+ : Array.from( buffer );
59915
+
59916
+ }
59917
+
59918
+ /**
59919
+ * @param {string} label
59920
+ * @return {import('./ProfilerService.js').ProfilerStats|null}
59921
+ */
59922
+ getStats( label ) {
59923
+
59924
+ const samples = this.getValidSamples( label );
59925
+ if ( samples.length === 0 ) return null;
59926
+
59927
+ const sorted = [ ...samples ].sort( ( a, b ) => a - b );
59928
+ const avg = sorted.reduce( ( a, b ) => a + b, 0 ) / sorted.length;
59929
+
59930
+ return {
59931
+ label,
59932
+ samples: samples.length,
59933
+ avg,
59934
+ min: sorted[ 0 ],
59935
+ max: sorted[ sorted.length - 1 ],
59936
+ p95: sorted[ Math.floor( sorted.length * 0.95 ) ],
59937
+ frameBudget: ( avg / FRAME_BUDGET_MS ) * 100,
59938
+ };
59939
+
59940
+ }
59941
+
59942
+ /**
59943
+ * @return {import('./ProfilerService.js').ProfilerStats[]}
59944
+ */
59945
+ getAllStats() {
59946
+
59947
+ return [ ...this.buffers.keys() ]
59948
+ .map( label => this.getStats( label ) )
59949
+ .filter( s => s !== null );
59950
+
59951
+ }
59952
+
59953
+ report() {
59954
+
59955
+ const stats = this.getAllStats();
59956
+ if ( stats.length === 0 ) {
59957
+
59958
+ console.log( '[ProfilerService] No data. Enable profiling first and wait a few frames.' );
59959
+ return;
59960
+
59961
+ }
59962
+
59963
+ stats.sort( ( a, b ) => b.avg - a.avg );
59964
+ console.table(
59965
+ stats.map( s => ( {
59966
+ label: s.label,
59967
+ 'avg ms': s.avg.toFixed( 3 ),
59968
+ 'min ms': s.min.toFixed( 3 ),
59969
+ 'max ms': s.max.toFixed( 3 ),
59970
+ 'p95 ms': s.p95.toFixed( 3 ),
59971
+ 'budget %': s.frameBudget.toFixed( 1 ) + '%',
59972
+ samples: s.samples,
59973
+ } ) )
59974
+ );
59975
+
59976
+ }
59977
+
59978
+ /**
59979
+ * @return {import('./ProfilerService.js').ChromeTrace}
59980
+ */
59981
+ exportChromeTrace() {
59982
+
59983
+ return {
59984
+ displayTimeUnit: 'ms',
59985
+ traceEvents: [
59986
+ { name: 'process_name', ph: 'M', pid: 1, args: { name: 'Genesys Profiler' } },
59987
+ { name: 'thread_name', ph: 'M', pid: 1, tid: 1, args: { name: 'Main Thread' } },
59988
+ ...this.traceEvents,
59989
+ ],
59990
+ };
59991
+
59992
+ }
59993
+
59994
+ /**
59995
+ * @param {number} time
59996
+ * @return {number}
59997
+ */
59998
+ _getTraceTimestamp( time ) {
59999
+
60000
+ return ( time - this.traceStartTime ) * 1000;
60001
+
60002
+ }
60003
+
60004
+ /**
60005
+ * @param {string} [filename='gnsx-trace.json']
60006
+ */
60007
+ downloadTrace( filename = 'gnsx-trace.json' ) {
60008
+
60009
+ if ( typeof document === 'undefined' ) {
60010
+
60011
+ console.log( '[ProfilerService] downloadTrace() only works in the browser. Use exportChromeTrace() in Node.js.' );
60012
+ return;
60013
+
60014
+ }
60015
+
60016
+ if ( this._profile !== 'full' ) {
60017
+
60018
+ console.log( `[ProfilerService] No trace — current profile is '${this._profile}'. Use setProfile('full') before enabling.` );
60019
+ return;
60020
+
60021
+ }
60022
+
60023
+ if ( this.traceEvents.length === 0 ) {
60024
+
60025
+ console.log( '[ProfilerService] No trace events. Enable the profiler and play for a few seconds first.' );
60026
+ return;
60027
+
60028
+ }
60029
+
60030
+ const json = JSON.stringify( this.exportChromeTrace() );
60031
+ const blob = new Blob( [ json ], { type: 'application/json' } );
60032
+ const url = URL.createObjectURL( blob );
60033
+ const a = document.createElement( 'a' );
60034
+ a.href = url;
60035
+ a.download = filename;
60036
+ a.click();
60037
+ URL.revokeObjectURL( url );
60038
+ console.log( `[ProfilerService] Trace downloaded: ${filename} (${this.traceEvents.length} events)` );
60039
+
60040
+ }
60041
+
60042
+ /**
60043
+ * @return {import('./ProfilerService.js').ProfilerStats[]}
60044
+ */
60045
+ exportJSON() {
60046
+
60047
+ return this.getAllStats();
60048
+
60049
+ }
60050
+
60051
+ _clearState() {
60052
+
60053
+ this.buffers.clear();
60054
+ this.cursors.clear();
60055
+ this.counts.clear();
60056
+ this.marks.clear();
60057
+ this.traceEvents = [];
60058
+ this.traceStartTime = performance.now();
60059
+ this._markId = 0;
60060
+
60061
+ }
60062
+
60063
+ reset() {
60064
+
60065
+ this._clearState();
60066
+ console.log( '[ProfilerService] data reset' );
60067
+
60068
+ }
60069
+
60070
+ }
60071
+
60072
+ /**
60073
+ * @typedef {Object} ProfilerStats
60074
+ * @property {string} label
60075
+ * @property {number} samples
60076
+ * @property {number} avg
60077
+ * @property {number} min
60078
+ * @property {number} max
60079
+ * @property {number} p95
60080
+ * @property {number} frameBudget Percentage of a 60 fps frame budget (16.67 ms)
60081
+ */
60082
+
60083
+ /**
60084
+ * @typedef {Object} ChromeTraceEvent
60085
+ * @property {string} name
60086
+ * @property {'B'|'E'} ph
60087
+ * @property {number} ts
60088
+ * @property {1} pid
60089
+ * @property {1} tid
60090
+ * @property {'gnsx'} cat
60091
+ */
60092
+
60093
+ /**
60094
+ * @typedef {Object} ChromeTraceMetadataEvent
60095
+ * @property {'process_name'|'thread_name'} name
60096
+ * @property {'M'} ph
60097
+ * @property {1} pid
60098
+ * @property {1} [tid]
60099
+ * @property {{ name: string }} args
60100
+ */
60101
+
60102
+ /**
60103
+ * @typedef {Object} ChromeTrace
60104
+ * @property {'ms'} displayTimeUnit
60105
+ * @property {Array<ChromeTraceEvent|ChromeTraceMetadataEvent>} traceEvents
60106
+ */
60107
+
60108
+ /**
60109
+ * @typedef {'full'|'stats'} ProfilingProfile
60110
+ */
60111
+
60112
+ const ProfilerService = new ProfilerServiceClass();
60113
+
60114
+ function applyProfileToMethod( label, descriptor ) {
60115
+
60116
+ if ( _readProfilingEnv() === false ) return descriptor;
60117
+
60118
+ const original = descriptor.value;
60119
+
60120
+ function profiled( ...args ) {
60121
+
60122
+ ProfilerService.begin( label );
60123
+ try {
60124
+
60125
+ const result = original.apply( this, args );
60126
+ if ( result instanceof Promise ) {
60127
+
60128
+ return result.finally( () => ProfilerService.end( label ) );
60129
+
60130
+ }
60131
+
60132
+ ProfilerService.end( label );
60133
+ return result;
60134
+
60135
+ } catch ( error ) {
60136
+
60137
+ ProfilerService.end( label );
60138
+ throw error;
60139
+
60140
+ }
60141
+
60142
+ }
60143
+
60144
+ descriptor.value = profiled;
60145
+ return descriptor;
60146
+
60147
+ }
60148
+
60149
+ /**
60150
+ * Method decorator that profiles the decorated method.
60151
+ * The label is automatically set to `ClassName.methodName`.
60152
+ *
60153
+ * @param {Object} target
60154
+ * @param {string|symbol} propertyKey
60155
+ * @param {PropertyDescriptor} descriptor
60156
+ * @return {PropertyDescriptor}
60157
+ */
60158
+ function profile( target, propertyKey, descriptor ) {
60159
+
60160
+ const className = target.constructor?.name ?? 'Unknown';
60161
+ return applyProfileToMethod( `${className}.${String( propertyKey )}`, descriptor );
60162
+
60163
+ }
60164
+
60165
+ /**
60166
+ * Class decorator that profiles every method of the decorated class.
60167
+ *
60168
+ * @template T
60169
+ * @param {T} constructor
60170
+ * @return {T}
60171
+ */
60172
+ function profileClass( constructor ) {
60173
+
60174
+ const proto = constructor.prototype;
60175
+ for ( const key of Object.getOwnPropertyNames( proto ) ) {
60176
+
60177
+ if ( key === 'constructor' ) continue;
60178
+ const descriptor = Object.getOwnPropertyDescriptor( proto, key );
60179
+ if ( ! descriptor || typeof descriptor.value !== 'function' ) continue;
60180
+ Object.defineProperty(
60181
+ proto,
60182
+ key,
60183
+ applyProfileToMethod( `${constructor.name}.${key}`, { ...descriptor } )
60184
+ );
60185
+
60186
+ }
60187
+
60188
+ return constructor;
60189
+
60190
+ }
60191
+ // !WITH_GENESYS
60192
+
60193
+ // !WITH_GENESYS
60194
+
60195
+
59690
60196
  if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {
59691
60197
 
59692
60198
  __THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'register', { detail: {
@@ -79528,6 +80034,7 @@ exports.PointsMaterial = PointsMaterial;
79528
80034
  exports.PolarGridHelper = PolarGridHelper;
79529
80035
  exports.PolyhedronGeometry = PolyhedronGeometry;
79530
80036
  exports.PositionalAudio = PositionalAudio;
80037
+ exports.ProfilerService = ProfilerService;
79531
80038
  exports.PropertyBinding = PropertyBinding;
79532
80039
  exports.PropertyMixer = PropertyMixer;
79533
80040
  exports.QuadraticBezierCurve = QuadraticBezierCurve;
@@ -79690,6 +80197,8 @@ exports.createCanvasElement = createCanvasElement;
79690
80197
  exports.error = error;
79691
80198
  exports.getConsoleFunction = getConsoleFunction;
79692
80199
  exports.log = log;
80200
+ exports.profile = profile;
80201
+ exports.profileClass = profileClass;
79693
80202
  exports.setConsoleFunction = setConsoleFunction;
79694
80203
  exports.warn = warn;
79695
80204
  exports.warnOnce = warnOnce;