Skip to content

Commit b71f3c7

Browse files
committed
feat: add NodeJSProfiler
1 parent 91f50cf commit b71f3c7

10 files changed

Lines changed: 1705 additions & 17 deletions

packages/utils/docs/profiler.md

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
# @code-pushup/utils - Profiler
2+
3+
[![npm](https://img.shields.io/npm/v/%40code-pushup%2Futils.svg)](https://www.npmjs.com/package/@code-pushup/utils)
4+
[![downloads](https://img.shields.io/npm/dm/%40code-pushup%2Futils)](https://npmtrends.com/@code-pushup/utils)
5+
[![dependencies](https://img.shields.io/librariesio/release/npm/%40code-pushup/utils)](https://www.npmjs.com/package/@code-pushup/utils?activeTab=dependencies)
6+
7+
⏱️ **High-performance profiling utility for structured timing measurements with Chrome DevTools Extensibility API payloads.** 📊
8+
9+
---
10+
11+
The `Profiler` class provides a clean, type-safe API for performance monitoring that integrates seamlessly with Chrome DevTools. It supports both synchronous and asynchronous operations with smart defaults for custom track visualization, enabling developers to track performance bottlenecks and optimize application speed.
12+
13+
## Getting started
14+
15+
1. If you haven't already, install [@code-pushup/utils](../../README.md).
16+
17+
2. Install as a dependency with your package manager:
18+
19+
```sh
20+
npm install @code-pushup/utils
21+
```
22+
23+
```sh
24+
yarn add @code-pushup/utils
25+
```
26+
27+
```sh
28+
pnpm add @code-pushup/utils
29+
```
30+
31+
3. Import and create a profiler instance:
32+
33+
```ts
34+
import { Profiler } from '@code-pushup/utils';
35+
36+
const profiler = new Profiler({
37+
prefix: 'cp',
38+
track: 'CLI',
39+
trackGroup: 'Code Pushup',
40+
color: 'primary-dark',
41+
tracks: {
42+
utils: { track: 'Utils', color: 'primary' },
43+
core: { track: 'Core', color: 'primary-light' },
44+
},
45+
enabled: true,
46+
});
47+
```
48+
49+
4. Start measuring performance:
50+
51+
```ts
52+
// Measure synchronous operations
53+
const result = profiler.measure('data-processing', () => {
54+
return processData(data);
55+
});
56+
57+
// Measure asynchronous operations
58+
const asyncResult = await profiler.measureAsync('api-call', async () => {
59+
return await fetch('/api/data').then(r => r.json());
60+
});
61+
```
62+
63+
## Configuration
64+
65+
```ts
66+
new Profiler<T>(options: ProfilerOptions<T>)
67+
```
68+
69+
**Parameters:**
70+
71+
- `options` - Configuration options for the profiler instance
72+
73+
**Options:**
74+
75+
| Property | Type | Default | Description |
76+
| ------------ | --------- | ----------- | --------------------------------------------------------------- |
77+
| `tracks` | `object` | `undefined` | Custom track configurations merged with defaults |
78+
| `prefix` | `string` | `undefined` | Prefix for all measurement names |
79+
| `track` | `string` | `undefined` | Default track name for measurements |
80+
| `trackGroup` | `string` | `undefined` | Default track group for organization |
81+
| `color` | `string` | `undefined` | Default color for track entries |
82+
| `enabled` | `boolean` | `env var` | Whether profiling is enabled (defaults to CP_PROFILING env var) |
83+
84+
### Environment Variables
85+
86+
- `CP_PROFILING` - Enables or disables profiling globally (boolean)
87+
88+
```bash
89+
# Enable profiling in development
90+
CP_PROFILING=true npm run dev
91+
92+
# Disable profiling in production
93+
CP_PROFILING=false npm run build
94+
```
95+
96+
## API Methods
97+
98+
The profiler provides several methods for different types of performance measurements:
99+
100+
### Synchronous measurements
101+
102+
```ts
103+
profiler.measure<R>(event: string, work: () => R, options?: MeasureOptions<R>): R
104+
```
105+
106+
Measures the execution time of a synchronous operation. Creates performance start/end marks and a final measure with Chrome DevTools Extensibility API payloads.
107+
108+
```ts
109+
const result = profiler.measure(
110+
'file-processing',
111+
() => {
112+
return fs.readFileSync('large-file.txt', 'utf8');
113+
},
114+
{
115+
track: 'io-operations',
116+
color: 'warning',
117+
},
118+
);
119+
```
120+
121+
### Asynchronous measurements
122+
123+
```ts
124+
profiler.measureAsync<R>(event: string, work: () => Promise<R>, options?: MeasureOptions<R>): Promise<R>
125+
```
126+
127+
Measures the execution time of an asynchronous operation.
128+
129+
```ts
130+
const data = await profiler.measureAsync(
131+
'api-request',
132+
async () => {
133+
const response = await fetch('/api/data');
134+
return response.json();
135+
},
136+
{
137+
track: 'network',
138+
trackGroup: 'external',
139+
},
140+
);
141+
```
142+
143+
### Performance markers
144+
145+
```ts
146+
profiler.marker(name: string, options?: EntryMeta & { color?: DevToolsColor }): void
147+
```
148+
149+
Creates a performance mark with Chrome DevTools marker visualization. Markers appear as vertical lines spanning all tracks and can include custom metadata.
150+
151+
```ts
152+
profiler.marker('user-action', {
153+
color: 'secondary',
154+
tooltipText: 'User clicked save button',
155+
properties: [
156+
['action', 'save'],
157+
['elementId', 'save-btn'],
158+
],
159+
});
160+
```
161+
162+
### Runtime control
163+
164+
```ts
165+
profiler.setEnabled(enabled: boolean): void
166+
profiler.isEnabled(): boolean
167+
```
168+
169+
Control profiling at runtime and check current status.
170+
171+
```ts
172+
// Disable profiling temporarily
173+
profiler.setEnabled(false);
174+
175+
// Check if profiling is active
176+
if (profiler.isEnabled()) {
177+
console.log('Performance monitoring is active');
178+
}
179+
```
180+
181+
## Examples
182+
183+
### Basic usage
184+
185+
```ts
186+
import { Profiler } from '@code-pushup/utils';
187+
188+
const profiler = new Profiler({
189+
prefix: 'cp',
190+
track: 'CLI',
191+
trackGroup: 'Code Pushup',
192+
color: 'primary-dark',
193+
tracks: {
194+
utils: { track: 'Utils', color: 'primary' },
195+
core: { track: 'Core', color: 'primary-light' },
196+
},
197+
enabled: true,
198+
});
199+
200+
// Simple measurement
201+
const result = profiler.measure('data-transform', () => {
202+
return transformData(input);
203+
});
204+
205+
// Async measurement with custom options
206+
const data = await profiler.measureAsync(
207+
'fetch-user',
208+
async () => {
209+
return await api.getUser(userId);
210+
},
211+
{
212+
track: 'api',
213+
color: 'info',
214+
},
215+
);
216+
217+
// Add a marker for important events
218+
profiler.marker('user-login', {
219+
tooltipText: 'User authentication completed',
220+
});
221+
```
222+
223+
### Custom tracks
224+
225+
Define custom track configurations for better organization:
226+
227+
```ts
228+
interface AppTracks {
229+
api: ActionTrackEntryPayload;
230+
db: ActionTrackEntryPayload;
231+
cache: ActionTrackEntryPayload;
232+
}
233+
234+
const profiler = new Profiler<AppTracks>({
235+
tracks: {
236+
api: { track: 'api', trackGroup: 'network', color: 'primary' },
237+
db: { track: 'database', trackGroup: 'data', color: 'warning' },
238+
cache: { track: 'cache', trackGroup: 'data', color: 'success' },
239+
},
240+
});
241+
242+
// Use predefined tracks
243+
const users = await profiler.measureAsync('fetch-users', fetchUsers, {
244+
track: 'api',
245+
});
246+
247+
const saved = profiler.measure('save-user', () => saveToDb(user), {
248+
track: 'db',
249+
});
250+
```
251+
252+
## Resources
253+
254+
- **[Chrome DevTools Extensibility API](?)** - Official documentation for performance profiling
255+
- **[User Timing API](https://developer.mozilla.org/en-US/docs/Web/API/User_Timing_API)** - Web Performance API reference

packages/utils/mocks/sink.mock.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { Sink } from '../src/lib/sink-source.type';
1+
import type {
2+
RecoverResult,
3+
Recoverable,
4+
Sink,
5+
} from '../src/lib/sink-source.type';
26

37
export class MockSink implements Sink<string, string> {
48
private writtenItems: string[] = [];
@@ -28,3 +32,21 @@ export class MockSink implements Sink<string, string> {
2832
return [...this.writtenItems];
2933
}
3034
}
35+
36+
export class MockRecoverableSink<T> extends MockSink implements Recoverable<T> {
37+
recover(): RecoverResult<T> {
38+
return {
39+
records: this.getWrittenItems() as T[],
40+
errors: [],
41+
partialTail: null,
42+
};
43+
}
44+
45+
repack(): void {
46+
this.getWrittenItems().forEach(item => this.write(item));
47+
}
48+
49+
finalize(): void {
50+
this.close();
51+
}
52+
}

packages/utils/src/lib/exit-process.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ export function installExitHandlers(options: ExitHandlerOptions = {}): void {
4444
const {
4545
onExit,
4646
onError,
47-
exitOnFatal,
48-
exitOnSignal,
47+
exitOnFatal = false,
48+
exitOnSignal = false,
4949
fatalExitCode = DEFAULT_FATAL_EXIT_CODE,
5050
} = options;
5151

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

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,31 @@ import {
66
} from 'node:perf_hooks';
77
import type { Buffered, Encoder, Observer, Sink } from './sink-source.type';
88

9+
/**
10+
* Encoder that converts PerformanceEntry to domain events.
11+
*
12+
* Pure function that transforms performance entries into one or more domain events.
13+
* Should be stateless, synchronous, and have no side effects.
14+
*/
15+
export type PerformanceEntryEncoder<F> = (
16+
entry: PerformanceEntry,
17+
) => readonly F[];
18+
919
const OBSERVED_TYPES = ['mark', 'measure'] as const;
1020
type ObservedEntryType = 'mark' | 'measure';
1121
export const DEFAULT_FLUSH_THRESHOLD = 20;
1222

1323
export type PerformanceObserverOptions<T> = {
1424
sink: Sink<T, unknown>;
15-
encode: (entry: PerformanceEntry) => T[];
25+
encodePerfEntry: PerformanceEntryEncoder<T>;
1626
buffered?: boolean;
1727
flushThreshold?: number;
1828
};
1929

2030
export class PerformanceObserverSink<T>
21-
implements Observer, Buffered, Encoder<PerformanceEntry, T[]>
31+
implements Observer, Buffered, Encoder<PerformanceEntry, readonly T[]>
2232
{
23-
#encode: (entry: PerformanceEntry) => T[];
33+
#encodePerfEntry: PerformanceEntryEncoder<T>;
2434
#buffered: boolean;
2535
#flushThreshold: number;
2636
#sink: Sink<T, unknown>;
@@ -32,8 +42,8 @@ export class PerformanceObserverSink<T>
3242
#written: Map<ObservedEntryType, number>;
3343

3444
constructor(options: PerformanceObserverOptions<T>) {
35-
const { encode, sink, buffered, flushThreshold } = options;
36-
this.#encode = encode;
45+
const { encodePerfEntry, sink, buffered, flushThreshold } = options;
46+
this.#encodePerfEntry = encodePerfEntry;
3747
this.#written = new Map<ObservedEntryType, number>(
3848
OBSERVED_TYPES.map(t => [t, 0]),
3949
);
@@ -42,14 +52,19 @@ export class PerformanceObserverSink<T>
4252
this.#flushThreshold = flushThreshold ?? DEFAULT_FLUSH_THRESHOLD;
4353
}
4454

45-
encode(entry: PerformanceEntry): T[] {
46-
return this.#encode(entry);
55+
encode(entry: PerformanceEntry): readonly T[] {
56+
return this.#encodePerfEntry(entry);
4757
}
4858

4959
subscribe(): void {
5060
if (this.#observer) {
5161
return;
5262
}
63+
if (this.#sink.isClosed()) {
64+
throw new Error(
65+
'Sink must be opened before subscribing PerformanceObserver',
66+
);
67+
}
5368

5469
// Only used to trigger the flush - it's not processing the entries, just counting them
5570
this.#observer = new PerformanceObserver(
@@ -76,6 +91,11 @@ export class PerformanceObserverSink<T>
7691
if (!this.#observer) {
7792
return;
7893
}
94+
if (this.#sink.isClosed()) {
95+
throw new Error(
96+
'Sink must be opened before subscribing PerformanceObserver',
97+
);
98+
}
7999

80100
OBSERVED_TYPES.forEach(t => {
81101
const written = this.#written.get(t) ?? 0;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const PROFILER_ENABLED = 'CP_PROFILING';

0 commit comments

Comments
 (0)