|
| 1 | +import path from 'node:path'; |
| 2 | +import { performance } from 'node:perf_hooks'; |
1 | 3 | import type { PerformanceEntry } from 'node:perf_hooks'; |
2 | 4 | import process from 'node:process'; |
3 | 5 | import { threadId } from 'node:worker_threads'; |
@@ -26,7 +28,14 @@ import { |
26 | 28 | import { entryToTraceEvents } from './trace-file-utils.js'; |
27 | 29 | import type { UserTimingTraceEvent } from './trace-file.type.js'; |
28 | 30 | import { traceEventWalFormat } from './wal-json-trace.js'; |
29 | | -import { ShardedWal, WriteAheadLogFile } from './wal.js'; |
| 31 | +import { |
| 32 | + ShardedWal, |
| 33 | + WriteAheadLogFile, |
| 34 | + getShardId, |
| 35 | + getShardedGroupId, |
| 36 | + isLeaderWal, |
| 37 | + setLeaderWal, |
| 38 | +} from './wal.js'; |
30 | 39 | import type { WalFormat } from './wal.js'; |
31 | 40 |
|
32 | 41 | /** |
@@ -80,11 +89,6 @@ export class Profiler<T extends ActionTrackConfigs> { |
80 | 89 | * |
81 | 90 | */ |
82 | 91 | constructor(options: ProfilerOptions<T>) { |
83 | | - // Initialize origin PID early - must happen before user code runs |
84 | | - if (!process.env[PROFILER_ORIGIN_PID_ENV_VAR]) { |
85 | | - process.env[PROFILER_ORIGIN_PID_ENV_VAR] = String(process.pid); |
86 | | - } |
87 | | - |
88 | 92 | const { tracks, prefix, enabled, ...defaults } = options; |
89 | 93 | const dataType = 'track-entry'; |
90 | 94 |
|
@@ -113,6 +117,13 @@ export class Profiler<T extends ActionTrackConfigs> { |
113 | 117 | this.#enabled = enabled; |
114 | 118 | } |
115 | 119 |
|
| 120 | + /** |
| 121 | + * Close the profiler. Subclasses should override this to perform cleanup. |
| 122 | + */ |
| 123 | + close(): void { |
| 124 | + // Base implementation does nothing |
| 125 | + } |
| 126 | + |
116 | 127 | /** |
117 | 128 | * Is profiling enabled? |
118 | 129 | * |
@@ -235,74 +246,103 @@ export class Profiler<T extends ActionTrackConfigs> { |
235 | 246 | } |
236 | 247 | } |
237 | 248 |
|
238 | | -/** |
239 | | - * Determines if this process is the leader WAL process using the origin PID heuristic. |
240 | | - * |
241 | | - * The leader is the process that first enabled profiling (the one that set CP_PROFILER_ORIGIN_PID). |
242 | | - * All descendant processes inherit the environment but have different PIDs. |
243 | | - * |
244 | | - * @returns true if this is the leader WAL process, false otherwise |
245 | | - */ |
246 | | -export function isLeaderWal(): boolean { |
247 | | - return process.env[PROFILER_ORIGIN_PID_ENV_VAR] === String(process.pid); |
248 | | -} |
249 | | - |
250 | 249 | export class NodeProfiler< |
251 | 250 | TracksConfig extends ActionTrackConfigs = ActionTrackConfigs, |
252 | | - CodecOutput extends string | object = UserTimingTraceEvent, |
| 251 | + CodecOutput extends UserTimingTraceEvent = UserTimingTraceEvent, |
253 | 252 | > extends Profiler<TracksConfig> { |
254 | 253 | #shard: WriteAheadLogFile<CodecOutput>; |
255 | 254 | #perfObserver: PerformanceObserverSink<CodecOutput>; |
256 | 255 | #shardWal: ShardedWal<CodecOutput>; |
257 | 256 | readonly #format: WalFormat<CodecOutput>; |
| 257 | + readonly #debug: boolean; |
| 258 | + #closed: boolean = false; |
| 259 | + |
258 | 260 | constructor( |
259 | 261 | options: ProfilerOptions<TracksConfig> & { |
260 | 262 | directory?: string; |
261 | 263 | performanceEntryEncode: (entry: PerformanceEntry) => CodecOutput[]; |
262 | | - format: WalFormat<CodecOutput>; |
| 264 | + debug?: boolean; |
263 | 265 | }, |
264 | 266 | ) { |
| 267 | + // Initialize origin PID early - must happen before user code runs |
| 268 | + setLeaderWal(PROFILER_ORIGIN_PID_ENV_VAR); |
| 269 | + |
265 | 270 | const { |
266 | 271 | directory = PROFILER_DIRECTORY, |
267 | 272 | performanceEntryEncode, |
268 | | - format, |
| 273 | + debug = false, |
| 274 | + ...profilerOptions |
269 | 275 | } = options; |
270 | | - super(options); |
271 | | - const shardId = `${process.pid}-${threadId}`; |
| 276 | + super(profilerOptions); |
| 277 | + const walGroupId = getShardedGroupId(); |
| 278 | + const shardId = getShardId(process.pid, threadId); |
272 | 279 |
|
273 | | - this.#format = format; |
274 | | - this.#shardWal = new ShardedWal(directory, format); |
| 280 | + this.#format = traceEventWalFormat({ groupId: walGroupId }); |
| 281 | + this.#debug = debug; |
| 282 | + this.#shardWal = new ShardedWal( |
| 283 | + path.join(directory, walGroupId), |
| 284 | + this.#format, |
| 285 | + ); |
275 | 286 | this.#shard = this.#shardWal.shard(shardId); |
276 | 287 |
|
277 | 288 | this.#perfObserver = new PerformanceObserverSink({ |
278 | 289 | sink: this.#shard, |
279 | 290 | encode: performanceEntryEncode, |
280 | 291 | buffered: true, |
281 | | - flushThreshold: 100, |
| 292 | + flushThreshold: 1, // Lower threshold for immediate flushing |
282 | 293 | }); |
283 | 294 |
|
| 295 | + this.#perfObserver.subscribe(); |
| 296 | + |
284 | 297 | installExitHandlers({ |
285 | 298 | onExit: () => { |
286 | | - this.#perfObserver.flush(); |
287 | | - this.#perfObserver.unsubscribe(); |
288 | | - this.#shard.close(); |
289 | | - if (isLeaderWal()) { |
290 | | - this.#shardWal.finalize(); |
291 | | - this.#shardWal.cleanup(); |
292 | | - } |
| 299 | + this.close(); |
293 | 300 | }, |
294 | 301 | }); |
295 | 302 | } |
296 | 303 |
|
297 | 304 | getFinalPath() { |
298 | 305 | return this.#format.finalPath(); |
299 | 306 | } |
| 307 | + |
| 308 | + /** |
| 309 | + * Close the profiler and finalize files if this is the leader process. |
| 310 | + * This method can be called manually to ensure proper cleanup. |
| 311 | + */ |
| 312 | + close(): void { |
| 313 | + if (this.#closed) { |
| 314 | + return; |
| 315 | + } |
| 316 | + |
| 317 | + this.#closed = true; |
| 318 | + |
| 319 | + try { |
| 320 | + if (!this.#perfObserver || !this.#shard || !this.#shardWal) { |
| 321 | + console.warn('Warning: Profiler not fully initialized during close'); |
| 322 | + return; |
| 323 | + } |
| 324 | + |
| 325 | + this.#perfObserver.flush(); |
| 326 | + this.#perfObserver.unsubscribe(); |
| 327 | + |
| 328 | + this.#shard.close(); |
| 329 | + |
| 330 | + if (isLeaderWal(PROFILER_ORIGIN_PID_ENV_VAR)) { |
| 331 | + this.#shardWal.finalize(); |
| 332 | + if (!this.#debug) { |
| 333 | + this.#shardWal.cleanup(); |
| 334 | + } |
| 335 | + } |
| 336 | + } catch (error) { |
| 337 | + console.warn('Warning: Error during profiler close:', error); |
| 338 | + } |
| 339 | + } |
300 | 340 | } |
301 | 341 |
|
302 | 342 | export const profiler = new NodeProfiler({ |
303 | 343 | prefix: 'cp', |
304 | 344 | track: 'CLI', |
305 | 345 | trackGroup: 'Code Pushup', |
306 | 346 | performanceEntryEncode: entryToTraceEvents, |
307 | | - format: traceEventWalFormat(), |
| 347 | + debug: process.env.CP_PROFILER_DEBUG === 'true', |
308 | 348 | }); |
0 commit comments