Skip to content

Commit 7c069cc

Browse files
authored
feat: add profiler class and measure API (#1216)
1 parent cb4de31 commit 7c069cc

File tree

6 files changed

+984
-25
lines changed

6 files changed

+984
-25
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const PROFILER_ENABLED_ENV_VAR = 'CP_PROFILING';
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
import { performance } from 'node:perf_hooks';
2+
import { beforeEach, describe, expect, it } from 'vitest';
3+
import type { ActionTrackEntryPayload } from '../user-timing-extensibility-api.type.js';
4+
import { Profiler } from './profiler.js';
5+
6+
describe('Profiler Integration', () => {
7+
let profiler: Profiler<Record<string, ActionTrackEntryPayload>>;
8+
9+
beforeEach(() => {
10+
performance.clearMarks();
11+
performance.clearMeasures();
12+
13+
profiler = new Profiler({
14+
prefix: 'cp',
15+
track: 'CLI',
16+
trackGroup: 'Code Pushup',
17+
color: 'primary-dark',
18+
tracks: {
19+
utils: { track: 'Utils', color: 'primary' },
20+
core: { track: 'Core', color: 'primary-light' },
21+
},
22+
enabled: true,
23+
});
24+
});
25+
26+
it('should create complete performance timeline for sync operation', () => {
27+
const result = profiler.measure('sync-test', () =>
28+
Array.from({ length: 1000 }, (_, i) => i).reduce(
29+
(sum, num) => sum + num,
30+
0,
31+
),
32+
);
33+
34+
expect(result).toBe(499_500);
35+
36+
const marks = performance.getEntriesByType('mark');
37+
const measures = performance.getEntriesByType('measure');
38+
39+
expect(marks).toStrictEqual(
40+
expect.arrayContaining([
41+
expect.objectContaining({
42+
name: 'cp:sync-test:start',
43+
detail: expect.objectContaining({
44+
devtools: expect.objectContaining({ dataType: 'track-entry' }),
45+
}),
46+
}),
47+
expect.objectContaining({
48+
name: 'cp:sync-test:end',
49+
detail: expect.objectContaining({
50+
devtools: expect.objectContaining({ dataType: 'track-entry' }),
51+
}),
52+
}),
53+
]),
54+
);
55+
56+
expect(measures).toStrictEqual(
57+
expect.arrayContaining([
58+
expect.objectContaining({
59+
name: 'cp:sync-test',
60+
duration: expect.any(Number),
61+
detail: expect.objectContaining({
62+
devtools: expect.objectContaining({ dataType: 'track-entry' }),
63+
}),
64+
}),
65+
]),
66+
);
67+
});
68+
69+
it('should create complete performance timeline for async operation', async () => {
70+
const result = await profiler.measureAsync('async-test', async () => {
71+
await new Promise(resolve => setTimeout(resolve, 10));
72+
return 'async-result';
73+
});
74+
75+
expect(result).toBe('async-result');
76+
77+
const marks = performance.getEntriesByType('mark');
78+
const measures = performance.getEntriesByType('measure');
79+
80+
expect(marks).toStrictEqual(
81+
expect.arrayContaining([
82+
expect.objectContaining({
83+
name: 'cp:async-test:start',
84+
detail: expect.objectContaining({
85+
devtools: expect.objectContaining({ dataType: 'track-entry' }),
86+
}),
87+
}),
88+
expect.objectContaining({
89+
name: 'cp:async-test:end',
90+
detail: expect.objectContaining({
91+
devtools: expect.objectContaining({ dataType: 'track-entry' }),
92+
}),
93+
}),
94+
]),
95+
);
96+
97+
expect(measures).toStrictEqual(
98+
expect.arrayContaining([
99+
expect.objectContaining({
100+
name: 'cp:async-test',
101+
duration: expect.any(Number),
102+
detail: expect.objectContaining({
103+
devtools: expect.objectContaining({ dataType: 'track-entry' }),
104+
}),
105+
}),
106+
]),
107+
);
108+
});
109+
110+
it('should handle nested measurements correctly', () => {
111+
profiler.measure('outer', () => {
112+
profiler.measure('inner', () => 'inner-result');
113+
return 'outer-result';
114+
});
115+
116+
const marks = performance.getEntriesByType('mark');
117+
const measures = performance.getEntriesByType('measure');
118+
119+
expect(marks).toHaveLength(4);
120+
expect(measures).toHaveLength(2);
121+
122+
const markNames = marks.map(m => m.name);
123+
expect(markNames).toStrictEqual(
124+
expect.arrayContaining([
125+
'cp:outer:start',
126+
'cp:outer:end',
127+
'cp:inner:start',
128+
'cp:inner:end',
129+
]),
130+
);
131+
132+
const measureNames = measures.map(m => m.name);
133+
expect(measureNames).toStrictEqual(
134+
expect.arrayContaining(['cp:outer', 'cp:inner']),
135+
);
136+
});
137+
138+
it('should create markers with proper metadata', () => {
139+
profiler.marker('test-marker', {
140+
color: 'warning',
141+
tooltipText: 'Test marker tooltip',
142+
properties: [
143+
['event', 'test-event'],
144+
['timestamp', Date.now()],
145+
],
146+
});
147+
148+
const marks = performance.getEntriesByType('mark');
149+
expect(marks).toStrictEqual(
150+
expect.arrayContaining([
151+
expect.objectContaining({
152+
name: 'test-marker',
153+
detail: {
154+
devtools: expect.objectContaining({
155+
dataType: 'marker',
156+
color: 'warning',
157+
tooltipText: 'Test marker tooltip',
158+
properties: [
159+
['event', 'test-event'],
160+
['timestamp', expect.any(Number)],
161+
],
162+
}),
163+
},
164+
}),
165+
]),
166+
);
167+
});
168+
169+
it('should create proper DevTools payloads for tracks', () => {
170+
profiler.measure('track-test', (): string => 'result', {
171+
success: result => ({
172+
properties: [['result', result]],
173+
tooltipText: 'Track test completed',
174+
}),
175+
});
176+
177+
const measures = performance.getEntriesByType('measure');
178+
expect(measures).toStrictEqual(
179+
expect.arrayContaining([
180+
expect.objectContaining({
181+
name: 'cp:track-test',
182+
detail: {
183+
devtools: expect.objectContaining({
184+
dataType: 'track-entry',
185+
track: 'CLI',
186+
trackGroup: 'Code Pushup',
187+
color: 'primary-dark',
188+
properties: [['result', 'result']],
189+
tooltipText: 'Track test completed',
190+
}),
191+
},
192+
}),
193+
]),
194+
);
195+
});
196+
197+
it('should merge track defaults with measurement options', () => {
198+
profiler.measure('sync-op', () => 'sync-result', {
199+
success: result => ({
200+
properties: [
201+
['operation', 'sync'],
202+
['result', result],
203+
],
204+
}),
205+
});
206+
207+
const measures = performance.getEntriesByType('measure');
208+
expect(measures).toStrictEqual(
209+
expect.arrayContaining([
210+
expect.objectContaining({
211+
name: 'cp:sync-op',
212+
detail: {
213+
devtools: expect.objectContaining({
214+
dataType: 'track-entry',
215+
track: 'CLI',
216+
trackGroup: 'Code Pushup',
217+
color: 'primary-dark',
218+
properties: [
219+
['operation', 'sync'],
220+
['result', 'sync-result'],
221+
],
222+
}),
223+
},
224+
}),
225+
]),
226+
);
227+
});
228+
229+
it('should mark errors with red color in DevTools', () => {
230+
const error = new Error('Test error');
231+
232+
expect(() => {
233+
profiler.measure('error-test', () => {
234+
throw error;
235+
});
236+
}).toThrow(error);
237+
238+
const measures = performance.getEntriesByType('measure');
239+
expect(measures).toStrictEqual(
240+
expect.arrayContaining([
241+
expect.objectContaining({
242+
detail: {
243+
devtools: expect.objectContaining({
244+
color: 'error',
245+
properties: expect.arrayContaining([
246+
['Error Type', 'Error'],
247+
['Error Message', 'Test error'],
248+
]),
249+
}),
250+
},
251+
}),
252+
]),
253+
);
254+
});
255+
256+
it('should include error metadata in DevTools properties', () => {
257+
const customError = new TypeError('Custom type error');
258+
259+
expect(() => {
260+
profiler.measure('custom-error-test', () => {
261+
throw customError;
262+
});
263+
}).toThrow(customError);
264+
265+
const measures = performance.getEntriesByType('measure');
266+
expect(measures).toStrictEqual(
267+
expect.arrayContaining([
268+
expect.objectContaining({
269+
detail: {
270+
devtools: expect.objectContaining({
271+
properties: expect.arrayContaining([
272+
['Error Type', 'TypeError'],
273+
['Error Message', 'Custom type error'],
274+
]),
275+
}),
276+
},
277+
}),
278+
]),
279+
);
280+
});
281+
282+
it('should not create performance entries when disabled', async () => {
283+
profiler.setEnabled(false);
284+
285+
const syncResult = profiler.measure('disabled-sync', () => 'sync');
286+
expect(syncResult).toBe('sync');
287+
288+
const asyncResult = profiler.measureAsync(
289+
'disabled-async',
290+
async () => 'async',
291+
);
292+
await expect(asyncResult).resolves.toBe('async');
293+
294+
profiler.marker('disabled-marker');
295+
296+
expect(performance.getEntriesByType('mark')).toHaveLength(0);
297+
expect(performance.getEntriesByType('measure')).toHaveLength(0);
298+
});
299+
});

0 commit comments

Comments
 (0)