1- import { performance } from 'node:perf_hooks' ;
21import { isEnvVarEnabled } from '../env.js' ;
32import { type FatalKind , subscribeProcessExit } from '../exit-process.js' ;
43import {
@@ -13,7 +12,7 @@ import type {
1312 MarkerPayload ,
1413} from '../user-timing-extensibility-api.type.js' ;
1514import { ShardedWal } from '../wal-sharded.js' ;
16- import { type WalFormat , WriteAheadLogFile } from '../wal.js' ;
15+ import { type WalFormat , type WalRecord , WriteAheadLogFile } from '../wal.js' ;
1716import {
1817 PROFILER_DEBUG_MEASURE_PREFIX ,
1918 PROFILER_ENABLED_ENV_VAR ,
@@ -24,15 +23,15 @@ import {
2423} from './constants.js' ;
2524import { Profiler , type ProfilerOptions } from './profiler.js' ;
2625
27- export type ProfilerBufferOptions < DomainEvents extends object > = Omit <
26+ export type ProfilerBufferOptions < DomainEvents extends WalRecord > = Omit <
2827 PerformanceObserverOptions < DomainEvents > ,
2928 'sink' | 'encodePerfEntry'
3029> ;
31- export type ProfilerFormat < DomainEvents extends object > = Partial <
30+ export type ProfilerFormat < DomainEvents extends WalRecord > = Partial <
3231 WalFormat < DomainEvents >
3332> &
3433 Pick < PerformanceObserverOptions < DomainEvents > , 'encodePerfEntry' > ;
35- export type PersistOptions < DomainEvents extends object > = {
34+ export type PersistOptions < DomainEvents extends WalRecord > = {
3635 /**
3736 * Output directory for WAL shards and final files.
3837 * @default 'tmp/profiles'
@@ -58,19 +57,13 @@ export type PersistOptions<DomainEvents extends object> = {
5857 * @template Tracks - Record type defining available track names and their configurations
5958 */
6059export type NodejsProfilerOptions <
61- DomainEvents extends object ,
60+ DomainEvents extends WalRecord ,
6261 Tracks extends Record < string , Omit < ActionTrackEntryPayload , 'dataType' > > ,
6362> = ProfilerOptions < Tracks > &
6463 ProfilerBufferOptions < DomainEvents > &
6564 PersistOptions < DomainEvents > ;
6665
6766export type NodeJsProfilerState = 'idle' | 'running' | 'closed' ;
68- type NodeJsProfilerTransitionReason =
69- | 'enable'
70- | 'disable'
71- | 'process-exit'
72- | 'fatal-error'
73- | 'api-call' ;
7467
7568/**
7669 * Performance profiler with automatic process exit handling for buffered performance data.
@@ -89,7 +82,7 @@ type NodeJsProfilerTransitionReason =
8982 * @template Tracks - Record type defining available track names and their configurations
9083 */
9184export class NodejsProfiler <
92- DomainEvents extends object ,
85+ DomainEvents extends WalRecord ,
9386 Tracks extends Record < string , ActionTrackEntryPayload > = Record <
9487 string ,
9588 ActionTrackEntryPayload
@@ -145,58 +138,53 @@ export class NodejsProfiler<
145138 captureBufferedEntries,
146139 flushThreshold,
147140 maxQueueSize,
141+ debug : this . isDebugMode ( )
148142 } ) ;
149143
150144 this . #unsubscribeExitHandlers = subscribeProcessExit ( {
151- onError : ( error : unknown , kind : FatalKind ) => {
152- this . #fatalErrorMarker ( error , kind ) ;
153- if ( this . #state !== 'closed' ) {
154- this . #transition ( 'closed' , 'fatal-error' ) ;
155- }
145+ onError : (
146+ error : unknown ,
147+ kind : 'uncaughtException' | 'unhandledRejection' ,
148+ ) => {
149+ this . #handleFatalError ( error , kind ) ;
156150 } ,
157151 onExit : ( _code : number ) => {
158- if ( this . #state !== 'closed' ) {
159- this . #transition( 'closed' , 'process-exit' ) ;
160- }
152+ this . close ( ) ;
161153 } ,
162154 } ) ;
163155
164156 const initialEnabled =
165157 options . enabled ?? isEnvVarEnabled ( PROFILER_ENABLED_ENV_VAR ) ;
166158 if ( initialEnabled ) {
167- this . #transition( 'running' , 'enable' ) ;
159+ this . #transition( 'running' ) ;
168160 }
169161 }
170162
171163 /**
172164 * Creates a performance marker for a profiler state transition.
173165 */
174- #logTransition(
175- from : NodeJsProfilerState ,
176- to : NodeJsProfilerState ,
177- reason : NodeJsProfilerTransitionReason ,
178- ) : void {
179- if ( ! this . isDebugMode ( ) ) {
180- return ;
181- }
182-
183- performance . mark (
184- `${ PROFILER_DEBUG_MEASURE_PREFIX } :transition` ,
185- asOptions (
186- markerPayload ( {
187- color : 'warning' ,
188- tooltipText : `Profiler transition: ${ from } -> ${ to } ` ,
189- properties : [
190- [ 'From' , from ] ,
191- [ 'To' , to ] ,
192- [ 'Reason' , reason ] ,
193- ...objectToEntries ( this . stats ) ,
194- ] ,
195- } ) ,
196- ) ,
197- ) ;
166+ #transitionMarker( transition : string ) : void {
167+ const transitionMarkerPayload : MarkerPayload = {
168+ dataType : 'marker' ,
169+ color : 'primary' ,
170+ tooltipText : `Profiler state transition: ${ transition } ` ,
171+ properties : [ [ 'Transition' , transition ] , ...objectToEntries ( this . stats ) ] ,
172+ } ;
173+ this . marker ( transition , transitionMarkerPayload ) ;
198174 }
199175
176+ /**
177+ * Handles fatal errors by marking them and shutting down the profiler.
178+ * @param error - The error that occurred
179+ * @param kind - The kind of fatal error (uncaughtException or unhandledRejection)
180+ */
181+ #handleFatalError(
182+ error : unknown ,
183+ kind : 'uncaughtException' | 'unhandledRejection' ,
184+ ) : void {
185+ this . #fatalErrorMarker( error , kind ) ;
186+ this . close ( ) ; // Ensures buffers flush and sink finalizes
187+ }
200188 /**
201189 * Creates a fatal errors by marking them and shutting down the profiler.
202190 * @param error - The error that occurred
@@ -221,20 +209,17 @@ export class NodejsProfiler<
221209 * - `idle -> closed`: Closes sink if it was opened and finalizes shards (irreversible)
222210 *
223211 * @param next - The target state to transition to
224- * @param reason - The caller intent for this transition
225212 * @throws {Error } If attempting to transition from 'closed' state or invalid transition
226213 */
227- #transition( next : NodeJsProfilerState , reason : NodeJsProfilerTransitionReason ) : void {
214+ #transition( next : NodeJsProfilerState ) : void {
228215 if ( this . #state === next ) {
229216 return ;
230217 }
231218 if ( this . #state === 'closed' ) {
232219 throw new Error ( 'Profiler already closed' ) ;
233220 }
234221
235- const prev = this . #state;
236- const transition = `${ prev } ->${ next } ` ;
237- this . #logTransition( prev , next , reason ) ;
222+ const transition = `${ this . #state} ->${ next } ` ;
238223
239224 switch ( transition ) {
240225 case 'idle->running' :
@@ -259,7 +244,7 @@ export class NodejsProfiler<
259244 break ;
260245
261246 default :
262- throw new Error ( `Invalid transition: ${ prev } -> ${ next } ` ) ;
247+ throw new Error ( `Invalid transition: ${ this . #state } -> ${ next } ` ) ;
263248 }
264249
265250 this . #state = next ;
@@ -273,7 +258,7 @@ export class NodejsProfiler<
273258 if ( this . #state === 'closed' ) {
274259 return ;
275260 }
276- this . #transition( 'closed' , 'api-call' ) ;
261+ this . #transition( 'closed' ) ;
277262 }
278263
279264 /** @returns Whether profiler is in 'running' state */
@@ -284,9 +269,9 @@ export class NodejsProfiler<
284269 /** Enables profiling (start/stop) */
285270 override setEnabled ( enabled : boolean ) : void {
286271 if ( enabled ) {
287- this . #transition( 'running' , 'api-call' ) ;
272+ this . #transition( 'running' ) ;
288273 } else {
289- this . #transition( 'idle' , 'api-call' ) ;
274+ this . #transition( 'idle' ) ;
290275 }
291276 }
292277
@@ -295,21 +280,15 @@ export class NodejsProfiler<
295280 return this . #state;
296281 }
297282
298- /** @returns Whether debug mode is enabled */
299- get debug ( ) : boolean {
300- return this . isDebugMode ( ) ;
301- }
302-
303283 /** @returns Queue statistics and profiling state for monitoring */
304- override get stats ( ) {
284+ get stats ( ) {
305285 const {
306286 state : sharderState ,
307287 isCoordinator,
308288 ...sharderStats
309289 } = this . #sharder. getStats ( ) ;
310290
311291 return {
312- ...super . stats ,
313292 profilerState : this . #state,
314293 debug : this . isDebugMode ( ) ,
315294 sharderState,
0 commit comments