Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 172 additions & 26 deletions packages/utils/src/lib/user-timing-extensibility-api-utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { performance } from 'node:perf_hooks';
import { objectToEntries } from './transform.js';
import type {
DevToolsColor,
DevToolsProperties,
Expand All @@ -12,13 +14,20 @@ import type {
const dataTypeTrackEntry = 'track-entry';
const dataTypeMarker = 'marker';

export function mergePropertiesWithOverwrite<
const T extends DevToolsProperties,
const U extends DevToolsProperties,
>(baseProperties: T, overrideProperties: U): (T[number] | U[number])[];
export function mergePropertiesWithOverwrite<
const T extends DevToolsProperties,
>(baseProperties: T): T;
export function mergePropertiesWithOverwrite(
baseProperties: DevToolsProperties | undefined,
overrideProperties?: DevToolsProperties | undefined,
) {
baseProperties?: DevToolsProperties,
overrideProperties?: DevToolsProperties,
): DevToolsProperties {
return [
...new Map([...(baseProperties ?? []), ...(overrideProperties ?? [])]),
] satisfies DevToolsProperties;
];
}

export function markerPayload(options?: Omit<MarkerPayload, 'dataType'>) {
Expand Down Expand Up @@ -49,19 +58,15 @@ export function markerErrorPayload<T extends DevToolsColor>(
} satisfies MarkerPayload;
}

export function trackEntryErrorPayload<
T extends string,
C extends DevToolsColor,
>(
export function trackEntryErrorPayload<T extends string>(
options: Omit<TrackEntryPayload, 'color' | 'dataType'> & {
track: T;
color?: C;
},
) {
const { track, color = 'error' as const, ...restOptions } = options;
const { track, ...restOptions } = options;
return {
dataType: dataTypeTrackEntry,
color,
color: 'error' as const,
track,
...restOptions,
} satisfies TrackEntryPayload;
Expand All @@ -86,7 +91,7 @@ export function errorToEntryMeta(
const { properties, tooltipText } = options ?? {};
const props = mergePropertiesWithOverwrite(
errorToDevToolsProperties(e),
properties,
properties ?? [],
);
return {
properties: props,
Expand Down Expand Up @@ -127,19 +132,6 @@ export function errorToMarkerPayload(
} satisfies MarkerPayload;
}

/**
* asOptions wraps a DevTools payload into the `detail` property of User Timing entry options.
*
* @example
* profiler.mark('mark', asOptions({
* dataType: 'marker',
* color: 'error',
* tooltipText: 'This is a marker',
* properties: [
* ['str', 'This is a detail property']
* ],
* }));
*/
export function asOptions<T extends MarkerPayload>(
devtools?: T | null,
): MarkOptionsWithDevtools<T>;
Expand All @@ -151,5 +143,159 @@ export function asOptions<T extends MarkerPayload | TrackEntryPayload>(
): {
detail?: WithDevToolsPayload<T>;
} {
return devtools == null ? { detail: {} } : { detail: { devtools } };
if (devtools == null) {
return { detail: {} };
}

return { detail: { devtools } };
}

export type Names<N extends string> = {
startName: `${N}:start`;
endName: `${N}:end`;
measureName: N;
};

export function getNames<T extends string>(base: T): Names<T>;
export function getNames<T extends string, P extends string>(
base: T,
prefix?: P,
): Names<`${P}:${T}`>;
export function getNames(base: string, prefix?: string) {
const n = prefix ? `${prefix}:${base}` : base;
return {
startName: `${n}:start`,
endName: `${n}:end`,
measureName: n,
} as const;
}

type Simplify<T> = { [K in keyof T]: T[K] } & object;

type MergeObjects<T extends readonly object[]> = T extends readonly [
infer F extends object,
...infer R extends readonly object[],
]
? Simplify<Omit<F, keyof MergeObjects<R>> & MergeObjects<R>>
: object;

export type MergeResult<
P extends readonly Partial<TrackEntryPayload | MarkerPayload>[],
> = MergeObjects<P> & { properties?: DevToolsProperties };

export function mergeDevtoolsPayload<
const P extends readonly Partial<TrackEntryPayload | MarkerPayload>[],
>(...parts: P): MergeResult<P> {
return parts.reduce(
(acc, cur) => ({
...acc,
...cur,
...(cur.properties || acc.properties
? {
properties: mergePropertiesWithOverwrite(
acc.properties ?? [],
cur.properties ?? [],
),
}
: {}),
}),
{},
) as MergeResult<P>;
}

export function mergeDevtoolsPayloadAction<
const P extends readonly [ActionTrack, ...Partial<ActionTrack>[]],
>(...parts: P): MergeObjects<P> & { properties?: DevToolsProperties } {
return mergeDevtoolsPayload(
...(parts as unknown as readonly Partial<
TrackEntryPayload | MarkerPayload
>[]),
) as MergeObjects<P> & { properties?: DevToolsProperties };
}

export type ActionColorPayload = {
color?: DevToolsColor;
};
export type ActionTrack = TrackEntryPayload & ActionColorPayload;

export function setupTracks<
const T extends Record<string, Partial<ActionTrack>>,
const D extends ActionTrack,
>(defaults: D, tracks: T) {
return objectToEntries(tracks).reduce(
(result, [key, track]) => ({
...result,
[key]: mergeDevtoolsPayload(defaults, track, {
dataType: dataTypeTrackEntry,
}),
}),
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
{} as Record<keyof T, ActionTrack>,
) as Record<keyof T, ActionTrack>;
}

/**
* This is a helper function used to ensure that the marks used to create a measure do not contain UI interaction properties.
* @param devtools - The devtools payload to convert to mark options.
* @returns The mark options without tooltipText and properties.
*/
function toMarkMeasureOpts(devtools: TrackEntryPayload) {
const { tooltipText: _, properties: __, ...markDevtools } = devtools;
return { detail: { devtools: markDevtools } };
}

export type MeasureOptions = Partial<ActionTrack> & {
success?: (result: unknown) => EntryMeta;
error?: (error: unknown) => EntryMeta;
};

export type MeasureCtxOptions = ActionTrack & {
prefix?: string;
} & {
error?: (error: unknown) => EntryMeta;
};
export function measureCtx(cfg: MeasureCtxOptions) {
const { prefix, error: globalErr, ...defaults } = cfg;

return (event: string, opt?: MeasureOptions) => {
const { success, error, ...measurePayload } = opt ?? {};
const merged = mergeDevtoolsPayloadAction(defaults, measurePayload, {
dataType: dataTypeTrackEntry,
}) as TrackEntryPayload;

const {
startName: s,
endName: e,
measureName: m,
} = getNames(event, prefix);

return {
start: () => performance.mark(s, toMarkMeasureOpts(merged)),

success: (r: unknown) => {
const successPayload = mergeDevtoolsPayload(merged, success?.(r) ?? {});
performance.mark(e, toMarkMeasureOpts(successPayload));
performance.measure(m, {
start: s,
end: e,
...asOptions(successPayload),
});
},

error: (err: unknown) => {
const errorPayload = mergeDevtoolsPayload(
errorToEntryMeta(err),
globalErr?.(err) ?? {},
error?.(err) ?? {},
{ ...merged, color: 'error' },
);
performance.mark(e, toMarkMeasureOpts(errorPayload));
performance.measure(m, {
start: s,
end: e,
...asOptions(errorPayload),
});
},
};
};
}
Loading