Skip to content

Commit 022eeef

Browse files
committed
refactor: use sharded WAL
1 parent e47bf34 commit 022eeef

File tree

6 files changed

+398
-137
lines changed

6 files changed

+398
-137
lines changed

packages/utils/docs/profiler.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ const saved = profiler.measure('save-user', () => saveToDb(user), {
259259

260260
This profiler extends all options and API from Profiler with automatic process exit handling for buffered performance data.
261261

262-
The NodeJSProfiler automatically subscribes to performance observation and installs exit handlers that flush buffered data on process termination (signals, fatal errors, or normal exit).
262+
The NodeJSProfiler automatically subscribes to performance observation and installs exit handlers that flush buffered data on process termination (signals, fatal errors, or normal exit). It uses a `ShardedWal` internally to coordinate multiple WAL shards across processes/files.
263263

264264
## Configuration
265265

@@ -273,12 +273,16 @@ new NodejsProfiler<DomainEvents, Tracks>(options: NodejsProfilerOptions<DomainEv
273273

274274
**Options:**
275275

276-
| Property | Type | Default | Description |
277-
| ------------------------ | --------------------------------------- | ---------- | ------------------------------------------------------------------------------- |
278-
| `encodePerfEntry` | `PerformanceEntryEncoder<DomainEvents>` | _required_ | Function that encodes raw PerformanceEntry objects into domain-specific types |
279-
| `captureBufferedEntries` | `boolean` | `true` | Whether to capture performance entries that occurred before observation started |
280-
| `flushThreshold` | `number` | `20` | Threshold for triggering queue flushes based on queue length |
281-
| `maxQueueSize` | `number` | `10_000` | Maximum number of items allowed in the queue before new entries are dropped |
276+
| Property | Type | Default | Description |
277+
| ------------------------ | --------------------------------------- | ---------------- | ------------------------------------------------------------------------------------ |
278+
| `format` | `Partial<WalFormat<DomainEvents>>` | _required_ | WAL format configuration for sharded write-ahead logging |
279+
| `measureName` | `string` | _auto-generated_ | Optional folder name for sharding. If not provided, a new group ID will be generated |
280+
| `outDir` | `string` | `'tmp/profiles'` | Output directory for WAL shards and final files |
281+
| `outBaseName` | `string` | _optional_ | Override the base name for WAL files (overrides format.baseName) |
282+
| `encodePerfEntry` | `PerformanceEntryEncoder<DomainEvents>` | _required_ | Function that encodes raw PerformanceEntry objects into domain-specific types |
283+
| `captureBufferedEntries` | `boolean` | `true` | Whether to capture performance entries that occurred before observation started |
284+
| `flushThreshold` | `number` | `20` | Threshold for triggering queue flushes based on queue length |
285+
| `maxQueueSize` | `number` | `10_000` | Maximum number of items allowed in the queue before new entries are dropped |
282286

283287
## API Methods
284288

packages/utils/src/lib/profiler/constants.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,21 @@ export const PROFILER_ENABLED_ENV_VAR = 'CP_PROFILING';
1919
* ```
2020
*/
2121
export const PROFILER_DEBUG_ENV_VAR = 'CP_PROFILER_DEBUG';
22+
23+
/**
24+
* Default output directory for persisted profiler data.
25+
* Matches the default persist output directory from models.
26+
*/
27+
export const PERSIST_OUT_DIR = '.code-pushup';
28+
29+
/**
30+
* Default filename (without extension) for persisted profiler data.
31+
* Matches the default persist filename from models.
32+
*/
33+
export const PERSIST_OUT_FILENAME = 'report';
34+
35+
/**
36+
* Default base name for WAL files.
37+
* Used as the base name for sharded WAL files (e.g., "trace").
38+
*/
39+
export const PERSIST_OUT_BASENAME = 'trace';

packages/utils/src/lib/profiler/profiler.int.test.ts

Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { MockTraceEventFileSink } from '../../../mocks/sink.mock.js';
21
import type { PerformanceEntryEncoder } from '../performance-observer.js';
32
import type { ActionTrackEntryPayload } from '../user-timing-extensibility-api.type.js';
4-
import { NodejsProfiler, Profiler } from './profiler.js';
3+
import { NodeJsProfiler, Profiler } from './profiler.js';
54

65
describe('Profiler Integration', () => {
76
let profiler: Profiler<Record<string, ActionTrackEntryPayload>>;
@@ -306,25 +305,31 @@ describe('NodeJS Profiler Integration', () => {
306305
return [];
307306
};
308307

309-
let mockSink: MockTraceEventFileSink;
310-
let nodejsProfiler: NodejsProfiler<string>;
308+
let nodejsProfiler: NodeJsProfiler<string>;
311309

312310
beforeEach(() => {
313-
mockSink = new MockTraceEventFileSink();
314-
315-
nodejsProfiler = new NodejsProfiler({
311+
nodejsProfiler = new NodeJsProfiler({
316312
prefix: 'test',
317313
track: 'test-track',
318-
sink: mockSink,
314+
format: {
315+
baseName: 'test',
316+
walExtension: '.log',
317+
finalExtension: '.log',
318+
codec: {
319+
encode: (v: string) => v,
320+
decode: (v: string) => v,
321+
},
322+
finalizer: (records: (string | { __invalid: true; raw: string })[]) =>
323+
records.filter((r): r is string => typeof r === 'string').join('\n'),
324+
},
319325
encodePerfEntry: simpleEncoder,
320326
enabled: true,
321327
});
322328
});
323329

324330
it('should initialize with sink opened when enabled', () => {
325-
expect(mockSink.isClosed()).toBeFalse();
326331
expect(nodejsProfiler.isEnabled()).toBeTrue();
327-
expect(mockSink.open).toHaveBeenCalledOnce();
332+
expect(nodejsProfiler.stats.walOpen).toBeTrue();
328333
});
329334

330335
it('should create performance entries and write to sink', () => {
@@ -340,41 +345,52 @@ describe('NodeJS Profiler Integration', () => {
340345
return 'async-result';
341346
}),
342347
).resolves.toBe('async-result');
348+
349+
const stats = nodejsProfiler.stats;
350+
await expect(JSON.stringify(stats, null, 2)).toMatchFileSnapshot(
351+
'__snapshots__/profiler.int.test.async-operations.json',
352+
);
343353
});
344354

345355
it('should disable profiling and close sink', () => {
346356
nodejsProfiler.setEnabled(false);
347357
expect(nodejsProfiler.isEnabled()).toBeFalse();
348-
expect(mockSink.isClosed()).toBeTrue();
349-
expect(mockSink.close).toHaveBeenCalledOnce();
358+
expect(nodejsProfiler.stats.walOpen).toBeFalse();
350359

351360
expect(nodejsProfiler.measure('disabled-test', () => 'success')).toBe(
352361
'success',
353362
);
354-
355-
expect(mockSink.getWrittenItems()).toHaveLength(0);
356363
});
357364

358365
it('should re-enable profiling correctly', () => {
359366
nodejsProfiler.setEnabled(false);
360367
nodejsProfiler.setEnabled(true);
361368

362369
expect(nodejsProfiler.isEnabled()).toBeTrue();
363-
expect(mockSink.isClosed()).toBeFalse();
364-
expect(mockSink.open).toHaveBeenCalledTimes(2);
370+
expect(nodejsProfiler.stats.walOpen).toBeTrue();
365371

366372
expect(nodejsProfiler.measure('re-enabled-test', () => 42)).toBe(42);
367373
});
368374

369375
it('should support custom tracks', () => {
370-
const profilerWithTracks = new NodejsProfiler({
376+
const profilerWithTracks = new NodeJsProfiler({
371377
prefix: 'api-server',
372378
track: 'HTTP',
373379
tracks: {
374380
db: { track: 'Database', color: 'secondary' },
375381
cache: { track: 'Cache', color: 'primary' },
376382
},
377-
sink: mockSink,
383+
format: {
384+
baseName: 'test',
385+
walExtension: '.log',
386+
finalExtension: '.log',
387+
codec: {
388+
encode: (v: string) => v,
389+
decode: (v: string) => v,
390+
},
391+
finalizer: (records: (string | { __invalid: true; raw: string })[]) =>
392+
records.filter((r): r is string => typeof r === 'string').join('\n'),
393+
},
378394
encodePerfEntry: simpleEncoder,
379395
});
380396

@@ -386,10 +402,20 @@ describe('NodeJS Profiler Integration', () => {
386402
});
387403

388404
it('should capture buffered entries when buffered option is enabled', () => {
389-
const bufferedProfiler = new NodejsProfiler({
405+
const bufferedProfiler = new NodeJsProfiler({
390406
prefix: 'buffered-test',
391407
track: 'Test',
392-
sink: mockSink,
408+
format: {
409+
baseName: 'test',
410+
walExtension: '.log',
411+
finalExtension: '.log',
412+
codec: {
413+
encode: (v: string) => v,
414+
decode: (v: string) => v,
415+
},
416+
finalizer: (records: (string | { __invalid: true; raw: string })[]) =>
417+
records.filter((r): r is string => typeof r === 'string').join('\n'),
418+
},
393419
encodePerfEntry: simpleEncoder,
394420
captureBufferedEntries: true,
395421
enabled: true,
@@ -407,10 +433,20 @@ describe('NodeJS Profiler Integration', () => {
407433
});
408434

409435
it('should return correct getStats with dropped and written counts', () => {
410-
const statsProfiler = new NodejsProfiler({
436+
const statsProfiler = new NodeJsProfiler({
411437
prefix: 'stats-test',
412438
track: 'Stats',
413-
sink: mockSink,
439+
format: {
440+
baseName: 'test',
441+
walExtension: '.log',
442+
finalExtension: '.log',
443+
codec: {
444+
encode: (v: string) => v,
445+
decode: (v: string) => v,
446+
},
447+
finalizer: (records: (string | { __invalid: true; raw: string })[]) =>
448+
records.filter((r): r is string => typeof r === 'string').join('\n'),
449+
},
414450
encodePerfEntry: simpleEncoder,
415451
maxQueueSize: 2,
416452
flushThreshold: 2,
@@ -431,10 +467,20 @@ describe('NodeJS Profiler Integration', () => {
431467
});
432468

433469
it('should provide comprehensive queue statistics via getStats', () => {
434-
const profiler = new NodejsProfiler({
470+
const profiler = new NodeJsProfiler({
435471
prefix: 'stats-profiler',
436472
track: 'Stats',
437-
sink: mockSink,
473+
format: {
474+
baseName: 'test',
475+
walExtension: '.log',
476+
finalExtension: '.log',
477+
codec: {
478+
encode: (v: string) => v,
479+
decode: (v: string) => v,
480+
},
481+
finalizer: (records: (string | { __invalid: true; raw: string })[]) =>
482+
records.filter((r): r is string => typeof r === 'string').join('\n'),
483+
},
438484
encodePerfEntry: simpleEncoder,
439485
maxQueueSize: 3,
440486
flushThreshold: 2,
@@ -462,4 +508,13 @@ describe('NodeJS Profiler Integration', () => {
462508
expect(finalStats.isSubscribed).toBeFalse();
463509
expect(finalStats.queued).toBe(0);
464510
});
511+
512+
it('should handle async operations', async () => {
513+
await expect(
514+
nodejsProfiler.measureAsync('async-test', async () => {
515+
await new Promise(resolve => setTimeout(resolve, 1));
516+
return 'async-result';
517+
}),
518+
).resolves.toBe('async-result');
519+
});
465520
});

0 commit comments

Comments
 (0)