Skip to content

Commit 56b3cbe

Browse files
author
John Doe
committed
fix: add measure utils
1 parent 7d3681c commit 56b3cbe

File tree

2 files changed

+667
-35
lines changed

2 files changed

+667
-35
lines changed

packages/utils/src/lib/user-timing-extensibility-api-utils.ts

Lines changed: 168 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { performance } from 'node:perf_hooks';
12
import type {
23
DevToolsColor,
34
DevToolsProperties,
@@ -12,13 +13,20 @@ import type {
1213
const dataTypeTrackEntry = 'track-entry';
1314
const dataTypeMarker = 'marker';
1415

16+
export function mergePropertiesWithOverwrite<
17+
const T extends DevToolsProperties,
18+
const U extends DevToolsProperties,
19+
>(baseProperties: T, overrideProperties: U): (T[number] | U[number])[];
20+
export function mergePropertiesWithOverwrite<
21+
const T extends DevToolsProperties,
22+
>(baseProperties: T): T;
1523
export function mergePropertiesWithOverwrite(
16-
baseProperties: DevToolsProperties | undefined,
17-
overrideProperties?: DevToolsProperties | undefined,
18-
) {
24+
baseProperties?: DevToolsProperties,
25+
overrideProperties?: DevToolsProperties,
26+
): DevToolsProperties {
1927
return [
2028
...new Map([...(baseProperties ?? []), ...(overrideProperties ?? [])]),
21-
] satisfies DevToolsProperties;
29+
];
2230
}
2331

2432
export function markerPayload(options?: Omit<MarkerPayload, 'dataType'>) {
@@ -49,19 +57,15 @@ export function markerErrorPayload<T extends DevToolsColor>(
4957
} satisfies MarkerPayload;
5058
}
5159

52-
export function trackEntryErrorPayload<
53-
T extends string,
54-
C extends DevToolsColor,
55-
>(
60+
export function trackEntryErrorPayload<T extends string>(
5661
options: Omit<TrackEntryPayload, 'color' | 'dataType'> & {
5762
track: T;
58-
color?: C;
5963
},
6064
) {
61-
const { track, color = 'error' as const, ...restOptions } = options;
65+
const { track, ...restOptions } = options;
6266
return {
6367
dataType: dataTypeTrackEntry,
64-
color,
68+
color: 'error' as const,
6569
track,
6670
...restOptions,
6771
} satisfies TrackEntryPayload;
@@ -86,7 +90,7 @@ export function errorToEntryMeta(
8690
const { properties, tooltipText } = options ?? {};
8791
const props = mergePropertiesWithOverwrite(
8892
errorToDevToolsProperties(e),
89-
properties,
93+
properties ?? [],
9094
);
9195
return {
9296
properties: props,
@@ -127,19 +131,6 @@ export function errorToMarkerPayload(
127131
} satisfies MarkerPayload;
128132
}
129133

130-
/**
131-
* asOptions wraps a DevTools payload into the `detail` property of User Timing entry options.
132-
*
133-
* @example
134-
* profiler.mark('mark', asOptions({
135-
* dataType: 'marker',
136-
* color: 'error',
137-
* tooltipText: 'This is a marker',
138-
* properties: [
139-
* ['str', 'This is a detail property']
140-
* ],
141-
* }));
142-
*/
143134
export function asOptions<T extends MarkerPayload>(
144135
devtools?: T | null,
145136
): MarkOptionsWithDevtools<T>;
@@ -151,5 +142,156 @@ export function asOptions<T extends MarkerPayload | TrackEntryPayload>(
151142
): {
152143
detail?: WithDevToolsPayload<T>;
153144
} {
154-
return devtools == null ? { detail: {} } : { detail: { devtools } };
145+
if (devtools == null) {
146+
return { detail: {} };
147+
}
148+
149+
return { detail: { devtools } };
150+
}
151+
152+
export type Names<N extends string> = {
153+
startName: `${N}:start`;
154+
endName: `${N}:end`;
155+
measureName: N;
156+
};
157+
158+
export function getNames<T extends string>(base: T): Names<T>;
159+
export function getNames<T extends string, P extends string>(
160+
base: T,
161+
prefix?: P,
162+
): Names<`${P}:${T}`>;
163+
export function getNames(base: string, prefix?: string) {
164+
const n = prefix ? `${prefix}:${base}` : base;
165+
return {
166+
startName: `${n}:start`,
167+
endName: `${n}:end`,
168+
measureName: n,
169+
} as const;
170+
}
171+
172+
type Simplify<T> = { [K in keyof T]: T[K] } & object;
173+
174+
type MergeObjects<T extends readonly object[]> = T extends readonly [
175+
infer F extends object,
176+
...infer R extends readonly object[],
177+
]
178+
? Simplify<Omit<F, keyof MergeObjects<R>> & MergeObjects<R>>
179+
: object;
180+
181+
export type MergeResult<
182+
P extends readonly Partial<TrackEntryPayload | MarkerPayload>[],
183+
> = MergeObjects<P> & { properties?: DevToolsProperties };
184+
185+
export function mergeDevtoolsPayload<
186+
const P extends readonly Partial<TrackEntryPayload | MarkerPayload>[],
187+
>(...parts: P): MergeResult<P> {
188+
return parts.reduce(
189+
(acc, cur) => ({
190+
...acc,
191+
...cur,
192+
...(cur.properties || acc.properties
193+
? {
194+
properties: mergePropertiesWithOverwrite(
195+
acc.properties ?? [],
196+
cur.properties ?? [],
197+
),
198+
}
199+
: {}),
200+
}),
201+
{} as Partial<TrackEntryPayload>,
202+
) as MergeResult<P>;
203+
}
204+
205+
export function mergeDevtoolsPayloadAction<
206+
const P extends readonly [ActionTrack, ...Partial<ActionTrack>[]],
207+
>(...parts: P): MergeObjects<P> & { properties?: DevToolsProperties } {
208+
return mergeDevtoolsPayload(
209+
...(parts as unknown as readonly Partial<
210+
TrackEntryPayload | MarkerPayload
211+
>[]),
212+
) as MergeObjects<P> & { properties?: DevToolsProperties };
213+
}
214+
215+
export type ActionColorPayload = {
216+
color?: DevToolsColor;
217+
};
218+
export type ActionTrack = TrackEntryPayload & ActionColorPayload;
219+
220+
export function setupTracks<
221+
const T extends Record<string, Partial<ActionTrack>>,
222+
const D extends ActionTrack,
223+
>(defaults: D, tracks: T): Record<keyof T, ActionTrack> {
224+
return Object.entries(tracks).reduce(
225+
(result, [key, track]) => ({
226+
...result,
227+
[key]: mergeDevtoolsPayload(defaults, track) as ActionTrack,
228+
}),
229+
{} as Record<keyof T, ActionTrack>,
230+
);
231+
}
232+
233+
/**
234+
* This is a helper function used to ensure that the marks used to create a measure do not contain UI interaction properties.
235+
* @param devtools - The devtools payload to convert to mark options.
236+
* @returns The mark options without tooltipText and properties.
237+
*/
238+
function toMarkMeasureOpts(devtools: TrackEntryPayload) {
239+
const { tooltipText: _, properties: __, ...markDevtools } = devtools;
240+
return { detail: { devtools: markDevtools } };
241+
}
242+
243+
export type MeasureOptions = Partial<ActionTrack> & {
244+
success?: (result: unknown) => EntryMeta;
245+
error?: (error: unknown) => EntryMeta;
246+
};
247+
248+
export type MeasureCtxOptions = ActionTrack & {
249+
prefix?: string;
250+
} & {
251+
error?: (error: unknown) => EntryMeta;
252+
};
253+
export function measureCtx(cfg: MeasureCtxOptions) {
254+
const { prefix, error: globalErr, ...defaults } = cfg;
255+
256+
return (event: string, opt?: MeasureOptions) => {
257+
const { success, error, ...measurePayload } = opt ?? {};
258+
const merged = mergeDevtoolsPayloadAction(defaults, measurePayload, {
259+
dataType: dataTypeTrackEntry,
260+
}) as TrackEntryPayload;
261+
262+
const {
263+
startName: s,
264+
endName: e,
265+
measureName: m,
266+
} = getNames(event, prefix);
267+
268+
return {
269+
start: () => performance.mark(s, toMarkMeasureOpts(merged)),
270+
271+
success: (r: unknown) => {
272+
const successPayload = mergeDevtoolsPayload(merged, success?.(r) ?? {});
273+
performance.mark(e, toMarkMeasureOpts(successPayload));
274+
performance.measure(m, {
275+
start: s,
276+
end: e,
277+
...asOptions(successPayload),
278+
});
279+
},
280+
281+
error: (err: unknown) => {
282+
const errorPayload = mergeDevtoolsPayload(
283+
errorToEntryMeta(err),
284+
globalErr?.(err) ?? {},
285+
error?.(err) ?? {},
286+
{ ...merged, color: 'error' },
287+
);
288+
performance.mark(e, toMarkMeasureOpts(errorPayload));
289+
performance.measure(m, {
290+
start: s,
291+
end: e,
292+
...asOptions(errorPayload),
293+
});
294+
},
295+
};
296+
};
155297
}

0 commit comments

Comments
 (0)