Skip to content

Commit 8c2c51c

Browse files
committed
Merge branch 'main' into feat/utils/profiler-perf-buffer
# Conflicts: # packages/utils/mocks/sink.mock.ts # packages/utils/src/lib/performance-observer.int.test.ts # packages/utils/src/lib/performance-observer.ts # packages/utils/src/lib/performance-observer.unit.test.ts # packages/utils/src/lib/profiler/constants.ts # packages/utils/src/lib/profiler/profiler.ts # packages/utils/src/lib/profiler/profiler.unit.test.ts # packages/utils/src/lib/sink-source.type.ts
2 parents e46f208 + e677d4f commit 8c2c51c

17 files changed

Lines changed: 2597 additions & 81 deletions

packages/utils/eslint.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ export default tseslint.config(
1212
},
1313
},
1414
},
15+
{
16+
files: ['packages/utils/src/lib/**/wal*.ts'],
17+
rules: {
18+
'n/no-sync': 'off',
19+
},
20+
},
1521
{
1622
files: ['**/*.json'],
1723
rules: {

packages/utils/src/lib/clock-epoch.unit.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { defaultClock, epochClock } from './clock-epoch.js';
33
describe('epochClock', () => {
44
it('should create epoch clock with defaults', () => {
55
const c = epochClock();
6-
expect(c.timeOriginMs).toBe(500_000);
6+
expect(c.timeOriginMs).toBe(1_700_000_000_000);
77
expect(c.tid).toBe(2);
88
expect(c.pid).toBe(10_001);
99
expect(c.fromEpochMs).toBeFunction();
@@ -32,8 +32,8 @@ describe('epochClock', () => {
3232

3333
it('should support performance clock by default for epochNowUs', () => {
3434
const c = epochClock();
35-
expect(c.timeOriginMs).toBe(500_000);
36-
expect(c.epochNowUs()).toBe(500_000_000);
35+
expect(c.timeOriginMs).toBe(1_700_000_000_000);
36+
expect(c.epochNowUs()).toBe(1_700_000_000_000_000);
3737
});
3838

3939
it.each([
@@ -55,8 +55,8 @@ describe('epochClock', () => {
5555
});
5656

5757
it.each([
58-
[0, 500_000_000],
59-
[1000, 501_000_000],
58+
[0, 1_700_000_000_000_000],
59+
[1000, 1_700_000_001_000_000],
6060
])(
6161
'should convert performance milliseconds to microseconds',
6262
(perfMs, expected) => {

packages/utils/src/lib/performance-observer.int.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import { type PerformanceEntry, performance } from 'node:perf_hooks';
22
import type { MockedFunction } from 'vitest';
3-
import { MockSink } from '../../mocks/sink.mock';
3+
import { MockFileSink } from '../../mocks/sink.mock';
44
import {
55
type PerformanceObserverOptions,
66
PerformanceObserverSink,
77
} from './performance-observer.js';
88

99
describe('PerformanceObserverSink', () => {
1010
let encode: MockedFunction<(entry: PerformanceEntry) => string[]>;
11-
let sink: MockSink;
11+
let sink: MockFileSink;
1212
let options: PerformanceObserverOptions<string>;
1313

1414
const awaitObserverCallback = () =>
1515
new Promise(resolve => setTimeout(resolve, 10));
1616

1717
beforeEach(() => {
18-
sink = new MockSink();
18+
sink = new MockFileSink();
1919
sink.open();
2020
encode = vi.fn((entry: PerformanceEntry) => [
2121
`${entry.name}:${entry.entryType}`,

packages/utils/src/lib/performance-observer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import { isEnvVarEnabled } from './env.js';
77
import { PROFILER_DEBUG_ENV_VAR } from './profiler/constants.js';
88
import type { Buffered, Observer, Sink } from './sink-source.type';
9+
import type { AppendableSink } from './wal.js';
910

1011
/**
1112
* Encoder that converts PerformanceEntry to domain events.
@@ -163,7 +164,7 @@ export type PerformanceObserverOptions<T> = {
163164
* @implements {Observer} - Lifecycle management interface
164165
* @implements {Buffered} - Queue statistics interface
165166
*/
166-
export class PerformanceObserverSink<T> implements Observer, Buffered {
167+
export class PerformanceObserverSink<T> {
167168
/** Encoder function for transforming PerformanceEntry objects into domain types */
168169
#encodePerfEntry: PerformanceEntryEncoder<T>;
169170

@@ -352,7 +353,7 @@ export class PerformanceObserverSink<T> implements Observer, Buffered {
352353

353354
this.#queue.forEach(item => {
354355
try {
355-
this.#sink.write(item);
356+
this.#sink.append(item);
356357
this.#written++;
357358
} catch {
358359
failedItems.push(item);

packages/utils/src/lib/performance-observer.unit.test.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { type PerformanceEntry, performance } from 'node:perf_hooks';
22
import type { MockedFunction } from 'vitest';
33
import { MockPerformanceObserver } from '@code-pushup/test-utils';
4-
import { MockSink } from '../../mocks/sink.mock';
4+
import { MockFileSink } from '../../mocks/sink.mock';
55
import {
66
DEFAULT_FLUSH_THRESHOLD,
77
DEFAULT_MAX_QUEUE_SIZE,
88
type PerformanceObserverOptions,
99
PerformanceObserverSink,
1010
validateFlushThreshold,
1111
} from './performance-observer.js';
12+
import type { Codec } from './wal.js';
1213

1314
describe('validateFlushThreshold', () => {
1415
it.each([
@@ -55,12 +56,12 @@ describe('validateFlushThreshold', () => {
5556

5657
describe('PerformanceObserverSink', () => {
5758
let encodePerfEntry: MockedFunction<(entry: PerformanceEntry) => string[]>;
58-
let sink: MockSink;
59+
let sink: MockFileSink;
5960
let options: PerformanceObserverOptions<string>;
6061

6162
beforeEach(() => {
6263
vi.clearAllMocks();
63-
sink = new MockSink();
64+
sink = new MockFileSink();
6465
sink.open();
6566
encodePerfEntry = vi.fn((entry: PerformanceEntry) => [
6667
`${entry.name}:${entry.entryType}`,
@@ -663,6 +664,28 @@ describe('PerformanceObserverSink', () => {
663664
);
664665
});
665666

667+
it('accepts custom sinks with append method', () => {
668+
const collectedItems: string[] = [];
669+
const customSink = {
670+
// eslint-disable-next-line functional/immutable-data
671+
append: (item: string) => collectedItems.push(item),
672+
};
673+
674+
const observer = new PerformanceObserverSink({
675+
sink: customSink,
676+
encode: (entry: PerformanceEntry) => [`${entry.name}:${entry.duration}`],
677+
});
678+
679+
observer.subscribe();
680+
681+
const mockObserver = MockPerformanceObserver.lastInstance();
682+
mockObserver?.emitMark('test-mark');
683+
684+
observer.flush();
685+
686+
expect(collectedItems).toContain('test-mark:0');
687+
});
688+
666689
it('tracks addedSinceLastFlush counter correctly', () => {
667690
const observer = new PerformanceObserverSink({
668691
sink,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
export const PROFILER_ENABLED_ENV_VAR = 'CP_PROFILING';
22
export const PROFILER_DEBUG_ENV_VAR = 'CP_PROFILER_DEBUG';
3+
export const PROFILER_COORDINATOR_FLAG_ENV_VAR = 'CP_PROFILER_COORDINATOR';
4+
export const PROFILER_ORIGIN_PID_ENV_VAR = 'CP_PROFILER_ORIGIN_PID';
5+
export const PROFILER_DIRECTORY_ENV_VAR = 'CP_PROFILER_DIR';
6+
export const PROFILER_BASE_NAME = 'trace';
7+
export const PROFILER_DIRECTORY = './tmp/profiles';

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { performance } from 'node:perf_hooks';
2+
import process from 'node:process';
3+
import { threadId } from 'node:worker_threads';
24
import { isEnvVarEnabled } from '../env.js';
35
import {
46
type PerformanceObserverOptions,
@@ -26,6 +28,14 @@ import {
2628
PROFILER_ENABLED_ENV_VAR,
2729
} from './constants.js';
2830

31+
/**
32+
* Generates a unique profiler ID based on performance time origin, process ID, thread ID, and instance count.
33+
*/
34+
export function getProfilerId() {
35+
// eslint-disable-next-line functional/immutable-data
36+
return `${Math.round(performance.timeOrigin)}.${process.pid}.${threadId}.${++Profiler.instanceCount}`;
37+
}
38+
2939
/**
3040
* Configuration options for creating a Profiler instance.
3141
*
@@ -69,6 +79,8 @@ export type ProfilerOptions<T extends ActionTrackConfigs = ActionTrackConfigs> =
6979
*
7080
*/
7181
export class Profiler<T extends ActionTrackConfigs> {
82+
static instanceCount = 0;
83+
readonly id = getProfilerId();
7284
#enabled: boolean = false;
7385
readonly #defaults: ActionTrackEntryPayload;
7486
readonly tracks: Record<keyof T, ActionTrackEntryPayload> | undefined;

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { performance } from 'node:perf_hooks';
2+
import { threadId } from 'node:worker_threads';
23
import { beforeEach, describe, expect, it, vi } from 'vitest';
34
import { MockTraceEventFileSink } from '../../../mocks/sink.mock.js';
45
import type { PerformanceEntryEncoder } from '../performance-observer.js';
@@ -9,8 +10,19 @@ import {
910
type NodejsProfilerOptions,
1011
Profiler,
1112
type ProfilerOptions,
13+
getProfilerId,
1214
} from './profiler.js';
1315

16+
describe('getProfilerId', () => {
17+
it('should generate a unique id per process', () => {
18+
expect(getProfilerId()).toBe(
19+
`${Math.round(performance.timeOrigin)}.${process.pid}.${threadId}.1`,
20+
);
21+
expect(getProfilerId()).toBe(
22+
`${Math.round(performance.timeOrigin)}.${process.pid}.${threadId}.2`,
23+
);
24+
});
25+
});
1426
describe('Profiler', () => {
1527
const getProfiler = (overrides?: Partial<ProfilerOptions>) =>
1628
new Profiler({

0 commit comments

Comments
 (0)