@code-pushup/utils 0.110.0 → 0.111.1
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/package.json +2 -2
- package/src/lib/exit-process.d.ts +11 -1
- package/src/lib/exit-process.js +36 -12
- package/src/lib/exit-process.js.map +1 -1
- package/src/lib/performance-observer.d.ts +184 -3
- package/src/lib/performance-observer.js +240 -27
- package/src/lib/performance-observer.js.map +1 -1
- package/src/lib/profiler/constants.d.ts +23 -5
- package/src/lib/profiler/constants.js +23 -5
- package/src/lib/profiler/constants.js.map +1 -1
- package/src/lib/profiler/profiler-node.d.ts +101 -0
- package/src/lib/profiler/profiler-node.js +217 -0
- package/src/lib/profiler/profiler-node.js.map +1 -0
- package/src/lib/profiler/profiler.js +5 -4
- package/src/lib/profiler/profiler.js.map +1 -1
- package/src/lib/wal.d.ts +31 -3
- package/src/lib/wal.js +30 -3
- package/src/lib/wal.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-pushup/utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.111.1",
|
|
4
4
|
"description": "Low-level utilities (helper functions, etc.) used by Code PushUp CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/code-pushup/cli/tree/main/packages/utils#readme",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"node": ">=18.2.0"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@code-pushup/models": "0.
|
|
30
|
+
"@code-pushup/models": "0.111.1",
|
|
31
31
|
"ansis": "^3.3.0",
|
|
32
32
|
"build-md": "^0.4.2",
|
|
33
33
|
"bundle-require": "^5.1.0",
|
|
@@ -18,4 +18,14 @@ export type ExitHandlerOptions = {
|
|
|
18
18
|
exitOnSignal?: boolean;
|
|
19
19
|
fatalExitCode?: number;
|
|
20
20
|
};
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
*
|
|
23
|
+
* @param options - Options for the exit handler
|
|
24
|
+
* @param options.onExit - Callback to be called when the process exits
|
|
25
|
+
* @param options.onError - Callback to be called when an error occurs
|
|
26
|
+
* @param options.exitOnFatal - Whether to exit the process on fatal errors
|
|
27
|
+
* @param options.exitOnSignal - Whether to exit the process on signals
|
|
28
|
+
* @param options.fatalExitCode - The exit code to use for fatal errors
|
|
29
|
+
* @returns A function to unsubscribe from the exit handlers
|
|
30
|
+
*/
|
|
31
|
+
export declare function subscribeProcessExit(options?: ExitHandlerOptions): () => void;
|
package/src/lib/exit-process.js
CHANGED
|
@@ -16,10 +16,21 @@ export const SIGNAL_EXIT_CODES = () => {
|
|
|
16
16
|
};
|
|
17
17
|
};
|
|
18
18
|
export const DEFAULT_FATAL_EXIT_CODE = 1;
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
*
|
|
21
|
+
* @param options - Options for the exit handler
|
|
22
|
+
* @param options.onExit - Callback to be called when the process exits
|
|
23
|
+
* @param options.onError - Callback to be called when an error occurs
|
|
24
|
+
* @param options.exitOnFatal - Whether to exit the process on fatal errors
|
|
25
|
+
* @param options.exitOnSignal - Whether to exit the process on signals
|
|
26
|
+
* @param options.fatalExitCode - The exit code to use for fatal errors
|
|
27
|
+
* @returns A function to unsubscribe from the exit handlers
|
|
28
|
+
*/
|
|
29
|
+
// eslint-disable-next-line max-lines-per-function
|
|
30
|
+
export function subscribeProcessExit(options = {}) {
|
|
20
31
|
// eslint-disable-next-line functional/no-let
|
|
21
32
|
let closedReason;
|
|
22
|
-
const { onExit, onError, exitOnFatal, exitOnSignal, fatalExitCode = DEFAULT_FATAL_EXIT_CODE, } = options;
|
|
33
|
+
const { onExit, onError, exitOnFatal = false, exitOnSignal = false, fatalExitCode = DEFAULT_FATAL_EXIT_CODE, } = options;
|
|
23
34
|
const close = (code, reason) => {
|
|
24
35
|
if (closedReason) {
|
|
25
36
|
return;
|
|
@@ -27,7 +38,7 @@ export function installExitHandlers(options = {}) {
|
|
|
27
38
|
closedReason = reason;
|
|
28
39
|
onExit?.(code, reason);
|
|
29
40
|
};
|
|
30
|
-
|
|
41
|
+
const uncaughtExceptionHandler = (err) => {
|
|
31
42
|
onError?.(err, 'uncaughtException');
|
|
32
43
|
if (exitOnFatal) {
|
|
33
44
|
close(fatalExitCode, {
|
|
@@ -35,8 +46,8 @@ export function installExitHandlers(options = {}) {
|
|
|
35
46
|
fatal: 'uncaughtException',
|
|
36
47
|
});
|
|
37
48
|
}
|
|
38
|
-
}
|
|
39
|
-
|
|
49
|
+
};
|
|
50
|
+
const unhandledRejectionHandler = (reason) => {
|
|
40
51
|
onError?.(reason, 'unhandledRejection');
|
|
41
52
|
if (exitOnFatal) {
|
|
42
53
|
close(fatalExitCode, {
|
|
@@ -44,21 +55,34 @@ export function installExitHandlers(options = {}) {
|
|
|
44
55
|
fatal: 'unhandledRejection',
|
|
45
56
|
});
|
|
46
57
|
}
|
|
47
|
-
}
|
|
48
|
-
['SIGINT', 'SIGTERM', 'SIGQUIT'].
|
|
49
|
-
|
|
58
|
+
};
|
|
59
|
+
const signalHandlers = ['SIGINT', 'SIGTERM', 'SIGQUIT'].map(signal => {
|
|
60
|
+
const handler = () => {
|
|
50
61
|
close(SIGNAL_EXIT_CODES()[signal], { kind: 'signal', signal });
|
|
51
62
|
if (exitOnSignal) {
|
|
52
|
-
// eslint-disable-next-line n/no-process-exit
|
|
63
|
+
// eslint-disable-next-line unicorn/no-process-exit,n/no-process-exit
|
|
53
64
|
process.exit(SIGNAL_EXIT_CODES()[signal]);
|
|
54
65
|
}
|
|
55
|
-
}
|
|
66
|
+
};
|
|
67
|
+
process.on(signal, handler);
|
|
68
|
+
return { signal, handler };
|
|
56
69
|
});
|
|
57
|
-
|
|
70
|
+
const exitHandler = (code) => {
|
|
58
71
|
if (closedReason) {
|
|
59
72
|
return;
|
|
60
73
|
}
|
|
61
74
|
close(code, { kind: 'exit' });
|
|
62
|
-
}
|
|
75
|
+
};
|
|
76
|
+
process.on('uncaughtException', uncaughtExceptionHandler);
|
|
77
|
+
process.on('unhandledRejection', unhandledRejectionHandler);
|
|
78
|
+
process.on('exit', exitHandler);
|
|
79
|
+
return () => {
|
|
80
|
+
process.removeListener('uncaughtException', uncaughtExceptionHandler);
|
|
81
|
+
process.removeListener('unhandledRejection', unhandledRejectionHandler);
|
|
82
|
+
process.removeListener('exit', exitHandler);
|
|
83
|
+
signalHandlers.forEach(({ signal, handler }) => {
|
|
84
|
+
process.removeListener(signal, handler);
|
|
85
|
+
});
|
|
86
|
+
};
|
|
63
87
|
}
|
|
64
88
|
//# sourceMappingURL=exit-process.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exit-process.js","sourceRoot":"","sources":["../../../src/lib/exit-process.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,6DAA6D;AAC7D,uOAAuO;AACvO,MAAM,4BAA4B,GAAG,GAAG,CAAC;AACzC,MAAM,kBAAkB,GAAG,CAAC,YAAoB,EAAE,EAAE,CAClD,4BAA4B,GAAG,YAAY,CAAC;AAE9C,MAAM,WAAW,GAAG,CAAC,CAAC;AACtB,MAAM,YAAY,GAAG,EAAE,CAAC;AACxB,MAAM,YAAY,GAAG,CAAC,CAAC;AAEvB,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAA+B,EAAE;IAChE,MAAM,gBAAgB,GAAG,EAAE,CAAC,QAAQ,EAAE,KAAK,OAAO,CAAC;IACnD,OAAO;QACL,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,kBAAkB,CAAC,WAAW,CAAC;QACxE,OAAO,EAAE,kBAAkB,CAAC,YAAY,CAAC;QACzC,OAAO,EAAE,kBAAkB,CAAC,YAAY,CAAC;KAC1C,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAkBzC,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"exit-process.js","sourceRoot":"","sources":["../../../src/lib/exit-process.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,18 +1,199 @@
|
|
|
1
1
|
import { type PerformanceEntry } from 'node:perf_hooks';
|
|
2
2
|
import type { AppendableSink } from './wal.js';
|
|
3
|
+
/**
|
|
4
|
+
* Encoder that converts PerformanceEntry to domain events.
|
|
5
|
+
*
|
|
6
|
+
* Pure function that transforms performance entries into domain events.
|
|
7
|
+
* Should be stateless, synchronous, and have no side effects.
|
|
8
|
+
* Returns a readonly array of encoded items.
|
|
9
|
+
*/
|
|
10
|
+
export type PerformanceEntryEncoder<F> = (entry: PerformanceEntry) => readonly F[];
|
|
11
|
+
/**
|
|
12
|
+
* Default threshold for triggering queue flushes based on queue length.
|
|
13
|
+
* When the queue length reaches (maxQueueSize - flushThreshold),
|
|
14
|
+
* a flush is triggered to prevent overflow. This provides a buffer zone
|
|
15
|
+
* before hitting the maximum queue capacity.
|
|
16
|
+
*/
|
|
3
17
|
export declare const DEFAULT_FLUSH_THRESHOLD = 20;
|
|
18
|
+
/**
|
|
19
|
+
* Default maximum number of items allowed in the queue before entries are dropped.
|
|
20
|
+
* This acts as a memory safety limit to prevent unbounded memory growth
|
|
21
|
+
* in case of sink slowdown or high-frequency performance entries.
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_MAX_QUEUE_SIZE = 10000;
|
|
24
|
+
/**
|
|
25
|
+
* Validates the flush threshold configuration to ensure sensible bounds.
|
|
26
|
+
*
|
|
27
|
+
* The flush threshold must be positive and cannot exceed the maximum queue size,
|
|
28
|
+
* as it represents a buffer zone within the queue capacity.
|
|
29
|
+
*
|
|
30
|
+
* @param flushThreshold - The threshold value to validate (must be > 0)
|
|
31
|
+
* @param maxQueueSize - The maximum queue size for comparison (flushThreshold <= maxQueueSize)
|
|
32
|
+
* @throws {Error} If flushThreshold is not positive or exceeds maxQueueSize
|
|
33
|
+
*/
|
|
34
|
+
export declare function validateFlushThreshold(flushThreshold: number, maxQueueSize: number): void;
|
|
35
|
+
/**
|
|
36
|
+
* Configuration options for the PerformanceObserverSink.
|
|
37
|
+
*
|
|
38
|
+
* @template T - The type of encoded performance data that will be written to the sink
|
|
39
|
+
*/
|
|
4
40
|
export type PerformanceObserverOptions<T> = {
|
|
41
|
+
/**
|
|
42
|
+
* The sink where encoded performance entries will be written.
|
|
43
|
+
* Must implement the AppendableSink interface for handling the encoded data.
|
|
44
|
+
*/
|
|
5
45
|
sink: AppendableSink<T>;
|
|
6
|
-
|
|
7
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Function that encodes raw PerformanceEntry objects into domain-specific types.
|
|
48
|
+
* This transformer converts Node.js performance entries into application-specific data structures.
|
|
49
|
+
* Returns a readonly array of encoded items.
|
|
50
|
+
*/
|
|
51
|
+
encodePerfEntry: PerformanceEntryEncoder<T>;
|
|
52
|
+
/**
|
|
53
|
+
* Whether to enable buffered observation mode.
|
|
54
|
+
* When true, captures all performance entries that occurred before observation started.
|
|
55
|
+
* When false, only captures entries after subscription begins.
|
|
56
|
+
*
|
|
57
|
+
* @default true
|
|
58
|
+
*/
|
|
59
|
+
captureBufferedEntries?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Threshold for triggering queue flushes.
|
|
62
|
+
* Flushes occur in two scenarios:
|
|
63
|
+
* 1. When queue length reaches (maxQueueSize - flushThreshold)
|
|
64
|
+
* 2. When the number of items added since last flush reaches flushThreshold
|
|
65
|
+
* Larger values provide more buffer space before hitting capacity limits.
|
|
66
|
+
*
|
|
67
|
+
* @default DEFAULT_FLUSH_THRESHOLD (20)
|
|
68
|
+
*/
|
|
8
69
|
flushThreshold?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Maximum number of items allowed in the queue before new entries are dropped.
|
|
72
|
+
* Acts as a memory safety limit to prevent unbounded growth during sink slowdown.
|
|
73
|
+
*
|
|
74
|
+
* @default DEFAULT_MAX_QUEUE_SIZE (10000)
|
|
75
|
+
*/
|
|
76
|
+
maxQueueSize?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Name of the environment variable to check for debug mode.
|
|
79
|
+
* When the env var is set to 'true', encode failures create performance marks for debugging.
|
|
80
|
+
*
|
|
81
|
+
* @default 'CP_PROFILER_DEBUG'
|
|
82
|
+
*/
|
|
83
|
+
debugEnvVar?: string;
|
|
9
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* A sink implementation that observes Node.js performance entries and forwards them to a configurable sink.
|
|
87
|
+
*
|
|
88
|
+
* This class provides a buffered, memory-safe bridge between Node.js PerformanceObserver
|
|
89
|
+
* and application-specific data sinks. It handles performance entry encoding, queue management,
|
|
90
|
+
* and graceful degradation under high load conditions.
|
|
91
|
+
*
|
|
92
|
+
* Performance entries flow through the following lifecycle:
|
|
93
|
+
*
|
|
94
|
+
* - Queued in Memory 💾
|
|
95
|
+
* - Items stored in queue (`#queue`) until flushed
|
|
96
|
+
* - Queue limited by `maxQueueSize` to prevent unbounded growth
|
|
97
|
+
* - Items remain in queue if sink is closed during flush
|
|
98
|
+
*
|
|
99
|
+
* - Successfully Written 📤
|
|
100
|
+
* - Items written to sink and counted in `getStats().written`
|
|
101
|
+
* - Queue cleared after successful batch writes
|
|
102
|
+
*
|
|
103
|
+
* - Item Disposition Scenarios 💥
|
|
104
|
+
* - **Encode Failure**: ❌ Items lost when `encode()` throws. Creates perf mark if debug env var (specified by `debugEnvVar`) is set to 'true'.
|
|
105
|
+
* - **Sink Write Failure**: 💾 Items stay in queue when sink write fails during flush
|
|
106
|
+
* - **Sink Closed**: 💾 Items stay in queue when sink is closed during flush
|
|
107
|
+
* - **Proactive Flush Throws**: 💾 Items stay in queue when `flush()` throws during threshold check
|
|
108
|
+
* - **Final Flush Throws**: 💾 Items stay in queue when `flush()` throws at end of callback
|
|
109
|
+
* - **Buffered Flush Throws**: 💾 Items stay in queue when buffered entries flush fails
|
|
110
|
+
* - **Queue Overflow**: ❌ Items dropped when queue reaches `maxQueueSize`
|
|
111
|
+
*
|
|
112
|
+
* @template T - The type of encoded performance data written to the sink
|
|
113
|
+
* @implements {Observer} - Lifecycle management interface
|
|
114
|
+
* @implements {Buffered} - Queue statistics interface
|
|
115
|
+
*/
|
|
10
116
|
export declare class PerformanceObserverSink<T> {
|
|
11
117
|
#private;
|
|
118
|
+
/**
|
|
119
|
+
* Creates a new PerformanceObserverSink with the specified configuration.
|
|
120
|
+
*
|
|
121
|
+
* @param options - Configuration options for the performance observer sink
|
|
122
|
+
* @throws {Error} If flushThreshold validation fails (must be > 0 and <= maxQueueSize)
|
|
123
|
+
*/
|
|
12
124
|
constructor(options: PerformanceObserverOptions<T>);
|
|
13
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Returns whether debug mode is enabled for encode failures.
|
|
127
|
+
*
|
|
128
|
+
* Debug mode is determined by the environment variable specified by `debugEnvVar`
|
|
129
|
+
* (defaults to 'CP_PROFILER_DEBUG'). When enabled, encode failures create
|
|
130
|
+
* performance marks for debugging.
|
|
131
|
+
*
|
|
132
|
+
* @returns true if debug mode is enabled, false otherwise
|
|
133
|
+
*/
|
|
134
|
+
get debug(): boolean;
|
|
135
|
+
/**
|
|
136
|
+
* Returns current queue statistics for monitoring and debugging.
|
|
137
|
+
*
|
|
138
|
+
* Provides insight into the current state of the performance entry queue,
|
|
139
|
+
* useful for monitoring memory usage and processing throughput.
|
|
140
|
+
*
|
|
141
|
+
* @returns Object containing all states and entry counts
|
|
142
|
+
*/
|
|
143
|
+
getStats(): {
|
|
144
|
+
isSubscribed: boolean;
|
|
145
|
+
queued: number;
|
|
146
|
+
dropped: number;
|
|
147
|
+
written: number;
|
|
148
|
+
maxQueueSize: number;
|
|
149
|
+
flushThreshold: number;
|
|
150
|
+
addedSinceLastFlush: number;
|
|
151
|
+
buffered: boolean;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Encodes a raw PerformanceEntry using the configured encoder function.
|
|
155
|
+
*
|
|
156
|
+
* This method delegates to the user-provided encoder function, allowing
|
|
157
|
+
* transformation of Node.js performance entries into application-specific types.
|
|
158
|
+
*
|
|
159
|
+
* @param entry - The raw performance entry to encode
|
|
160
|
+
* @returns Readonly array of encoded items
|
|
161
|
+
*/
|
|
162
|
+
encode(entry: PerformanceEntry): readonly T[];
|
|
163
|
+
/**
|
|
164
|
+
* Starts observing performance entries and forwarding them to the sink.
|
|
165
|
+
*
|
|
166
|
+
* Creates a Node.js PerformanceObserver that monitors 'mark' and 'measure' entries.
|
|
167
|
+
* The observer uses a bounded queue with proactive flushing to manage memory usage.
|
|
168
|
+
* When buffered mode is enabled, any existing buffered entries are immediately flushed.
|
|
169
|
+
* If the sink is closed, items stay in the queue until reopened.
|
|
170
|
+
*
|
|
171
|
+
*/
|
|
14
172
|
subscribe(): void;
|
|
173
|
+
/**
|
|
174
|
+
* Flushes all queued performance entries to the sink.
|
|
175
|
+
*
|
|
176
|
+
* Writes all currently queued encoded performance entries to the configured sink.
|
|
177
|
+
* If the sink is closed, flush is a no-op and items stay in the queue until reopened.
|
|
178
|
+
* The queue is always cleared after flush attempt, regardless of success or failure.
|
|
179
|
+
*/
|
|
15
180
|
flush(): void;
|
|
181
|
+
/**
|
|
182
|
+
* Stops observing performance entries and cleans up resources.
|
|
183
|
+
*
|
|
184
|
+
* Performs a final flush of any remaining queued entries, then disconnects
|
|
185
|
+
* the PerformanceObserver and releases all references.
|
|
186
|
+
*
|
|
187
|
+
* This method is idempotent - safe to call multiple times.
|
|
188
|
+
*/
|
|
16
189
|
unsubscribe(): void;
|
|
190
|
+
/**
|
|
191
|
+
* Checks whether the performance observer is currently active.
|
|
192
|
+
*
|
|
193
|
+
* Returns true if the sink is subscribed and actively observing performance entries.
|
|
194
|
+
* This indicates that a PerformanceObserver instance exists and is connected.
|
|
195
|
+
*
|
|
196
|
+
* @returns true if currently subscribed and observing, false otherwise
|
|
197
|
+
*/
|
|
17
198
|
isSubscribed(): boolean;
|
|
18
199
|
}
|
|
@@ -1,35 +1,216 @@
|
|
|
1
1
|
import { PerformanceObserver, performance, } from 'node:perf_hooks';
|
|
2
|
+
import { isEnvVarEnabled } from './env.js';
|
|
3
|
+
import { PROFILER_DEBUG_ENV_VAR } from './profiler/constants.js';
|
|
4
|
+
/**
|
|
5
|
+
* Array of performance entry types that this observer monitors.
|
|
6
|
+
* Only 'mark' and 'measure' entries are tracked as they represent
|
|
7
|
+
* user-defined performance markers and measurements.
|
|
8
|
+
*/
|
|
2
9
|
const OBSERVED_TYPES = ['mark', 'measure'];
|
|
10
|
+
const OBSERVED_TYPE_SET = new Set(OBSERVED_TYPES);
|
|
11
|
+
/**
|
|
12
|
+
* Converts an error to a performance mark name for debugging.
|
|
13
|
+
* @param error - The error that occurred
|
|
14
|
+
* @param entry - The performance entry that failed to encode
|
|
15
|
+
* @returns A mark name string
|
|
16
|
+
*/
|
|
17
|
+
function errorToPerfMark(error, entry) {
|
|
18
|
+
const errorName = error instanceof Error ? error.name : 'UnknownError';
|
|
19
|
+
const entryName = entry.name || 'unnamed';
|
|
20
|
+
return `encode-error:${errorName}:${entryName}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Default threshold for triggering queue flushes based on queue length.
|
|
24
|
+
* When the queue length reaches (maxQueueSize - flushThreshold),
|
|
25
|
+
* a flush is triggered to prevent overflow. This provides a buffer zone
|
|
26
|
+
* before hitting the maximum queue capacity.
|
|
27
|
+
*/
|
|
3
28
|
export const DEFAULT_FLUSH_THRESHOLD = 20;
|
|
29
|
+
/**
|
|
30
|
+
* Default maximum number of items allowed in the queue before entries are dropped.
|
|
31
|
+
* This acts as a memory safety limit to prevent unbounded memory growth
|
|
32
|
+
* in case of sink slowdown or high-frequency performance entries.
|
|
33
|
+
*/
|
|
34
|
+
export const DEFAULT_MAX_QUEUE_SIZE = 10_000;
|
|
35
|
+
/**
|
|
36
|
+
* Validates the flush threshold configuration to ensure sensible bounds.
|
|
37
|
+
*
|
|
38
|
+
* The flush threshold must be positive and cannot exceed the maximum queue size,
|
|
39
|
+
* as it represents a buffer zone within the queue capacity.
|
|
40
|
+
*
|
|
41
|
+
* @param flushThreshold - The threshold value to validate (must be > 0)
|
|
42
|
+
* @param maxQueueSize - The maximum queue size for comparison (flushThreshold <= maxQueueSize)
|
|
43
|
+
* @throws {Error} If flushThreshold is not positive or exceeds maxQueueSize
|
|
44
|
+
*/
|
|
45
|
+
export function validateFlushThreshold(flushThreshold, maxQueueSize) {
|
|
46
|
+
if (flushThreshold <= 0) {
|
|
47
|
+
throw new Error('flushThreshold must be > 0');
|
|
48
|
+
}
|
|
49
|
+
if (flushThreshold > maxQueueSize) {
|
|
50
|
+
throw new Error('flushThreshold must be <= maxQueueSize');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A sink implementation that observes Node.js performance entries and forwards them to a configurable sink.
|
|
55
|
+
*
|
|
56
|
+
* This class provides a buffered, memory-safe bridge between Node.js PerformanceObserver
|
|
57
|
+
* and application-specific data sinks. It handles performance entry encoding, queue management,
|
|
58
|
+
* and graceful degradation under high load conditions.
|
|
59
|
+
*
|
|
60
|
+
* Performance entries flow through the following lifecycle:
|
|
61
|
+
*
|
|
62
|
+
* - Queued in Memory 💾
|
|
63
|
+
* - Items stored in queue (`#queue`) until flushed
|
|
64
|
+
* - Queue limited by `maxQueueSize` to prevent unbounded growth
|
|
65
|
+
* - Items remain in queue if sink is closed during flush
|
|
66
|
+
*
|
|
67
|
+
* - Successfully Written 📤
|
|
68
|
+
* - Items written to sink and counted in `getStats().written`
|
|
69
|
+
* - Queue cleared after successful batch writes
|
|
70
|
+
*
|
|
71
|
+
* - Item Disposition Scenarios 💥
|
|
72
|
+
* - **Encode Failure**: ❌ Items lost when `encode()` throws. Creates perf mark if debug env var (specified by `debugEnvVar`) is set to 'true'.
|
|
73
|
+
* - **Sink Write Failure**: 💾 Items stay in queue when sink write fails during flush
|
|
74
|
+
* - **Sink Closed**: 💾 Items stay in queue when sink is closed during flush
|
|
75
|
+
* - **Proactive Flush Throws**: 💾 Items stay in queue when `flush()` throws during threshold check
|
|
76
|
+
* - **Final Flush Throws**: 💾 Items stay in queue when `flush()` throws at end of callback
|
|
77
|
+
* - **Buffered Flush Throws**: 💾 Items stay in queue when buffered entries flush fails
|
|
78
|
+
* - **Queue Overflow**: ❌ Items dropped when queue reaches `maxQueueSize`
|
|
79
|
+
*
|
|
80
|
+
* @template T - The type of encoded performance data written to the sink
|
|
81
|
+
* @implements {Observer} - Lifecycle management interface
|
|
82
|
+
* @implements {Buffered} - Queue statistics interface
|
|
83
|
+
*/
|
|
4
84
|
export class PerformanceObserverSink {
|
|
5
|
-
|
|
85
|
+
/** Encoder function for transforming PerformanceEntry objects into domain types */
|
|
86
|
+
#encodePerfEntry;
|
|
87
|
+
/** Whether buffered observation mode is enabled */
|
|
6
88
|
#buffered;
|
|
89
|
+
/** Threshold for triggering flushes based on queue length proximity to max capacity */
|
|
7
90
|
#flushThreshold;
|
|
91
|
+
/** Maximum number of items allowed in queue before dropping new entries (hard memory limit) */
|
|
92
|
+
#maxQueueSize;
|
|
93
|
+
/** The target sink where encoded performance data is written */
|
|
8
94
|
#sink;
|
|
95
|
+
/** Node.js PerformanceObserver instance, undefined when not subscribed */
|
|
9
96
|
#observer;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
97
|
+
/** Bounded queue storing encoded performance items awaiting flush */
|
|
98
|
+
#queue = [];
|
|
99
|
+
/** Count of performance entries dropped due to queue overflow */
|
|
100
|
+
#dropped = 0;
|
|
101
|
+
/** Count of performance entries successfully written to sink */
|
|
102
|
+
#written = 0;
|
|
103
|
+
/** Number of items added to queue since last successful flush */
|
|
104
|
+
#addedSinceLastFlush = 0;
|
|
105
|
+
/** Whether debug mode is enabled for encode failures */
|
|
106
|
+
#debug;
|
|
107
|
+
/**
|
|
108
|
+
* Creates a new PerformanceObserverSink with the specified configuration.
|
|
109
|
+
*
|
|
110
|
+
* @param options - Configuration options for the performance observer sink
|
|
111
|
+
* @throws {Error} If flushThreshold validation fails (must be > 0 and <= maxQueueSize)
|
|
112
|
+
*/
|
|
13
113
|
constructor(options) {
|
|
14
|
-
const {
|
|
15
|
-
this.#
|
|
16
|
-
this.#written = new Map(OBSERVED_TYPES.map(t => [t, 0]));
|
|
114
|
+
const { encodePerfEntry, sink, captureBufferedEntries, flushThreshold = DEFAULT_FLUSH_THRESHOLD, maxQueueSize = DEFAULT_MAX_QUEUE_SIZE, debugEnvVar = PROFILER_DEBUG_ENV_VAR, } = options;
|
|
115
|
+
this.#encodePerfEntry = encodePerfEntry;
|
|
17
116
|
this.#sink = sink;
|
|
18
|
-
this.#buffered =
|
|
19
|
-
this.#
|
|
117
|
+
this.#buffered = captureBufferedEntries ?? true;
|
|
118
|
+
this.#maxQueueSize = maxQueueSize;
|
|
119
|
+
validateFlushThreshold(flushThreshold, this.#maxQueueSize);
|
|
120
|
+
this.#flushThreshold = flushThreshold;
|
|
121
|
+
this.#debug = isEnvVarEnabled(debugEnvVar);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Returns whether debug mode is enabled for encode failures.
|
|
125
|
+
*
|
|
126
|
+
* Debug mode is determined by the environment variable specified by `debugEnvVar`
|
|
127
|
+
* (defaults to 'CP_PROFILER_DEBUG'). When enabled, encode failures create
|
|
128
|
+
* performance marks for debugging.
|
|
129
|
+
*
|
|
130
|
+
* @returns true if debug mode is enabled, false otherwise
|
|
131
|
+
*/
|
|
132
|
+
get debug() {
|
|
133
|
+
return this.#debug;
|
|
20
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Returns current queue statistics for monitoring and debugging.
|
|
137
|
+
*
|
|
138
|
+
* Provides insight into the current state of the performance entry queue,
|
|
139
|
+
* useful for monitoring memory usage and processing throughput.
|
|
140
|
+
*
|
|
141
|
+
* @returns Object containing all states and entry counts
|
|
142
|
+
*/
|
|
143
|
+
getStats() {
|
|
144
|
+
return {
|
|
145
|
+
isSubscribed: this.isSubscribed(),
|
|
146
|
+
queued: this.#queue.length,
|
|
147
|
+
dropped: this.#dropped,
|
|
148
|
+
written: this.#written,
|
|
149
|
+
maxQueueSize: this.#maxQueueSize,
|
|
150
|
+
flushThreshold: this.#flushThreshold,
|
|
151
|
+
addedSinceLastFlush: this.#addedSinceLastFlush,
|
|
152
|
+
buffered: this.#buffered,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Encodes a raw PerformanceEntry using the configured encoder function.
|
|
157
|
+
*
|
|
158
|
+
* This method delegates to the user-provided encoder function, allowing
|
|
159
|
+
* transformation of Node.js performance entries into application-specific types.
|
|
160
|
+
*
|
|
161
|
+
* @param entry - The raw performance entry to encode
|
|
162
|
+
* @returns Readonly array of encoded items
|
|
163
|
+
*/
|
|
21
164
|
encode(entry) {
|
|
22
|
-
return this.#
|
|
165
|
+
return this.#encodePerfEntry(entry);
|
|
23
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Starts observing performance entries and forwarding them to the sink.
|
|
169
|
+
*
|
|
170
|
+
* Creates a Node.js PerformanceObserver that monitors 'mark' and 'measure' entries.
|
|
171
|
+
* The observer uses a bounded queue with proactive flushing to manage memory usage.
|
|
172
|
+
* When buffered mode is enabled, any existing buffered entries are immediately flushed.
|
|
173
|
+
* If the sink is closed, items stay in the queue until reopened.
|
|
174
|
+
*
|
|
175
|
+
*/
|
|
24
176
|
subscribe() {
|
|
25
177
|
if (this.#observer) {
|
|
26
178
|
return;
|
|
27
179
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
180
|
+
this.#observer = new PerformanceObserver(list => {
|
|
181
|
+
list.getEntries().forEach(entry => {
|
|
182
|
+
if (OBSERVED_TYPE_SET.has(entry.entryType)) {
|
|
183
|
+
try {
|
|
184
|
+
const items = this.encode(entry);
|
|
185
|
+
items.forEach(item => {
|
|
186
|
+
// ❌ MAX QUEUE OVERFLOW
|
|
187
|
+
if (this.#queue.length >= this.#maxQueueSize) {
|
|
188
|
+
this.#dropped++; // Items are lost forever
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (this.#queue.length >=
|
|
192
|
+
this.#maxQueueSize - this.#flushThreshold) {
|
|
193
|
+
this.flush();
|
|
194
|
+
}
|
|
195
|
+
this.#queue.push(item);
|
|
196
|
+
this.#addedSinceLastFlush++;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
// ❌ Encode failure: item lost forever as user has to fix encode function.
|
|
201
|
+
this.#dropped++;
|
|
202
|
+
if (this.#debug) {
|
|
203
|
+
try {
|
|
204
|
+
performance.mark(errorToPerfMark(error, entry));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Ignore mark failures to prevent double errors
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
if (this.#addedSinceLastFlush >= this.#flushThreshold) {
|
|
33
214
|
this.flush();
|
|
34
215
|
}
|
|
35
216
|
});
|
|
@@ -37,33 +218,65 @@ export class PerformanceObserverSink {
|
|
|
37
218
|
entryTypes: OBSERVED_TYPES,
|
|
38
219
|
buffered: this.#buffered,
|
|
39
220
|
});
|
|
221
|
+
if (this.#buffered) {
|
|
222
|
+
this.flush();
|
|
223
|
+
}
|
|
40
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Flushes all queued performance entries to the sink.
|
|
227
|
+
*
|
|
228
|
+
* Writes all currently queued encoded performance entries to the configured sink.
|
|
229
|
+
* If the sink is closed, flush is a no-op and items stay in the queue until reopened.
|
|
230
|
+
* The queue is always cleared after flush attempt, regardless of success or failure.
|
|
231
|
+
*/
|
|
41
232
|
flush() {
|
|
42
|
-
if (
|
|
233
|
+
if (this.#queue.length === 0) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (this.#sink.isClosed()) {
|
|
43
237
|
return;
|
|
44
238
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
239
|
+
// Process each item in queue
|
|
240
|
+
const failedItems = [];
|
|
241
|
+
this.#queue.forEach(item => {
|
|
48
242
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.forEach(item => this.#sink.append(item));
|
|
52
|
-
this.#written.set(t, written + fresh.length);
|
|
243
|
+
this.#sink.append(item);
|
|
244
|
+
this.#written++;
|
|
53
245
|
}
|
|
54
|
-
catch
|
|
55
|
-
|
|
246
|
+
catch {
|
|
247
|
+
failedItems.push(item);
|
|
56
248
|
}
|
|
57
249
|
});
|
|
58
|
-
|
|
250
|
+
// Clear queue but keep failed items for retry
|
|
251
|
+
this.#queue.length = 0;
|
|
252
|
+
this.#queue.push(...failedItems);
|
|
253
|
+
this.#addedSinceLastFlush = failedItems.length;
|
|
59
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Stops observing performance entries and cleans up resources.
|
|
257
|
+
*
|
|
258
|
+
* Performs a final flush of any remaining queued entries, then disconnects
|
|
259
|
+
* the PerformanceObserver and releases all references.
|
|
260
|
+
*
|
|
261
|
+
* This method is idempotent - safe to call multiple times.
|
|
262
|
+
*/
|
|
60
263
|
unsubscribe() {
|
|
61
264
|
if (!this.#observer) {
|
|
62
265
|
return;
|
|
63
266
|
}
|
|
64
|
-
this
|
|
267
|
+
this.flush();
|
|
268
|
+
this.#addedSinceLastFlush = 0;
|
|
269
|
+
this.#observer.disconnect();
|
|
65
270
|
this.#observer = undefined;
|
|
66
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Checks whether the performance observer is currently active.
|
|
274
|
+
*
|
|
275
|
+
* Returns true if the sink is subscribed and actively observing performance entries.
|
|
276
|
+
* This indicates that a PerformanceObserver instance exists and is connected.
|
|
277
|
+
*
|
|
278
|
+
* @returns true if currently subscribed and observing, false otherwise
|
|
279
|
+
*/
|
|
67
280
|
isSubscribed() {
|
|
68
281
|
return this.#observer !== undefined;
|
|
69
282
|
}
|