Skip to content

Commit 923659b

Browse files
author
John Doe
committed
feat: add profiler class
1 parent 76404c4 commit 923659b

File tree

4 files changed

+244
-152
lines changed

4 files changed

+244
-152
lines changed

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

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ describe('Profiler Integration', () => {
77
let profiler: Profiler<Record<string, ActionTrackEntryPayload>>;
88

99
beforeEach(() => {
10-
// Clear all performance entries before each test
1110
performance.clearMarks();
1211
performance.clearMeasures();
1312

@@ -19,7 +18,7 @@ describe('Profiler Integration', () => {
1918
async: { track: 'async-ops', color: 'secondary' },
2019
sync: { track: 'sync-ops', color: 'tertiary' },
2120
},
22-
enabled: true, // Explicitly enable for integration tests
21+
enabled: true,
2322
});
2423
});
2524

@@ -33,18 +32,17 @@ describe('Profiler Integration', () => {
3332

3433
expect(result).toBe(499_500);
3534

36-
// Verify performance entries were created
3735
const marks = performance.getEntriesByType('mark');
3836
const measures = performance.getEntriesByType('measure');
3937

40-
expect(marks).toEqual(
38+
expect(marks).toStrictEqual(
4139
expect.arrayContaining([
4240
expect.objectContaining({ name: 'test:sync-test:start' }),
4341
expect.objectContaining({ name: 'test:sync-test:end' }),
4442
]),
4543
);
4644

47-
expect(measures).toEqual(
45+
expect(measures).toStrictEqual(
4846
expect.arrayContaining([
4947
expect.objectContaining({
5048
name: 'test:sync-test',
@@ -56,25 +54,23 @@ describe('Profiler Integration', () => {
5654

5755
it('should create complete performance timeline for async operation', async () => {
5856
const result = await profiler.measureAsync('async-test', async () => {
59-
// Simulate async work
6057
await new Promise(resolve => setTimeout(resolve, 10));
6158
return 'async-result';
6259
});
6360

6461
expect(result).toBe('async-result');
6562

66-
// Verify performance entries were created
6763
const marks = performance.getEntriesByType('mark');
6864
const measures = performance.getEntriesByType('measure');
6965

70-
expect(marks).toEqual(
66+
expect(marks).toStrictEqual(
7167
expect.arrayContaining([
7268
expect.objectContaining({ name: 'test:async-test:start' }),
7369
expect.objectContaining({ name: 'test:async-test:end' }),
7470
]),
7571
);
7672

77-
expect(measures).toEqual(
73+
expect(measures).toStrictEqual(
7874
expect.arrayContaining([
7975
expect.objectContaining({
8076
name: 'test:async-test',
@@ -93,10 +89,9 @@ describe('Profiler Integration', () => {
9389
const marks = performance.getEntriesByType('mark');
9490
const measures = performance.getEntriesByType('measure');
9591

96-
expect(marks).toHaveLength(4); // 2 for outer + 2 for inner
97-
expect(measures).toHaveLength(2); // 1 for outer + 1 for inner
92+
expect(marks).toHaveLength(4);
93+
expect(measures).toHaveLength(2);
9894

99-
// Check all marks exist
10095
const markNames = marks.map(m => m.name);
10196
expect(markNames).toStrictEqual(
10297
expect.arrayContaining([
@@ -107,7 +102,6 @@ describe('Profiler Integration', () => {
107102
]),
108103
);
109104

110-
// Check all measures exist
111105
const measureNames = measures.map(m => m.name);
112106
expect(measureNames).toStrictEqual(
113107
expect.arrayContaining(['test:outer', 'test:inner']),
@@ -125,7 +119,7 @@ describe('Profiler Integration', () => {
125119
});
126120

127121
const marks = performance.getEntriesByType('mark');
128-
expect(marks).toEqual(
122+
expect(marks).toStrictEqual(
129123
expect.arrayContaining([
130124
expect.objectContaining({
131125
name: 'test-marker',
@@ -146,15 +140,15 @@ describe('Profiler Integration', () => {
146140
});
147141

148142
it('should create proper DevTools payloads for tracks', () => {
149-
profiler.measure('track-test', () => 'result', {
143+
profiler.measure('track-test', (): string => 'result', {
150144
success: result => ({
151145
properties: [['result', result]],
152146
tooltipText: 'Track test completed',
153147
}),
154148
});
155149

156150
const measures = performance.getEntriesByType('measure');
157-
expect(measures).toEqual(
151+
expect(measures).toStrictEqual(
158152
expect.arrayContaining([
159153
expect.objectContaining({
160154
detail: {
@@ -172,7 +166,6 @@ describe('Profiler Integration', () => {
172166
});
173167

174168
it('should merge track defaults with measurement options', () => {
175-
// Use the sync track from our configuration
176169
profiler.measure('sync-op', () => 'sync-result', {
177170
success: result => ({
178171
properties: [
@@ -183,14 +176,14 @@ describe('Profiler Integration', () => {
183176
});
184177

185178
const measures = performance.getEntriesByType('measure');
186-
expect(measures).toEqual(
179+
expect(measures).toStrictEqual(
187180
expect.arrayContaining([
188181
expect.objectContaining({
189182
detail: {
190183
devtools: expect.objectContaining({
191184
dataType: 'track-entry',
192-
track: 'integration-tests', // default track
193-
color: 'primary', // default color
185+
track: 'integration-tests',
186+
color: 'primary',
194187
properties: [
195188
['operation', 'sync'],
196189
['result', 'sync-result'],
@@ -212,7 +205,7 @@ describe('Profiler Integration', () => {
212205
}).toThrow(error);
213206

214207
const measures = performance.getEntriesByType('measure');
215-
expect(measures).toEqual(
208+
expect(measures).toStrictEqual(
216209
expect.arrayContaining([
217210
expect.objectContaining({
218211
detail: {
@@ -239,7 +232,7 @@ describe('Profiler Integration', () => {
239232
}).toThrow(customError);
240233

241234
const measures = performance.getEntriesByType('measure');
242-
expect(measures).toEqual(
235+
expect(measures).toStrictEqual(
243236
expect.arrayContaining([
244237
expect.objectContaining({
245238
detail: {
@@ -264,21 +257,17 @@ describe('Profiler Integration', () => {
264257
enabled: false,
265258
});
266259

267-
// Test sync measurement
268260
const syncResult = disabledProfiler.measure('disabled-sync', () => 'sync');
269261
expect(syncResult).toBe('sync');
270262

271-
// Test async measurement
272263
const asyncResult = disabledProfiler.measureAsync(
273264
'disabled-async',
274265
async () => 'async',
275266
);
276267
await expect(asyncResult).resolves.toBe('async');
277268

278-
// Test marker
279269
disabledProfiler.marker('disabled-marker');
280270

281-
// Verify no performance entries were created
282271
expect(performance.getEntriesByType('mark')).toHaveLength(0);
283272
expect(performance.getEntriesByType('measure')).toHaveLength(0);
284273
});

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

Lines changed: 20 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,31 @@
11
import process from 'node:process';
22
import { isEnvVarEnabled } from '../env.js';
33
import {
4+
type MeasureCtxOptions,
45
type MeasureOptions,
56
asOptions,
67
markerPayload,
78
measureCtx,
89
setupTracks,
910
} from '../user-timing-extensibility-api-utils.js';
1011
import type {
11-
ActionColorPayload,
1212
ActionTrackEntryPayload,
1313
DevToolsColor,
1414
EntryMeta,
15-
TrackMeta,
1615
} from '../user-timing-extensibility-api.type.js';
1716
import { PROFILER_ENABLED } from './constants.js';
1817

19-
/** Default track configuration combining metadata and color options. */
20-
type DefaultTrackOptions = TrackMeta & ActionColorPayload;
21-
2218
/**
2319
* Configuration options for creating a Profiler instance.
2420
*
2521
* @template T - Record type defining available track names and their configurations
2622
*/
2723
type ProfilerMeasureOptions<T extends Record<string, ActionTrackEntryPayload>> =
28-
DefaultTrackOptions & {
24+
MeasureCtxOptions & {
2925
/** Custom track configurations that will be merged with default settings */
30-
tracks: Record<keyof T, Partial<ActionTrackEntryPayload>>;
26+
tracks?: Record<keyof T, Partial<ActionTrackEntryPayload>>;
3127
/** Whether profiling should be enabled (defaults to CP_PROFILING env var) */
3228
enabled?: boolean;
33-
/** Prefix for all performance measurement names to avoid conflicts */
34-
prefix: string;
3529
};
3630

3731
/**
@@ -51,38 +45,11 @@ export type ProfilerOptions<T extends Record<string, ActionTrackEntryPayload>> =
5145
* integration for Chrome DevTools Performance panel. It supports both synchronous and
5246
* asynchronous operations with customizable track visualization.
5347
*
54-
* @example
55-
* ```typescript
56-
* const profiler = new Profiler({
57-
* prefix: 'api',
58-
* track: 'backend-calls',
59-
* trackGroup: 'api',
60-
* color: 'secondary',
61-
* tracks: {
62-
* database: { track: 'database', color: 'tertiary' },
63-
* external: { track: 'external-apis', color: 'primary' }
64-
* }
65-
* });
66-
*
67-
* // Measure synchronous operation
68-
* const result = profiler.measure('fetch-user', () => api.getUser(id));
69-
*
70-
* // Measure async operation
71-
* const asyncResult = await profiler.measureAsync('save-data',
72-
* () => api.saveData(data)
73-
* );
74-
*
75-
* // Add marker
76-
* profiler.marker('cache-invalidated', {
77-
* color: 'warning',
78-
* tooltipText: 'Cache cleared due to stale data'
79-
* });
80-
* ```
8148
*/
8249
export class Profiler<T extends Record<string, ActionTrackEntryPayload>> {
8350
#enabled: boolean;
8451
private readonly defaults: ActionTrackEntryPayload;
85-
readonly tracks: Record<keyof T, ActionTrackEntryPayload>;
52+
readonly tracks: Record<keyof T, ActionTrackEntryPayload> | undefined;
8653
private readonly ctxOf: ReturnType<typeof measureCtx>;
8754

8855
/**
@@ -96,28 +63,16 @@ export class Profiler<T extends Record<string, ActionTrackEntryPayload>> {
9663
* @param options.color - Default color for track entries
9764
* @param options.enabled - Whether profiling is enabled (defaults to CP_PROFILING env var)
9865
*
99-
* @example
100-
* ```typescript
101-
* const profiler = new Profiler({
102-
* prefix: 'api',
103-
* track: 'backend-calls',
104-
* trackGroup: 'api',
105-
* color: 'secondary',
106-
* enabled: true,
107-
* tracks: {
108-
* database: { track: 'database', color: 'tertiary' },
109-
* cache: { track: 'cache', color: 'primary' }
110-
* }
111-
* });
112-
* ```
11366
*/
11467
constructor(options: ProfilerOptions<T>) {
11568
const { tracks, prefix, enabled, ...defaults } = options;
11669
const dataType = 'track-entry';
11770

11871
this.#enabled = enabled ?? isEnvVarEnabled(PROFILER_ENABLED);
11972
this.defaults = { ...defaults, dataType };
120-
this.tracks = setupTracks({ ...defaults, dataType }, tracks);
73+
this.tracks = tracks
74+
? setupTracks({ ...defaults, dataType }, tracks)
75+
: undefined;
12176
this.ctxOf = measureCtx({
12277
...defaults,
12378
dataType,
@@ -157,13 +112,12 @@ export class Profiler<T extends Record<string, ActionTrackEntryPayload>> {
157112
* returns immediately without creating any performance entries.
158113
*
159114
* @param name - Unique name for the marker
160-
* @param opt - Optional metadata and styling for the marker
115+
* @param opt - Metadata and styling for the marker
161116
* @param opt.color - Color of the marker line (defaults to profiler default)
162117
* @param opt.tooltipText - Text shown on hover
163118
* @param opt.properties - Key-value pairs for detailed view
164119
*
165120
* @example
166-
* ```typescript
167121
* profiler.marker('user-action-start', {
168122
* color: 'primary',
169123
* tooltipText: 'User clicked save button',
@@ -172,7 +126,6 @@ export class Profiler<T extends Record<string, ActionTrackEntryPayload>> {
172126
* ['elementId', 'save-btn']
173127
* ]
174128
* });
175-
* ```
176129
*/
177130
marker(name: string, opt?: EntryMeta & { color: DevToolsColor }) {
178131
if (!this.#enabled) {
@@ -201,26 +154,19 @@ export class Profiler<T extends Record<string, ActionTrackEntryPayload>> {
201154
* @template R - The return type of the work function
202155
* @param event - Name for this measurement event
203156
* @param work - Function to execute and measure
204-
* @param options - Optional measurement configuration overrides
157+
* @param options - Measurement configuration overrides
205158
* @returns The result of the work function
206159
*
207-
* @example
208-
* ```typescript
209-
* const user = profiler.measure('fetch-user', () => {
210-
* return api.getUser(userId);
211-
* }, {
212-
* success: (result) => ({
213-
* properties: [['userId', result.id], ['loadTime', Date.now()]]
214-
* })
215-
* });
216-
* ```
217160
*/
218-
measure<R>(event: string, work: () => R, options?: MeasureOptions): R {
161+
measure<R>(event: string, work: () => R, options?: MeasureOptions<R>): R {
219162
if (!this.#enabled) {
220163
return work();
221164
}
222165

223-
const { start, success, error } = this.ctxOf(event, options);
166+
const { start, success, error } = this.ctxOf(
167+
event,
168+
options as MeasureOptions,
169+
);
224170
start();
225171
try {
226172
const r = work();
@@ -242,34 +188,23 @@ export class Profiler<T extends Record<string, ActionTrackEntryPayload>> {
242188
* @template R - The resolved type of the work promise
243189
* @param event - Name for this measurement event
244190
* @param work - Function returning a promise to execute and measure
245-
* @param options - Optional measurement configuration overrides
191+
* @param options - Measurement configuration overrides
246192
* @returns Promise that resolves to the result of the work function
247193
*
248-
* @example
249-
* ```typescript
250-
* const data = await profiler.measureAsync('save-form', async () => {
251-
* const result = await api.saveForm(formData);
252-
* return result;
253-
* }, {
254-
* success: (result) => ({
255-
* properties: [['recordsSaved', result.count]]
256-
* }),
257-
* error: (err) => ({
258-
* properties: [['errorType', err.name]]
259-
* })
260-
* });
261-
* ```
262194
*/
263195
async measureAsync<R>(
264196
event: string,
265197
work: () => Promise<R>,
266-
options?: MeasureOptions,
198+
options?: MeasureOptions<R>,
267199
): Promise<R> {
268200
if (!this.#enabled) {
269201
return await work();
270202
}
271203

272-
const { start, success, error } = this.ctxOf(event, options);
204+
const { start, success, error } = this.ctxOf(
205+
event,
206+
options as MeasureOptions,
207+
);
273208
start();
274209
try {
275210
const r = work();

0 commit comments

Comments
 (0)