From a86caacdf8abf8dbe2b97c0829ba470acc8982e4 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 13 Jan 2026 01:29:07 +0100 Subject: [PATCH 01/15] refactor: add trace event utils --- packages/utils/src/lib/trace-file-utils.ts | 208 +++++++ .../src/lib/trace-file-utils.unit.test.ts | 517 ++++++++++++++++++ packages/utils/src/lib/trace-file.type.ts | 288 ++++++++++ .../lib/user-timing-extensibility-api.type.ts | 9 + 4 files changed, 1022 insertions(+) create mode 100644 packages/utils/src/lib/trace-file-utils.ts create mode 100644 packages/utils/src/lib/trace-file-utils.unit.test.ts create mode 100644 packages/utils/src/lib/trace-file.type.ts diff --git a/packages/utils/src/lib/trace-file-utils.ts b/packages/utils/src/lib/trace-file-utils.ts new file mode 100644 index 000000000..eba5a4692 --- /dev/null +++ b/packages/utils/src/lib/trace-file-utils.ts @@ -0,0 +1,208 @@ +import os from 'node:os'; +import { + type PerformanceMark, + type PerformanceMeasure, + performance, +} from 'node:perf_hooks'; +import { threadId } from 'node:worker_threads'; +import { defaultClock } from './clock-epoch'; +import type { + BeginEvent, + CompleteEvent, + EndEvent, + InstantEvent, + InstantEventArgs, + SpanEvent, + SpanEventArgs, + StartTracingEvent, + TraceEvent, + TraceEventContainer, + TraceFile, +} from './trace-file.type.js'; + +export const entryToTraceTimestamp = ( + entry: PerformanceEntry, + asEnd = false, +): number => + defaultClock.fromPerfMs( + entry.startTime + + (entry.entryType === 'measure' && asEnd ? entry.duration : 0), + ); + +export const nextId2 = (() => { + let i = 1; + return () => ({ local: `0x${i++}` }); +})(); + +const defaults = (opt?: { pid?: number; tid?: number; ts?: number }) => ({ + pid: opt?.pid ?? process.pid, + tid: opt?.tid ?? threadId, + ts: opt?.ts ?? defaultClock.fromPerfMs(performance.now()), +}); + +export const frameTreeNodeId = (pid: number, tid: number) => + Number.parseInt(`${pid}0${tid}`, 10); +export const frameName = (pid: number, tid: number) => `FRAME0P${pid}T${tid}`; + +export const getInstantEvent = (opt: { + name: string; + ts?: number; + pid?: number; + tid?: number; + args?: InstantEventArgs; +}): InstantEvent => ({ + cat: 'blink.user_timing', + ph: 'i', + s: 't', + name: opt.name, + ...defaults(opt), + args: opt.args ?? {}, +}); + +export const getStartTracing = (opt: { + url: string; + ts?: number; + pid?: number; + tid?: number; +}): StartTracingEvent => { + const { pid, tid, ts } = defaults(opt); + const id = frameTreeNodeId(pid, tid); + + return { + cat: 'devtools.timeline', + ph: 'i', + s: 't', + name: 'TracingStartedInBrowser', + pid, + tid, + ts, + args: { + data: { + frameTreeNodeId: id, + frames: [ + { + frame: frameName(pid, tid), + isInPrimaryMainFrame: true, + isOutermostMainFrame: true, + name: '', + processId: pid, + url: opt.url, + }, + ], + persistentIds: true, + }, + }, + }; +}; + +export const getCompleteEvent = (opt: { + name: string; + dur: number; + ts?: number; + pid?: number; + tid?: number; +}): CompleteEvent => ({ + cat: 'devtools.timeline', + ph: 'X', + name: opt.name, + dur: opt.dur, + ...defaults(opt), + args: {}, +}); + +type SpanOpt = { + name: string; + id2: { local: string }; + ts?: number; + pid?: number; + tid?: number; + args?: SpanEventArgs; +}; + +export function getSpanEvent(ph: 'b', opt: SpanOpt): BeginEvent; +export function getSpanEvent(ph: 'e', opt: SpanOpt): EndEvent; +export function getSpanEvent(ph: 'b' | 'e', opt: SpanOpt): SpanEvent { + return { + cat: 'blink.user_timing', + ph, + s: 't', + name: opt.name, + id2: opt.id2, + ...defaults(opt), + args: opt.args?.data?.detail + ? { data: { detail: opt.args.data.detail } } + : {}, + } as SpanEvent; +} + +export const getSpan = (opt: { + name: string; + tsB: number; + tsE: number; + id2?: { local: string }; + pid?: number; + tid?: number; + args?: SpanEventArgs; + tsMarkerPadding?: number; +}): [BeginEvent, EndEvent] => { + // tsMarkerPadding is here to make the measure slightly smaller so the markers align perfectly. + // Otherwise, the marker is visible at the start of the measure below the frame + // No padding Padding + // spans: ======== |======| + // marks: | | + const pad = opt.tsMarkerPadding ?? 1; + const id2 = opt.id2 ?? nextId2(); + + return [ + getSpanEvent('b', { + ...opt, + id2, + ts: opt.tsB + pad, + name: opt.name, + args: opt.args, + }), + getSpanEvent('e', { + ...opt, + id2, + ts: opt.tsE - pad, + name: opt.name, + args: opt.args, + }), + ]; +}; + +export const markToInstantEvent = ( + entry: PerformanceMark, + opt?: { name?: string; pid?: number; tid?: number }, +): InstantEvent => + getInstantEvent({ + ...opt, + name: opt?.name ?? entry.name, + ts: defaultClock.fromEntryStartTimeMs(entry.startTime), + args: entry.detail ? { detail: entry.detail } : {}, + }); + +export const measureToSpanEvents = ( + entry: PerformanceMeasure, + opt?: { name?: string; pid?: number; tid?: number }, +): [BeginEvent, EndEvent] => + getSpan({ + ...opt, + name: opt?.name ?? entry.name, + tsB: entryToTraceTimestamp(entry), + tsE: entryToTraceTimestamp(entry, true), + args: entry.detail ? { data: { detail: entry.detail } } : {}, + }); + +export const getTraceFile = (opt: { + traceEvents: TraceEvent[]; + startTime?: string; +}): TraceEventContainer => ({ + traceEvents: opt.traceEvents, + displayTimeUnit: 'ms', + metadata: { + source: 'Node.js UserTiming', + startTime: opt.startTime ?? new Date().toISOString(), + hardwareConcurrency: os.cpus().length, + }, +}); diff --git a/packages/utils/src/lib/trace-file-utils.unit.test.ts b/packages/utils/src/lib/trace-file-utils.unit.test.ts new file mode 100644 index 000000000..b8e53e6f7 --- /dev/null +++ b/packages/utils/src/lib/trace-file-utils.unit.test.ts @@ -0,0 +1,517 @@ +import type { PerformanceMark, PerformanceMeasure } from 'node:perf_hooks'; +import { describe, expect, it } from 'vitest'; +import { + entryToTraceTimestamp, + frameName, + frameTreeNodeId, + getCompleteEvent, + getInstantEvent, + getSpan, + getSpanEvent, + getStartTracing, + getTraceFile, + markToInstantEvent, + measureToSpanEvents, +} from './trace-file-utils.js'; + +describe('getTraceFile', () => { + it('should create trace file with empty events array', () => { + const result = getTraceFile({ traceEvents: [] }); + + expect(result).toStrictEqual({ + traceEvents: [], + displayTimeUnit: 'ms', + metadata: { + source: 'Node.js UserTiming', + startTime: expect.any(String), + hardwareConcurrency: expect.any(Number), + }, + }); + expect(() => new Date(result?.metadata!.startTime)).not.toThrow(); + }); + + it('should create trace file with events', () => { + expect( + getTraceFile({ + traceEvents: [ + getInstantEvent({ + name: 'test-event', + ts: 1234567890, + pid: 123, + tid: 456, + }), + ], + }), + ).toStrictEqual({ + traceEvents: [ + expect.objectContaining({ + name: 'test-event', + ts: 1234567890, + pid: 123, + tid: 456, + }), + ], + displayTimeUnit: 'ms', + metadata: { + source: 'Node.js UserTiming', + startTime: expect.any(String), + hardwareConcurrency: expect.any(Number), + }, + }); + }); + + it('should use custom startTime when provided', () => { + const result = getTraceFile({ + traceEvents: [], + startTime: '2023-01-01T00:00:00.000Z', + }); + + expect(result.metadata?.startTime).toBe('2023-01-01T00:00:00.000Z'); + }); + + it('should include hardware concurrency', () => { + expect( + getTraceFile({ traceEvents: [] }).metadata?.hardwareConcurrency, + ).toBeGreaterThan(0); + }); +}); + +describe('frameTreeNodeId', () => { + it('should generate correct frame tree node ID', () => { + expect(frameTreeNodeId(123, 456)).toBe(1230456); + expect(frameTreeNodeId(1, 2)).toBe(102); + expect(frameTreeNodeId(999, 999)).toBe(9990999); + }); +}); + +describe('frameName', () => { + it('should generate correct frame name', () => { + expect(frameName(123, 456)).toBe('FRAME0P123T456'); + expect(frameName(1, 2)).toBe('FRAME0P1T2'); + expect(frameName(999, 999)).toBe('FRAME0P999T999'); + }); +}); + +describe('getStartTracing', () => { + it('should create start tracing event with required url', () => { + expect(getStartTracing({ url: 'https://example.com' })).toStrictEqual({ + cat: 'devtools.timeline', + ph: 'i', + s: 't', + name: 'TracingStartedInBrowser', + pid: expect.any(Number), + tid: expect.any(Number), + ts: expect.any(Number), + args: { + data: { + frameTreeNodeId: expect.any(Number), + frames: [ + { + frame: expect.stringMatching(/^FRAME0P\d+T\d+$/), + isInPrimaryMainFrame: true, + isOutermostMainFrame: true, + name: '', + processId: expect.any(Number), + url: 'https://example.com', + }, + ], + persistentIds: true, + }, + }, + }); + }); + + it('should use custom pid and tid', () => { + expect( + getStartTracing({ + url: 'https://test.com', + pid: 777, + tid: 888, + }), + ).toStrictEqual({ + cat: 'devtools.timeline', + ph: 'i', + s: 't', + name: 'TracingStartedInBrowser', + pid: 777, + tid: 888, + ts: expect.any(Number), + args: { + data: { + frameTreeNodeId: 7770888, + frames: [ + { + frame: 'FRAME0P777T888', + isInPrimaryMainFrame: true, + isOutermostMainFrame: true, + name: '', + processId: 777, + url: 'https://test.com', + }, + ], + persistentIds: true, + }, + }, + }); + }); +}); + +describe('getCompleteEvent', () => { + it('should create complete event with required fields', () => { + expect( + getCompleteEvent({ + name: 'test-complete', + dur: 1000, + }), + ).toStrictEqual({ + cat: 'devtools.timeline', + ph: 'X', + name: 'test-complete', + dur: 1000, + pid: expect.any(Number), + tid: expect.any(Number), + ts: expect.any(Number), + args: {}, + }); + }); + + it('should use custom pid, tid, and ts', () => { + expect( + getCompleteEvent({ + name: 'custom-complete', + dur: 500, + pid: 111, + tid: 222, + ts: 1234567890, + }), + ).toStrictEqual({ + cat: 'devtools.timeline', + ph: 'X', + name: 'custom-complete', + dur: 500, + pid: 111, + tid: 222, + ts: 1234567890, + args: {}, + }); + }); +}); + +describe('markToInstantEvent', () => { + it('should convert performance mark to instant event with detail', () => { + expect( + markToInstantEvent({ + name: 'test-mark', + startTime: 1000, + detail: { customData: 'test' }, + } as PerformanceMark), + ).toStrictEqual({ + cat: 'blink.user_timing', + ph: 'i', + s: 't', + name: 'test-mark', + pid: expect.any(Number), + tid: expect.any(Number), + ts: expect.any(Number), + args: { detail: { customData: 'test' } }, + }); + }); + + it('should convert performance mark to instant event without detail', () => { + expect( + markToInstantEvent({ + name: 'test-mark', + startTime: 1000, + detail: null, + } as PerformanceMark), + ).toStrictEqual({ + cat: 'blink.user_timing', + ph: 'i', + s: 't', + name: 'test-mark', + pid: expect.any(Number), + tid: expect.any(Number), + ts: expect.any(Number), + args: {}, + }); + }); + + it('should use custom options when provided', () => { + expect( + markToInstantEvent( + { + name: 'test-mark', + startTime: 1000, + detail: { customData: 'test' }, + } as PerformanceMark, + { + name: 'custom-name', + pid: 999, + tid: 888, + }, + ), + ).toStrictEqual({ + cat: 'blink.user_timing', + ph: 'i', + s: 't', + name: 'custom-name', + pid: 999, + tid: 888, + ts: expect.any(Number), + args: { detail: { customData: 'test' } }, + }); + }); +}); + +describe('measureToSpanEvents', () => { + it('should convert performance measure to span events with detail', () => { + expect( + measureToSpanEvents({ + name: 'test-measure', + startTime: 1000, + duration: 500, + detail: { measurement: 'data' }, + } as PerformanceMeasure), + ).toStrictEqual([ + { + cat: 'blink.user_timing', + ph: 'b', + s: 't', + name: 'test-measure', + pid: expect.any(Number), + tid: expect.any(Number), + ts: expect.any(Number), + id2: { local: expect.stringMatching(/^0x\d+$/) }, + args: { data: { detail: { measurement: 'data' } } }, + }, + { + cat: 'blink.user_timing', + ph: 'e', + s: 't', + name: 'test-measure', + pid: expect.any(Number), + tid: expect.any(Number), + ts: expect.any(Number), + id2: { local: expect.stringMatching(/^0x\d+$/) }, + args: { data: { detail: { measurement: 'data' } } }, + }, + ]); + }); + + it('should convert performance measure to span events without detail', () => { + expect( + measureToSpanEvents({ + name: 'test-measure', + startTime: 1000, + duration: 500, + detail: undefined, + } as PerformanceMeasure), + ).toStrictEqual([ + { + cat: 'blink.user_timing', + ph: 'b', + s: 't', + name: 'test-measure', + pid: expect.any(Number), + tid: expect.any(Number), + ts: expect.any(Number), + id2: { local: expect.stringMatching(/^0x\d+$/) }, + args: {}, + }, + { + cat: 'blink.user_timing', + ph: 'e', + s: 't', + name: 'test-measure', + pid: expect.any(Number), + tid: expect.any(Number), + ts: expect.any(Number), + id2: { local: expect.stringMatching(/^0x\d+$/) }, + args: {}, + }, + ]); + }); + + it('should use custom options when provided', () => { + const result = measureToSpanEvents( + { + name: 'test-measure', + startTime: 1000, + duration: 500, + detail: { measurement: 'data' }, + } as PerformanceMeasure, + { + name: 'custom-measure', + pid: 777, + tid: 666, + }, + ); + + expect(result).toHaveLength(2); + expect(result[0].name).toBe('custom-measure'); + expect(result[0].pid).toBe(777); + expect(result[0].tid).toBe(666); + expect(result[0].args).toStrictEqual({ + data: { detail: { measurement: 'data' } }, + }); + }); +}); + +describe('entryToTraceTimestamp', () => { + it('should convert entry timestamp for start time', () => { + expect( + typeof entryToTraceTimestamp({ + startTime: 1000, + duration: 500, + entryType: 'measure', + } as PerformanceMeasure), + ).toBe('number'); + }); + + it('should convert entry timestamp for end time', () => { + const mockEntry = { + startTime: 1000, + duration: 500, + entryType: 'measure', + } as PerformanceMeasure; + + expect(entryToTraceTimestamp(mockEntry, true)).toBeGreaterThan( + entryToTraceTimestamp(mockEntry, false), + ); + }); + + it('should handle non-measure entries', () => { + expect( + typeof entryToTraceTimestamp( + { + startTime: 1000, + entryType: 'mark', + } as PerformanceMark, + true, + ), + ).toBe('number'); + }); +}); + +describe('getSpanEvent', () => { + it('should create begin event with args detail', () => { + expect( + getSpanEvent('b', { + name: 'test-span', + id2: { local: '0x1' }, + args: { data: { detail: { customData: 'test' } as any } }, + }), + ).toStrictEqual({ + cat: 'blink.user_timing', + ph: 'b', + s: 't', + name: 'test-span', + pid: expect.any(Number), + tid: expect.any(Number), + ts: expect.any(Number), + id2: { local: '0x1' }, + args: { data: { detail: { customData: 'test' } } }, + }); + }); + + it('should create end event without args detail', () => { + expect( + getSpanEvent('e', { + name: 'test-span', + id2: { local: '0x2' }, + }), + ).toStrictEqual({ + cat: 'blink.user_timing', + ph: 'e', + s: 't', + name: 'test-span', + pid: expect.any(Number), + tid: expect.any(Number), + ts: expect.any(Number), + id2: { local: '0x2' }, + args: {}, + }); + }); +}); + +describe('getSpan', () => { + it('should create span events with custom tsMarkerPadding', () => { + const result = getSpan({ + name: 'test-span', + tsB: 1000, + tsE: 1500, + tsMarkerPadding: 5, + }); + + expect(result).toStrictEqual([ + { + cat: 'blink.user_timing', + ph: 'b', + s: 't', + name: 'test-span', + pid: expect.any(Number), + tid: expect.any(Number), + ts: 1005, + id2: { local: expect.stringMatching(/^0x\d+$/) }, + args: {}, + }, + { + cat: 'blink.user_timing', + ph: 'e', + s: 't', + name: 'test-span', + pid: expect.any(Number), + tid: expect.any(Number), + ts: 1495, + id2: { local: expect.stringMatching(/^0x\d+$/) }, + args: {}, + }, + ]); + }); + + it('should generate id2 when not provided', () => { + const result = getSpan({ + name: 'test-span', + tsB: 1000, + tsE: 1500, + }); + + expect(result).toHaveLength(2); + expect(result[0].id2?.local).toMatch(/^0x\d+$/); + expect(result[1].id2).toEqual(result[0].id2); + }); + + it('should use provided id2', () => { + expect( + getSpan({ + name: 'test-span', + tsB: 1000, + tsE: 1500, + id2: { local: 'custom-id' }, + }), + ).toStrictEqual([ + { + cat: 'blink.user_timing', + ph: 'b', + s: 't', + name: 'test-span', + pid: expect.any(Number), + tid: expect.any(Number), + ts: 1001, + id2: { local: 'custom-id' }, + args: {}, + }, + { + cat: 'blink.user_timing', + ph: 'e', + s: 't', + name: 'test-span', + pid: expect.any(Number), + tid: expect.any(Number), + ts: 1499, + id2: { local: 'custom-id' }, + args: {}, + }, + ]); + }); +}); diff --git a/packages/utils/src/lib/trace-file.type.ts b/packages/utils/src/lib/trace-file.type.ts new file mode 100644 index 000000000..0cb564a61 --- /dev/null +++ b/packages/utils/src/lib/trace-file.type.ts @@ -0,0 +1,288 @@ +import type { UserTimingDetail } from './user-timing-extensibility-api.type'; + +/** + * Arguments for instant trace events. + * @property {UserTimingDetail} [detail] - Optional user timing detail with DevTools payload + */ +export type InstantEventArgs = { detail?: UserTimingDetail }; + +/** + * Arguments for span trace events (begin/end events). + * @property {object} [data] - Optional data object + * @property {UserTimingDetail} [data.detail] - Optional user timing detail with DevTools payload + */ +export type SpanEventArgs = { data?: { detail?: UserTimingDetail } }; + +/** + * Arguments for complete trace events. + * @property {Record} [detail] - Optional detail object with arbitrary properties + */ +export type CompleteEventArgs = { detail?: Record }; + +/** + * Arguments for start tracing events. + * @property {object} data - Tracing initialization data + * @property {number} data.frameTreeNodeId - Frame tree node identifier + * @property {Array} data.frames - Array of frame information + * @property {boolean} data.persistentIds - Whether IDs are persistent + */ +export type StartTracingEventArgs = { + data: { + frameTreeNodeId: number; + frames: Array<{ + frame: string; + isInPrimaryMainFrame: boolean; + isOutermostMainFrame: boolean; + name: string; + processId: number; + url: string; + }>; + persistentIds: boolean; + }; +}; + +/** + * Union type of all possible trace event arguments. + */ +export type TraceArgs = + | InstantEventArgs + | SpanEventArgs + | CompleteEventArgs + | StartTracingEventArgs; + +/** + * Base properties shared by all trace events. + * @property {string} cat - Event category + * @property {string} name - Event name + * @property {number} pid - Process ID + * @property {number} tid - Thread ID + * @property {number} ts - Timestamp in microseconds + * @property {TraceArgs} [args] - Optional event arguments + */ +export type BaseTraceEvent = { + cat: string; + name: string; + pid: number; + tid: number; + ts: number; + args: TraceArgs; +}; + +/** + * Complete trace event with duration. + * Represents a complete operation with start time and duration. + * @property {'X'} ph - Phase indicator for complete events + * @property {number} dur - Duration in microseconds + */ +export type CompleteEvent = BaseTraceEvent & { ph: 'X'; dur: number }; + +/** + * Instant trace event representing a single point in time. + * Used for user timing marks and other instantaneous events. + * @property {'blink.user_timing'} cat - Fixed category for user timing events + * @property {'i'} ph - Phase indicator for instant events + * @property {'t'} s - Scope indicator (thread) + * @property {never} [dur] - Duration is not applicable for instant events + * @property {InstantEventArgs} [args] - Optional event arguments + */ +export type InstantEvent = Omit & { + cat: 'blink.user_timing'; + ph: 'i'; + s: 't'; + dur?: never; + args: InstantEventArgs; +}; + +/** + * Core properties for span trace events (begin/end pairs). + * @property {object} id2 - Span identifier + * @property {string} id2.local - Local span ID (unique to the process, same for b and e events) + * @property {SpanEventArgs} [args] - Optional event arguments + */ +type SpanCore = Omit & { + id2: { local: string }; + args: SpanEventArgs; +}; +/** + * Begin event for a span (paired with an end event). + * @property {'b'} ph - Phase indicator for begin events + * @property {never} [dur] - Duration is not applicable for begin events + */ +export type BeginEvent = SpanCore & { ph: 'b'; dur?: never }; + +/** + * End event for a span (paired with a begin event). + * @property {'e'} ph - Phase indicator for end events + * @property {never} [dur] - Duration is not applicable for end events + */ +export type EndEvent = SpanCore & { ph: 'e'; dur?: never }; + +/** + * Union type for span events (begin or end). + */ +export type SpanEvent = BeginEvent | EndEvent; + +/** + * Union type of all trace event types. + */ +/** + * Start tracing event for Chrome DevTools tracing. + */ +export type StartTracingEvent = BaseTraceEvent & { + cat: 'devtools.timeline'; + ph: 'i'; + s: 't'; + name: 'TracingStartedInBrowser'; + args: StartTracingEventArgs; +}; + +export type TraceEvent = + | InstantEvent + | CompleteEvent + | SpanEvent + | StartTracingEvent; + +/** + * Raw arguments format for trace events before processing. + * Either contains a detail string directly or nested in a data object. + */ +type RawArgs = + | { detail?: string; [key: string]: unknown } + | { data?: { detail?: string }; [key: string]: unknown }; + +/** + * Raw trace event format before type conversion. + * Similar to TraceEvent but with unprocessed arguments. + */ +export type TraceEventRaw = Omit & { args: RawArgs }; + +/** + * Time window bounds (min, max) in trace time units (e.g. microseconds). + * @property {number} min - Minimum timestamp in the window + * @property {number} max - Maximum timestamp in the window + * @property {number} range - Calculated range (max - min) + */ +export type BreadcrumbWindow = { + min: number; + max: number; + range: number; +}; + +/** + * Custom label for a specific trace entry. + * @property {number | string} entryId - ID or index of the trace entry + * @property {string} label - Label text for the entry + * @property {string} [color] - Optional display color for the label + */ +export type EntryLabel = { + entryId: number | string; + label: string; + color?: string; +}; + +/** + * Link or relation between two trace entries. + * @property {number | string} fromEntryId - Source entry ID for the link + * @property {number | string} toEntryId - Target entry ID for the link + * @property {string} [linkType] - Optional type or description of the link + */ +export type EntryLink = { + fromEntryId: number | string; + toEntryId: number | string; + linkType?: string; +}; + +/** + * A time range annotated with a label. + * @property {number} startTime - Start timestamp of the range (microseconds) + * @property {number} endTime - End timestamp of the range (microseconds) + * @property {string} label - Annotation label for the time range + * @property {string} [color] - Optional display color for the range + */ +export type LabelledTimeRange = { + startTime: number; + endTime: number; + label: string; + color?: string; +}; + +/** + * Hidden or expandable entries information. + * @property {unknown[]} hiddenEntries - IDs or indexes of hidden entries + * @property {unknown[]} expandableEntries - IDs or indexes of expandable entries + */ +export type EntriesModifications = { + hiddenEntries: unknown[]; + expandableEntries: unknown[]; +}; + +/** + * Initial breadcrumb information for time ranges and window. + * @property {BreadcrumbWindow} window - Time window bounds + * @property {unknown | null} child - Child breadcrumb or null + */ +export type InitialBreadcrumb = { + window: BreadcrumbWindow; + child: unknown | null; +}; + +/** + * Annotations such as labels and links between entries. + * @property {EntryLabel[]} entryLabels - Custom labels for entries + * @property {LabelledTimeRange[]} labelledTimeRanges - Time ranges annotated with labels + * @property {EntryLink[]} linksBetweenEntries - Links or relations between entries + */ +export type Annotations = { + entryLabels: EntryLabel[]; + labelledTimeRanges: LabelledTimeRange[]; + linksBetweenEntries: EntryLink[]; +}; + +/** + * Modifications made to trace data or UI in DevTools export + */ +export type Modifications = { + entriesModifications: EntriesModifications; + initialBreadcrumb: InitialBreadcrumb; + annotations: Annotations; +}; + +/** + * Top-level metadata for a trace file exported by Chrome DevTools. + * DevTools may add new fields over time. + */ +export type TraceMetadata = { + /** Usually "DevTools" for exports from the Performance panel */ + source: string; + + /** ISO timestamp when trace was recorded */ + startTime: string; + + /** May be present when recorded with throttling settings */ + hardwareConcurrency?: number; + + /** Common fields found in DevTools traces */ + cpuThrottling?: number; + networkThrottling?: string; + enhancedTraceVersion?: number; + + /** Allow additional custom metadata properties */ + [key: string]: unknown; +}; + +/** + * Structured container for trace events with metadata. + * @property {TraceEvent[]} traceEvents - Array of trace events + * @property {'ms' | 'ns'} [displayTimeUnit] - Time unit for display (milliseconds or nanoseconds) + * @property {TraceMetadata} [metadata] - Optional metadata about the trace + */ +export type TraceEventContainer = { + traceEvents: TraceEvent[]; + displayTimeUnit?: 'ms' | 'ns'; + metadata?: TraceMetadata; +}; + +/** + * Trace file format - either an array of events or a structured container. + */ +export type TraceFile = TraceEvent[] | TraceEventContainer; diff --git a/packages/utils/src/lib/user-timing-extensibility-api.type.ts b/packages/utils/src/lib/user-timing-extensibility-api.type.ts index 0e75be77b..7ea4979f2 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api.type.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api.type.ts @@ -152,3 +152,12 @@ export type MarkOptionsWithDevtools< export type MeasureOptionsWithDevtools = { detail?: WithDevToolsPayload; } & Omit; + +/** + * Detail object containing DevTools payload for user timing events. + * Extends WithDevToolsPayload to include track entry or marker payload. + * This can be used in trace event arguments to provide additional context in DevTools. + */ +export type UserTimingDetail = WithDevToolsPayload< + TrackEntryPayload | MarkerPayload +>; From 510566fda5f7fe964a647efec2826fb8e255a19b Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 13 Jan 2026 21:08:52 +0100 Subject: [PATCH 02/15] refactor: wip trace helper --- packages/utils/package.json | 2 +- packages/utils/src/lib/trace-file-utils.ts | 16 +++----- .../src/lib/trace-file-utils.unit.test.ts | 29 ++++--------- packages/utils/src/lib/trace-file.type.ts | 41 +++++++++++-------- 4 files changed, 37 insertions(+), 51 deletions(-) diff --git a/packages/utils/package.json b/packages/utils/package.json index 0f8098777..b9bf87395 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -24,7 +24,7 @@ }, "type": "module", "engines": { - "node": ">=17.0.0" + "node": ">=18.2.0" }, "dependencies": { "@code-pushup/models": "0.102.0", diff --git a/packages/utils/src/lib/trace-file-utils.ts b/packages/utils/src/lib/trace-file-utils.ts index eba5a4692..9c9bca3cc 100644 --- a/packages/utils/src/lib/trace-file-utils.ts +++ b/packages/utils/src/lib/trace-file-utils.ts @@ -1,11 +1,12 @@ import os from 'node:os'; import { + type PerformanceEntry, type PerformanceMark, type PerformanceMeasure, performance, } from 'node:perf_hooks'; import { threadId } from 'node:worker_threads'; -import { defaultClock } from './clock-epoch'; +import { defaultClock } from './clock-epoch.js'; import type { BeginEvent, CompleteEvent, @@ -17,7 +18,6 @@ import type { StartTracingEvent, TraceEvent, TraceEventContainer, - TraceFile, } from './trace-file.type.js'; export const entryToTraceTimestamp = ( @@ -29,10 +29,9 @@ export const entryToTraceTimestamp = ( (entry.entryType === 'measure' && asEnd ? entry.duration : 0), ); -export const nextId2 = (() => { - let i = 1; - return () => ({ local: `0x${i++}` }); -})(); +// eslint-disable-next-line functional/no-let +let id2Count = 0; +export const nextId2 = () => ({ local: `0x${++id2Count}` }); const defaults = (opt?: { pid?: number; tid?: number; ts?: number }) => ({ pid: opt?.pid ?? process.pid, @@ -53,7 +52,6 @@ export const getInstantEvent = (opt: { }): InstantEvent => ({ cat: 'blink.user_timing', ph: 'i', - s: 't', name: opt.name, ...defaults(opt), args: opt.args ?? {}, @@ -71,7 +69,6 @@ export const getStartTracing = (opt: { return { cat: 'devtools.timeline', ph: 'i', - s: 't', name: 'TracingStartedInBrowser', pid, tid, @@ -125,14 +122,13 @@ export function getSpanEvent(ph: 'b' | 'e', opt: SpanOpt): SpanEvent { return { cat: 'blink.user_timing', ph, - s: 't', name: opt.name, id2: opt.id2, ...defaults(opt), args: opt.args?.data?.detail ? { data: { detail: opt.args.data.detail } } : {}, - } as SpanEvent; + }; } export const getSpan = (opt: { diff --git a/packages/utils/src/lib/trace-file-utils.unit.test.ts b/packages/utils/src/lib/trace-file-utils.unit.test.ts index b8e53e6f7..e61ea9521 100644 --- a/packages/utils/src/lib/trace-file-utils.unit.test.ts +++ b/packages/utils/src/lib/trace-file-utils.unit.test.ts @@ -36,7 +36,7 @@ describe('getTraceFile', () => { traceEvents: [ getInstantEvent({ name: 'test-event', - ts: 1234567890, + ts: 1_234_567_890, pid: 123, tid: 456, }), @@ -46,7 +46,7 @@ describe('getTraceFile', () => { traceEvents: [ expect.objectContaining({ name: 'test-event', - ts: 1234567890, + ts: 1_234_567_890, pid: 123, tid: 456, }), @@ -78,9 +78,9 @@ describe('getTraceFile', () => { describe('frameTreeNodeId', () => { it('should generate correct frame tree node ID', () => { - expect(frameTreeNodeId(123, 456)).toBe(1230456); + expect(frameTreeNodeId(123, 456)).toBe(1_230_456); expect(frameTreeNodeId(1, 2)).toBe(102); - expect(frameTreeNodeId(999, 999)).toBe(9990999); + expect(frameTreeNodeId(999, 999)).toBe(9_990_999); }); }); @@ -97,7 +97,6 @@ describe('getStartTracing', () => { expect(getStartTracing({ url: 'https://example.com' })).toStrictEqual({ cat: 'devtools.timeline', ph: 'i', - s: 't', name: 'TracingStartedInBrowser', pid: expect.any(Number), tid: expect.any(Number), @@ -131,14 +130,13 @@ describe('getStartTracing', () => { ).toStrictEqual({ cat: 'devtools.timeline', ph: 'i', - s: 't', name: 'TracingStartedInBrowser', pid: 777, tid: 888, ts: expect.any(Number), args: { data: { - frameTreeNodeId: 7770888, + frameTreeNodeId: 7_770_888, frames: [ { frame: 'FRAME0P777T888', @@ -182,7 +180,7 @@ describe('getCompleteEvent', () => { dur: 500, pid: 111, tid: 222, - ts: 1234567890, + ts: 1_234_567_890, }), ).toStrictEqual({ cat: 'devtools.timeline', @@ -191,7 +189,7 @@ describe('getCompleteEvent', () => { dur: 500, pid: 111, tid: 222, - ts: 1234567890, + ts: 1_234_567_890, args: {}, }); }); @@ -208,7 +206,6 @@ describe('markToInstantEvent', () => { ).toStrictEqual({ cat: 'blink.user_timing', ph: 'i', - s: 't', name: 'test-mark', pid: expect.any(Number), tid: expect.any(Number), @@ -227,7 +224,6 @@ describe('markToInstantEvent', () => { ).toStrictEqual({ cat: 'blink.user_timing', ph: 'i', - s: 't', name: 'test-mark', pid: expect.any(Number), tid: expect.any(Number), @@ -253,7 +249,6 @@ describe('markToInstantEvent', () => { ).toStrictEqual({ cat: 'blink.user_timing', ph: 'i', - s: 't', name: 'custom-name', pid: 999, tid: 888, @@ -276,7 +271,6 @@ describe('measureToSpanEvents', () => { { cat: 'blink.user_timing', ph: 'b', - s: 't', name: 'test-measure', pid: expect.any(Number), tid: expect.any(Number), @@ -287,7 +281,6 @@ describe('measureToSpanEvents', () => { { cat: 'blink.user_timing', ph: 'e', - s: 't', name: 'test-measure', pid: expect.any(Number), tid: expect.any(Number), @@ -310,7 +303,6 @@ describe('measureToSpanEvents', () => { { cat: 'blink.user_timing', ph: 'b', - s: 't', name: 'test-measure', pid: expect.any(Number), tid: expect.any(Number), @@ -321,7 +313,6 @@ describe('measureToSpanEvents', () => { { cat: 'blink.user_timing', ph: 'e', - s: 't', name: 'test-measure', pid: expect.any(Number), tid: expect.any(Number), @@ -404,7 +395,6 @@ describe('getSpanEvent', () => { ).toStrictEqual({ cat: 'blink.user_timing', ph: 'b', - s: 't', name: 'test-span', pid: expect.any(Number), tid: expect.any(Number), @@ -423,7 +413,6 @@ describe('getSpanEvent', () => { ).toStrictEqual({ cat: 'blink.user_timing', ph: 'e', - s: 't', name: 'test-span', pid: expect.any(Number), tid: expect.any(Number), @@ -447,7 +436,6 @@ describe('getSpan', () => { { cat: 'blink.user_timing', ph: 'b', - s: 't', name: 'test-span', pid: expect.any(Number), tid: expect.any(Number), @@ -458,7 +446,6 @@ describe('getSpan', () => { { cat: 'blink.user_timing', ph: 'e', - s: 't', name: 'test-span', pid: expect.any(Number), tid: expect.any(Number), @@ -493,7 +480,6 @@ describe('getSpan', () => { { cat: 'blink.user_timing', ph: 'b', - s: 't', name: 'test-span', pid: expect.any(Number), tid: expect.any(Number), @@ -504,7 +490,6 @@ describe('getSpan', () => { { cat: 'blink.user_timing', ph: 'e', - s: 't', name: 'test-span', pid: expect.any(Number), tid: expect.any(Number), diff --git a/packages/utils/src/lib/trace-file.type.ts b/packages/utils/src/lib/trace-file.type.ts index 0cb564a61..c081e926b 100644 --- a/packages/utils/src/lib/trace-file.type.ts +++ b/packages/utils/src/lib/trace-file.type.ts @@ -4,14 +4,18 @@ import type { UserTimingDetail } from './user-timing-extensibility-api.type'; * Arguments for instant trace events. * @property {UserTimingDetail} [detail] - Optional user timing detail with DevTools payload */ -export type InstantEventArgs = { detail?: UserTimingDetail }; +export type InstantEventArgs = { + detail?: UserTimingDetail & Record; +}; /** * Arguments for span trace events (begin/end events). * @property {object} [data] - Optional data object * @property {UserTimingDetail} [data.detail] - Optional user timing detail with DevTools payload */ -export type SpanEventArgs = { data?: { detail?: UserTimingDetail } }; +export type SpanEventArgs = { + data?: { detail?: UserTimingDetail & Record }; +}; /** * Arguments for complete trace events. @@ -29,14 +33,14 @@ export type CompleteEventArgs = { detail?: Record }; export type StartTracingEventArgs = { data: { frameTreeNodeId: number; - frames: Array<{ + frames: { frame: string; isInPrimaryMainFrame: boolean; isOutermostMainFrame: boolean; name: string; processId: number; url: string; - }>; + }[]; persistentIds: boolean; }; }; @@ -68,6 +72,16 @@ export type BaseTraceEvent = { args: TraceArgs; }; +/** + * Start tracing event for Chrome DevTools tracing. + */ +export type StartTracingEvent = BaseTraceEvent & { + cat: 'devtools.timeline'; + ph: 'i'; + name: 'TracingStartedInBrowser'; + args: StartTracingEventArgs; +}; + /** * Complete trace event with duration. * Represents a complete operation with start time and duration. @@ -81,14 +95,12 @@ export type CompleteEvent = BaseTraceEvent & { ph: 'X'; dur: number }; * Used for user timing marks and other instantaneous events. * @property {'blink.user_timing'} cat - Fixed category for user timing events * @property {'i'} ph - Phase indicator for instant events - * @property {'t'} s - Scope indicator (thread) * @property {never} [dur] - Duration is not applicable for instant events * @property {InstantEventArgs} [args] - Optional event arguments */ export type InstantEvent = Omit & { cat: 'blink.user_timing'; ph: 'i'; - s: 't'; dur?: never; args: InstantEventArgs; }; @@ -106,9 +118,13 @@ type SpanCore = Omit & { /** * Begin event for a span (paired with an end event). * @property {'b'} ph - Phase indicator for begin events + * @property {'t'} s - Scope indicator (thread) * @property {never} [dur] - Duration is not applicable for begin events */ -export type BeginEvent = SpanCore & { ph: 'b'; dur?: never }; +export type BeginEvent = SpanCore & { + ph: 'b'; + dur?: never; +}; /** * End event for a span (paired with a begin event). @@ -125,17 +141,6 @@ export type SpanEvent = BeginEvent | EndEvent; /** * Union type of all trace event types. */ -/** - * Start tracing event for Chrome DevTools tracing. - */ -export type StartTracingEvent = BaseTraceEvent & { - cat: 'devtools.timeline'; - ph: 'i'; - s: 't'; - name: 'TracingStartedInBrowser'; - args: StartTracingEventArgs; -}; - export type TraceEvent = | InstantEvent | CompleteEvent From 03c8484f48c9cc76cca058170447da6e53e98882 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 13 Jan 2026 22:54:25 +0100 Subject: [PATCH 03/15] refactor: fix typing --- packages/utils/src/lib/trace-file-utils.ts | 6 +++--- packages/utils/src/lib/trace-file.type.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/utils/src/lib/trace-file-utils.ts b/packages/utils/src/lib/trace-file-utils.ts index 9c9bca3cc..f2c9fccc0 100644 --- a/packages/utils/src/lib/trace-file-utils.ts +++ b/packages/utils/src/lib/trace-file-utils.ts @@ -48,7 +48,7 @@ export const getInstantEvent = (opt: { ts?: number; pid?: number; tid?: number; - args?: InstantEventArgs; + args: InstantEventArgs; }): InstantEvent => ({ cat: 'blink.user_timing', ph: 'i', @@ -113,7 +113,7 @@ type SpanOpt = { ts?: number; pid?: number; tid?: number; - args?: SpanEventArgs; + args: SpanEventArgs; }; export function getSpanEvent(ph: 'b', opt: SpanOpt): BeginEvent; @@ -138,7 +138,7 @@ export const getSpan = (opt: { id2?: { local: string }; pid?: number; tid?: number; - args?: SpanEventArgs; + args: SpanEventArgs; tsMarkerPadding?: number; }): [BeginEvent, EndEvent] => { // tsMarkerPadding is here to make the measure slightly smaller so the markers align perfectly. diff --git a/packages/utils/src/lib/trace-file.type.ts b/packages/utils/src/lib/trace-file.type.ts index c081e926b..dee593ebd 100644 --- a/packages/utils/src/lib/trace-file.type.ts +++ b/packages/utils/src/lib/trace-file.type.ts @@ -5,8 +5,8 @@ import type { UserTimingDetail } from './user-timing-extensibility-api.type'; * @property {UserTimingDetail} [detail] - Optional user timing detail with DevTools payload */ export type InstantEventArgs = { - detail?: UserTimingDetail & Record; -}; + detail?: UserTimingDetail; +} & { [key: string]: unknown }; /** * Arguments for span trace events (begin/end events). @@ -14,8 +14,8 @@ export type InstantEventArgs = { * @property {UserTimingDetail} [data.detail] - Optional user timing detail with DevTools payload */ export type SpanEventArgs = { - data?: { detail?: UserTimingDetail & Record }; -}; + data?: { detail?: UserTimingDetail }; +} & { [key: string]: unknown }; /** * Arguments for complete trace events. From e4f326b326eae34eaca0d631b80f1e0bf4efad0d Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 13 Jan 2026 22:58:49 +0100 Subject: [PATCH 04/15] refactor: fix build --- packages/utils/src/lib/trace-file-utils.ts | 2 +- packages/utils/src/lib/trace-file.type.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/lib/trace-file-utils.ts b/packages/utils/src/lib/trace-file-utils.ts index f2c9fccc0..5650efb2f 100644 --- a/packages/utils/src/lib/trace-file-utils.ts +++ b/packages/utils/src/lib/trace-file-utils.ts @@ -36,7 +36,7 @@ export const nextId2 = () => ({ local: `0x${++id2Count}` }); const defaults = (opt?: { pid?: number; tid?: number; ts?: number }) => ({ pid: opt?.pid ?? process.pid, tid: opt?.tid ?? threadId, - ts: opt?.ts ?? defaultClock.fromPerfMs(performance.now()), + ts: opt?.ts ?? defaultClock.epochNowUs(), }); export const frameTreeNodeId = (pid: number, tid: number) => diff --git a/packages/utils/src/lib/trace-file.type.ts b/packages/utils/src/lib/trace-file.type.ts index dee593ebd..2d9a28a75 100644 --- a/packages/utils/src/lib/trace-file.type.ts +++ b/packages/utils/src/lib/trace-file.type.ts @@ -60,7 +60,7 @@ export type TraceArgs = * @property {string} name - Event name * @property {number} pid - Process ID * @property {number} tid - Thread ID - * @property {number} ts - Timestamp in microseconds + * @property {number} ts - Timestamp in epoch microseconds * @property {TraceArgs} [args] - Optional event arguments */ export type BaseTraceEvent = { From 313a4899b94096b01910b9077613275b1c40a6c4 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 13 Jan 2026 23:07:01 +0100 Subject: [PATCH 05/15] refactor: wip --- packages/utils/src/lib/trace-file-utils.ts | 14 ++-- .../src/lib/trace-file-utils.unit.test.ts | 64 +++++++++++++------ 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/packages/utils/src/lib/trace-file-utils.ts b/packages/utils/src/lib/trace-file-utils.ts index 5650efb2f..892cfce06 100644 --- a/packages/utils/src/lib/trace-file-utils.ts +++ b/packages/utils/src/lib/trace-file-utils.ts @@ -48,7 +48,7 @@ export const getInstantEvent = (opt: { ts?: number; pid?: number; tid?: number; - args: InstantEventArgs; + args?: InstantEventArgs; }): InstantEvent => ({ cat: 'blink.user_timing', ph: 'i', @@ -113,7 +113,7 @@ type SpanOpt = { ts?: number; pid?: number; tid?: number; - args: SpanEventArgs; + args?: SpanEventArgs; }; export function getSpanEvent(ph: 'b', opt: SpanOpt): BeginEvent; @@ -138,7 +138,7 @@ export const getSpan = (opt: { id2?: { local: string }; pid?: number; tid?: number; - args: SpanEventArgs; + args?: SpanEventArgs; tsMarkerPadding?: number; }): [BeginEvent, EndEvent] => { // tsMarkerPadding is here to make the measure slightly smaller so the markers align perfectly. @@ -154,15 +154,11 @@ export const getSpan = (opt: { ...opt, id2, ts: opt.tsB + pad, - name: opt.name, - args: opt.args, }), getSpanEvent('e', { ...opt, id2, ts: opt.tsE - pad, - name: opt.name, - args: opt.args, }), ]; }; @@ -175,7 +171,7 @@ export const markToInstantEvent = ( ...opt, name: opt?.name ?? entry.name, ts: defaultClock.fromEntryStartTimeMs(entry.startTime), - args: entry.detail ? { detail: entry.detail } : {}, + args: entry.detail ? { detail: entry.detail } : undefined, }); export const measureToSpanEvents = ( @@ -187,7 +183,7 @@ export const measureToSpanEvents = ( name: opt?.name ?? entry.name, tsB: entryToTraceTimestamp(entry), tsE: entryToTraceTimestamp(entry, true), - args: entry.detail ? { data: { detail: entry.detail } } : {}, + args: entry.detail ? { data: { detail: entry.detail } } : undefined, }); export const getTraceFile = (opt: { diff --git a/packages/utils/src/lib/trace-file-utils.unit.test.ts b/packages/utils/src/lib/trace-file-utils.unit.test.ts index e61ea9521..e8e08e451 100644 --- a/packages/utils/src/lib/trace-file-utils.unit.test.ts +++ b/packages/utils/src/lib/trace-file-utils.unit.test.ts @@ -66,29 +66,41 @@ describe('getTraceFile', () => { startTime: '2023-01-01T00:00:00.000Z', }); - expect(result.metadata?.startTime).toBe('2023-01-01T00:00:00.000Z'); + expect(result).toHaveProperty( + 'metadata', + expect.objectContaining({ + startTime: '2023-01-01T00:00:00.000Z', + }), + ); }); it('should include hardware concurrency', () => { - expect( - getTraceFile({ traceEvents: [] }).metadata?.hardwareConcurrency, - ).toBeGreaterThan(0); + expect(getTraceFile({ traceEvents: [] })).toHaveProperty( + 'metadata', + expect.objectContaining({ + hardwareConcurrency: expect.any(Number), + }), + ); }); }); describe('frameTreeNodeId', () => { - it('should generate correct frame tree node ID', () => { - expect(frameTreeNodeId(123, 456)).toBe(1_230_456); - expect(frameTreeNodeId(1, 2)).toBe(102); - expect(frameTreeNodeId(999, 999)).toBe(9_990_999); + it.each([ + [123, 456, 1_230_456], + [1, 2, 102], + [999, 999, 9_990_999], + ])('should generate correct frame tree node ID', (pid, tid, expected) => { + expect(frameTreeNodeId(pid, tid)).toBe(expected); }); }); describe('frameName', () => { - it('should generate correct frame name', () => { - expect(frameName(123, 456)).toBe('FRAME0P123T456'); - expect(frameName(1, 2)).toBe('FRAME0P1T2'); - expect(frameName(999, 999)).toBe('FRAME0P999T999'); + it.each([ + [123, 456], + [1, 2], + [999, 999], + ])('should generate correct frame name', (pid, tid) => { + expect(frameName(pid, tid)).toBe(`FRAME0P${pid}T${tid}`); }); }); @@ -338,13 +350,26 @@ describe('measureToSpanEvents', () => { }, ); - expect(result).toHaveLength(2); - expect(result[0].name).toBe('custom-measure'); - expect(result[0].pid).toBe(777); - expect(result[0].tid).toBe(666); - expect(result[0].args).toStrictEqual({ - data: { detail: { measurement: 'data' } }, - }); + expect(result).toStrictEqual([ + expect.objectContaining({ + name: 'custom-measure', + pid: 777, + tid: 666, + args: { data: { detail: { measurement: 'data' } } }, + }), + expect.objectContaining({ + name: 'custom-measure', + pid: 777, + tid: 666, + args: { data: { detail: { measurement: 'data' } } }, + }), + expect.objectContaining({ + name: 'custom-measure', + pid: 777, + tid: 666, + args: { data: { detail: { measurement: 'data' } } }, + }), + ]); }); }); @@ -430,6 +455,7 @@ describe('getSpan', () => { tsB: 1000, tsE: 1500, tsMarkerPadding: 5, + args: {}, }); expect(result).toStrictEqual([ From a3e839d41d4503916f8b56bc1cc6f12e92882374 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 13 Jan 2026 23:12:16 +0100 Subject: [PATCH 06/15] refactor: adjust clock helper --- packages/utils/src/lib/clock-epoch.ts | 7 +++++++ packages/utils/src/lib/trace-file-utils.ts | 23 +++++----------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/utils/src/lib/clock-epoch.ts b/packages/utils/src/lib/clock-epoch.ts index b4380e6af..c4be9da2c 100644 --- a/packages/utils/src/lib/clock-epoch.ts +++ b/packages/utils/src/lib/clock-epoch.ts @@ -41,6 +41,12 @@ export function epochClock(init: EpochClockOptions = {}) { msToUs(timeOriginMs + perfMs); const fromEntryStartTimeMs = fromPerfMs; + const fromEntry = (entry: PerformanceEntry, asEnd = false) => { + return defaultClock.fromPerfMs( + entry.startTime + + (entry.entryType === 'measure' && asEnd ? entry.duration : 0), + ); + }; const fromDateNowMs = fromEpochMs; return { @@ -55,6 +61,7 @@ export function epochClock(init: EpochClockOptions = {}) { fromEpochMs, fromEpochUs, fromPerfMs, + fromEntry, fromEntryStartTimeMs, fromDateNowMs, }; diff --git a/packages/utils/src/lib/trace-file-utils.ts b/packages/utils/src/lib/trace-file-utils.ts index 892cfce06..32ec72e80 100644 --- a/packages/utils/src/lib/trace-file-utils.ts +++ b/packages/utils/src/lib/trace-file-utils.ts @@ -1,10 +1,5 @@ import os from 'node:os'; -import { - type PerformanceEntry, - type PerformanceMark, - type PerformanceMeasure, - performance, -} from 'node:perf_hooks'; +import { type PerformanceMark, type PerformanceMeasure } from 'node:perf_hooks'; import { threadId } from 'node:worker_threads'; import { defaultClock } from './clock-epoch.js'; import type { @@ -20,15 +15,6 @@ import type { TraceEventContainer, } from './trace-file.type.js'; -export const entryToTraceTimestamp = ( - entry: PerformanceEntry, - asEnd = false, -): number => - defaultClock.fromPerfMs( - entry.startTime + - (entry.entryType === 'measure' && asEnd ? entry.duration : 0), - ); - // eslint-disable-next-line functional/no-let let id2Count = 0; export const nextId2 = () => ({ local: `0x${++id2Count}` }); @@ -147,6 +133,7 @@ export const getSpan = (opt: { // spans: ======== |======| // marks: | | const pad = opt.tsMarkerPadding ?? 1; + // b|e need to share the same id2 const id2 = opt.id2 ?? nextId2(); return [ @@ -170,7 +157,7 @@ export const markToInstantEvent = ( getInstantEvent({ ...opt, name: opt?.name ?? entry.name, - ts: defaultClock.fromEntryStartTimeMs(entry.startTime), + ts: defaultClock.fromEntry(entry), args: entry.detail ? { detail: entry.detail } : undefined, }); @@ -181,8 +168,8 @@ export const measureToSpanEvents = ( getSpan({ ...opt, name: opt?.name ?? entry.name, - tsB: entryToTraceTimestamp(entry), - tsE: entryToTraceTimestamp(entry, true), + tsB: defaultClock.fromEntry(entry), + tsE: defaultClock.fromEntry(entry, true), args: entry.detail ? { data: { detail: entry.detail } } : undefined, }); From a931a3a32b1dbc5901a5ce07b96989db314338f8 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 13 Jan 2026 23:13:22 +0100 Subject: [PATCH 07/15] refactor: wip --- packages/utils/src/lib/clock-epoch.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/lib/clock-epoch.ts b/packages/utils/src/lib/clock-epoch.ts index c4be9da2c..7768e8c7e 100644 --- a/packages/utils/src/lib/clock-epoch.ts +++ b/packages/utils/src/lib/clock-epoch.ts @@ -41,10 +41,10 @@ export function epochClock(init: EpochClockOptions = {}) { msToUs(timeOriginMs + perfMs); const fromEntryStartTimeMs = fromPerfMs; - const fromEntry = (entry: PerformanceEntry, asEnd = false) => { + const fromEntry = (entry: PerformanceEntry, useEndTime = false) => { return defaultClock.fromPerfMs( entry.startTime + - (entry.entryType === 'measure' && asEnd ? entry.duration : 0), + (entry.entryType === 'measure' && useEndTime ? entry.duration : 0), ); }; const fromDateNowMs = fromEpochMs; From dd79da4e3044ebb4c6556420d91b16a1728d0b57 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 13 Jan 2026 23:16:35 +0100 Subject: [PATCH 08/15] refactor: wip --- .../utils/src/lib/clock-epoch.unit.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/utils/src/lib/clock-epoch.unit.test.ts b/packages/utils/src/lib/clock-epoch.unit.test.ts index cb464d9eb..d02f6d482 100644 --- a/packages/utils/src/lib/clock-epoch.unit.test.ts +++ b/packages/utils/src/lib/clock-epoch.unit.test.ts @@ -10,6 +10,7 @@ describe('epochClock', () => { expect(c.fromEpochMs).toBeFunction(); expect(c.fromEpochUs).toBeFunction(); expect(c.fromPerfMs).toBeFunction(); + expect(c.fromEntry).toBeFunction(); expect(c.fromEntryStartTimeMs).toBeFunction(); expect(c.fromDateNowMs).toBeFunction(); }); @@ -72,6 +73,41 @@ describe('epochClock', () => { ]).toStrictEqual([c.fromPerfMs(0), c.fromPerfMs(1000)]); }); + it('should convert performance mark to microseconds', () => { + const markEntry = { + name: 'test-mark', + entryType: 'mark', + startTime: 1000, + duration: 0, + } as PerformanceMark; + + expect(defaultClock.fromEntry(markEntry)).toBe( + defaultClock.fromPerfMs(1000), + ); + expect(defaultClock.fromEntry(markEntry, true)).toBe( + defaultClock.fromPerfMs(1000), + ); // useEndTime doesn't matter for marks + }); + + it('should convert performance measure to microseconds', () => { + const measureEntry = { + name: 'test-measure', + entryType: 'measure', + startTime: 1000, + duration: 500, + } as PerformanceMeasure; + + expect(defaultClock.fromEntry(measureEntry)).toBe( + defaultClock.fromPerfMs(1000), + ); // useEndTime = false (default) + expect(defaultClock.fromEntry(measureEntry, false)).toBe( + defaultClock.fromPerfMs(1000), + ); // explicit false + expect(defaultClock.fromEntry(measureEntry, true)).toBe( + defaultClock.fromPerfMs(1500), + ); // useEndTime = true, adds duration + }); + it('should convert Date.now() milliseconds to microseconds', () => { const c = epochClock(); expect([ From 81bac903d46ad04391cc05b055fbc4da4ea6dc86 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 13 Jan 2026 23:22:27 +0100 Subject: [PATCH 09/15] refactor: wip --- .../utils/src/lib/clock-epoch.unit.test.ts | 10 ++--- .../src/lib/trace-file-utils.unit.test.ts | 43 ------------------- 2 files changed, 5 insertions(+), 48 deletions(-) diff --git a/packages/utils/src/lib/clock-epoch.unit.test.ts b/packages/utils/src/lib/clock-epoch.unit.test.ts index d02f6d482..781f633ae 100644 --- a/packages/utils/src/lib/clock-epoch.unit.test.ts +++ b/packages/utils/src/lib/clock-epoch.unit.test.ts @@ -34,7 +34,7 @@ describe('epochClock', () => { it('should support performance clock by default for epochNowUs', () => { const c = epochClock(); expect(c.timeOriginMs).toBe(500_000); - expect(c.epochNowUs()).toBe(500_000_000); // timeOrigin + performance.now() = timeOrigin + 0 + expect(c.epochNowUs()).toBe(500_000_000); }); it.each([ @@ -86,7 +86,7 @@ describe('epochClock', () => { ); expect(defaultClock.fromEntry(markEntry, true)).toBe( defaultClock.fromPerfMs(1000), - ); // useEndTime doesn't matter for marks + ); }); it('should convert performance measure to microseconds', () => { @@ -99,13 +99,13 @@ describe('epochClock', () => { expect(defaultClock.fromEntry(measureEntry)).toBe( defaultClock.fromPerfMs(1000), - ); // useEndTime = false (default) + ); expect(defaultClock.fromEntry(measureEntry, false)).toBe( defaultClock.fromPerfMs(1000), - ); // explicit false + ); expect(defaultClock.fromEntry(measureEntry, true)).toBe( defaultClock.fromPerfMs(1500), - ); // useEndTime = true, adds duration + ); }); it('should convert Date.now() milliseconds to microseconds', () => { diff --git a/packages/utils/src/lib/trace-file-utils.unit.test.ts b/packages/utils/src/lib/trace-file-utils.unit.test.ts index e8e08e451..f0287b184 100644 --- a/packages/utils/src/lib/trace-file-utils.unit.test.ts +++ b/packages/utils/src/lib/trace-file-utils.unit.test.ts @@ -1,7 +1,6 @@ import type { PerformanceMark, PerformanceMeasure } from 'node:perf_hooks'; import { describe, expect, it } from 'vitest'; import { - entryToTraceTimestamp, frameName, frameTreeNodeId, getCompleteEvent, @@ -363,52 +362,10 @@ describe('measureToSpanEvents', () => { tid: 666, args: { data: { detail: { measurement: 'data' } } }, }), - expect.objectContaining({ - name: 'custom-measure', - pid: 777, - tid: 666, - args: { data: { detail: { measurement: 'data' } } }, - }), ]); }); }); -describe('entryToTraceTimestamp', () => { - it('should convert entry timestamp for start time', () => { - expect( - typeof entryToTraceTimestamp({ - startTime: 1000, - duration: 500, - entryType: 'measure', - } as PerformanceMeasure), - ).toBe('number'); - }); - - it('should convert entry timestamp for end time', () => { - const mockEntry = { - startTime: 1000, - duration: 500, - entryType: 'measure', - } as PerformanceMeasure; - - expect(entryToTraceTimestamp(mockEntry, true)).toBeGreaterThan( - entryToTraceTimestamp(mockEntry, false), - ); - }); - - it('should handle non-measure entries', () => { - expect( - typeof entryToTraceTimestamp( - { - startTime: 1000, - entryType: 'mark', - } as PerformanceMark, - true, - ), - ).toBe('number'); - }); -}); - describe('getSpanEvent', () => { it('should create begin event with args detail', () => { expect( From af7647f1935922b407305ecbef80ba105efcbe34 Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 13 Jan 2026 23:26:30 +0100 Subject: [PATCH 10/15] refactor: wip --- packages/utils/src/lib/clock-epoch.ts | 6 +++--- packages/utils/src/lib/trace-file-utils.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/utils/src/lib/clock-epoch.ts b/packages/utils/src/lib/clock-epoch.ts index 7768e8c7e..26a9db98d 100644 --- a/packages/utils/src/lib/clock-epoch.ts +++ b/packages/utils/src/lib/clock-epoch.ts @@ -1,3 +1,4 @@ +import type { PerformanceEntry } from 'node:perf_hooks'; import process from 'node:process'; import { threadId } from 'node:worker_threads'; @@ -41,12 +42,11 @@ export function epochClock(init: EpochClockOptions = {}) { msToUs(timeOriginMs + perfMs); const fromEntryStartTimeMs = fromPerfMs; - const fromEntry = (entry: PerformanceEntry, useEndTime = false) => { - return defaultClock.fromPerfMs( + const fromEntry = (entry: PerformanceEntry, useEndTime = false) => + defaultClock.fromPerfMs( entry.startTime + (entry.entryType === 'measure' && useEndTime ? entry.duration : 0), ); - }; const fromDateNowMs = fromEpochMs; return { diff --git a/packages/utils/src/lib/trace-file-utils.ts b/packages/utils/src/lib/trace-file-utils.ts index 32ec72e80..d218174db 100644 --- a/packages/utils/src/lib/trace-file-utils.ts +++ b/packages/utils/src/lib/trace-file-utils.ts @@ -1,5 +1,5 @@ import os from 'node:os'; -import { type PerformanceMark, type PerformanceMeasure } from 'node:perf_hooks'; +import type { PerformanceMark, PerformanceMeasure } from 'node:perf_hooks'; import { threadId } from 'node:worker_threads'; import { defaultClock } from './clock-epoch.js'; import type { From 5ec026d9fe0a55f81d4fd10267023d942a0a0d92 Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 14 Jan 2026 05:36:41 +0100 Subject: [PATCH 11/15] refactor: add js-docs --- packages/utils/src/lib/trace-file-utils.ts | 82 +++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/packages/utils/src/lib/trace-file-utils.ts b/packages/utils/src/lib/trace-file-utils.ts index d218174db..afe452450 100644 --- a/packages/utils/src/lib/trace-file-utils.ts +++ b/packages/utils/src/lib/trace-file-utils.ts @@ -15,20 +15,48 @@ import type { TraceEventContainer, } from './trace-file.type.js'; -// eslint-disable-next-line functional/no-let +/** Global counter for generating unique local IDs */ let id2Count = 0; + +/** + * Generates a unique local ID for span events, to link start and end with a id. + * @returns Object with local ID string + */ export const nextId2 = () => ({ local: `0x${++id2Count}` }); +/** + * Provides default values for trace event properties. + * @param opt - Optional overrides for pid, tid, and timestamp + * @returns Object with pid, tid, and timestamp + */ const defaults = (opt?: { pid?: number; tid?: number; ts?: number }) => ({ pid: opt?.pid ?? process.pid, tid: opt?.tid ?? threadId, ts: opt?.ts ?? defaultClock.epochNowUs(), }); +/** + * Generates a unique frame tree node ID from process and thread IDs. + * @param pid - Process ID + * @param tid - Thread ID + * @returns Combined numeric ID + */ export const frameTreeNodeId = (pid: number, tid: number) => Number.parseInt(`${pid}0${tid}`, 10); + +/** + * Generates a frame name string from process and thread IDs. + * @param pid - Process ID + * @param tid - Thread ID + * @returns Formatted frame name + */ export const frameName = (pid: number, tid: number) => `FRAME0P${pid}T${tid}`; +/** + * Creates an instant trace event for marking a point in time. + * @param opt - Event configuration options + * @returns InstantEvent object + */ export const getInstantEvent = (opt: { name: string; ts?: number; @@ -43,6 +71,12 @@ export const getInstantEvent = (opt: { args: opt.args ?? {}, }); +/** + * Creates a start tracing event with frame information. + * This event is needed to make events in general show up and also colores the track better. + * @param opt - Tracing configuration options + * @returns StartTracingEvent object + */ export const getStartTracing = (opt: { url: string; ts?: number; @@ -78,6 +112,11 @@ export const getStartTracing = (opt: { }; }; +/** + * Creates a complete trace event with duration. + * @param opt - Event configuration with name and duration + * @returns CompleteEvent object + */ export const getCompleteEvent = (opt: { name: string; dur: number; @@ -93,6 +132,7 @@ export const getCompleteEvent = (opt: { args: {}, }); +/** Options for creating span events */ type SpanOpt = { name: string; id2: { local: string }; @@ -102,8 +142,26 @@ type SpanOpt = { args?: SpanEventArgs; }; +/** + * Creates a begin span event. + * @param ph - Phase ('b' for begin) + * @param opt - Span event options + * @returns BeginEvent object + */ export function getSpanEvent(ph: 'b', opt: SpanOpt): BeginEvent; +/** + * Creates an end span event. + * @param ph - Phase ('e' for end) + * @param opt - Span event options + * @returns EndEvent object + */ export function getSpanEvent(ph: 'e', opt: SpanOpt): EndEvent; +/** + * Creates a span event (begin or end). + * @param ph - Phase ('b' or 'e') + * @param opt - Span event options + * @returns SpanEvent object + */ export function getSpanEvent(ph: 'b' | 'e', opt: SpanOpt): SpanEvent { return { cat: 'blink.user_timing', @@ -117,6 +175,11 @@ export function getSpanEvent(ph: 'b' | 'e', opt: SpanOpt): SpanEvent { }; } +/** + * Creates a pair of begin and end span events. + * @param opt - Span configuration with start/end timestamps + * @returns Tuple of BeginEvent and EndEvent + */ export const getSpan = (opt: { name: string; tsB: number; @@ -150,6 +213,12 @@ export const getSpan = (opt: { ]; }; +/** + * Converts a PerformanceMark to an instant trace event. + * @param entry - Performance mark entry + * @param opt - Optional overrides for name, pid, and tid + * @returns InstantEvent object + */ export const markToInstantEvent = ( entry: PerformanceMark, opt?: { name?: string; pid?: number; tid?: number }, @@ -161,6 +230,12 @@ export const markToInstantEvent = ( args: entry.detail ? { detail: entry.detail } : undefined, }); +/** + * Converts a PerformanceMeasure to a pair of span events. + * @param entry - Performance measure entry + * @param opt - Optional overrides for name, pid, and tid + * @returns Tuple of BeginEvent and EndEvent + */ export const measureToSpanEvents = ( entry: PerformanceMeasure, opt?: { name?: string; pid?: number; tid?: number }, @@ -173,6 +248,11 @@ export const measureToSpanEvents = ( args: entry.detail ? { data: { detail: entry.detail } } : undefined, }); +/** + * Creates a complete trace file container with metadata. + * @param opt - Trace file configuration + * @returns TraceEventContainer with events and metadata + */ export const getTraceFile = (opt: { traceEvents: TraceEvent[]; startTime?: string; From 878b939347b7cc2b4008162e8fd01de3654261d3 Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 14 Jan 2026 05:51:34 +0100 Subject: [PATCH 12/15] refactor: fix lint --- packages/utils/src/lib/trace-file-utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/utils/src/lib/trace-file-utils.ts b/packages/utils/src/lib/trace-file-utils.ts index afe452450..f3d094457 100644 --- a/packages/utils/src/lib/trace-file-utils.ts +++ b/packages/utils/src/lib/trace-file-utils.ts @@ -16,6 +16,7 @@ import type { } from './trace-file.type.js'; /** Global counter for generating unique local IDs */ +// eslint-disable-next-line functional/no-let let id2Count = 0; /** From ec67bc012eed25174b569506915a35a804612c05 Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 14 Jan 2026 18:29:38 +0100 Subject: [PATCH 13/15] refactor: impl feedback --- packages/utils/src/lib/clock-epoch.ts | 11 +++++++++-- packages/utils/src/lib/trace-file-utils.ts | 14 +++++++------- .../utils/src/lib/trace-file-utils.unit.test.ts | 10 ++++++---- packages/utils/src/lib/trace-file.type.ts | 13 +++++++------ 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/utils/src/lib/clock-epoch.ts b/packages/utils/src/lib/clock-epoch.ts index 26a9db98d..2c8938911 100644 --- a/packages/utils/src/lib/clock-epoch.ts +++ b/packages/utils/src/lib/clock-epoch.ts @@ -42,8 +42,15 @@ export function epochClock(init: EpochClockOptions = {}) { msToUs(timeOriginMs + perfMs); const fromEntryStartTimeMs = fromPerfMs; - const fromEntry = (entry: PerformanceEntry, useEndTime = false) => - defaultClock.fromPerfMs( + const fromEntry = ( + entry: { + startTime: Milliseconds; + entryType: string; + duration: Milliseconds; + }, + useEndTime = false, + ) => + fromPerfMs( entry.startTime + (entry.entryType === 'measure' && useEndTime ? entry.duration : 0), ); diff --git a/packages/utils/src/lib/trace-file-utils.ts b/packages/utils/src/lib/trace-file-utils.ts index f3d094457..2a2f3eb30 100644 --- a/packages/utils/src/lib/trace-file-utils.ts +++ b/packages/utils/src/lib/trace-file-utils.ts @@ -8,20 +8,20 @@ import type { EndEvent, InstantEvent, InstantEventArgs, + InstantEventTracingStartedInBrowser, SpanEvent, SpanEventArgs, - StartTracingEvent, TraceEvent, TraceEventContainer, } from './trace-file.type.js'; -/** Global counter for generating unique local IDs */ +/** Global counter for generating unique span IDs within a trace */ // eslint-disable-next-line functional/no-let let id2Count = 0; /** - * Generates a unique local ID for span events, to link start and end with a id. - * @returns Object with local ID string + * Generates a unique ID for linking begin and end span events in Chrome traces. + * @returns Object with local ID string for the id2 field */ export const nextId2 = () => ({ local: `0x${++id2Count}` }); @@ -74,16 +74,16 @@ export const getInstantEvent = (opt: { /** * Creates a start tracing event with frame information. - * This event is needed to make events in general show up and also colores the track better. + * This event is needed at the beginning of the traceEvents array to make tell the UI profiling has started, and it should visualize the data. * @param opt - Tracing configuration options * @returns StartTracingEvent object */ -export const getStartTracing = (opt: { +export const getInstantEventTracingStartedInBrowser = (opt: { url: string; ts?: number; pid?: number; tid?: number; -}): StartTracingEvent => { +}): InstantEventTracingStartedInBrowser => { const { pid, tid, ts } = defaults(opt); const id = frameTreeNodeId(pid, tid); diff --git a/packages/utils/src/lib/trace-file-utils.unit.test.ts b/packages/utils/src/lib/trace-file-utils.unit.test.ts index f0287b184..e8cbf319a 100644 --- a/packages/utils/src/lib/trace-file-utils.unit.test.ts +++ b/packages/utils/src/lib/trace-file-utils.unit.test.ts @@ -5,9 +5,9 @@ import { frameTreeNodeId, getCompleteEvent, getInstantEvent, + getInstantEventTracingStartedInBrowser, getSpan, getSpanEvent, - getStartTracing, getTraceFile, markToInstantEvent, measureToSpanEvents, @@ -103,9 +103,11 @@ describe('frameName', () => { }); }); -describe('getStartTracing', () => { +describe('getInstantEventTracingStartedInBrowser', () => { it('should create start tracing event with required url', () => { - expect(getStartTracing({ url: 'https://example.com' })).toStrictEqual({ + expect( + getInstantEventTracingStartedInBrowser({ url: 'https://example.com' }), + ).toStrictEqual({ cat: 'devtools.timeline', ph: 'i', name: 'TracingStartedInBrowser', @@ -133,7 +135,7 @@ describe('getStartTracing', () => { it('should use custom pid and tid', () => { expect( - getStartTracing({ + getInstantEventTracingStartedInBrowser({ url: 'https://test.com', pid: 777, tid: 888, diff --git a/packages/utils/src/lib/trace-file.type.ts b/packages/utils/src/lib/trace-file.type.ts index 2d9a28a75..f5c585ac7 100644 --- a/packages/utils/src/lib/trace-file.type.ts +++ b/packages/utils/src/lib/trace-file.type.ts @@ -1,3 +1,4 @@ +import { getInstantEventTracingStartedInBrowser } from './trace-file-utils'; import type { UserTimingDetail } from './user-timing-extensibility-api.type'; /** @@ -30,7 +31,7 @@ export type CompleteEventArgs = { detail?: Record }; * @property {Array} data.frames - Array of frame information * @property {boolean} data.persistentIds - Whether IDs are persistent */ -export type StartTracingEventArgs = { +export type InstantEventTracingStartedInBrowserArgs = { data: { frameTreeNodeId: number; frames: { @@ -52,7 +53,7 @@ export type TraceArgs = | InstantEventArgs | SpanEventArgs | CompleteEventArgs - | StartTracingEventArgs; + | InstantEventTracingStartedInBrowserArgs; /** * Base properties shared by all trace events. @@ -75,11 +76,11 @@ export type BaseTraceEvent = { /** * Start tracing event for Chrome DevTools tracing. */ -export type StartTracingEvent = BaseTraceEvent & { +export type InstantEventTracingStartedInBrowser = BaseTraceEvent & { cat: 'devtools.timeline'; ph: 'i'; name: 'TracingStartedInBrowser'; - args: StartTracingEventArgs; + args: InstantEventTracingStartedInBrowserArgs; }; /** @@ -118,7 +119,7 @@ type SpanCore = Omit & { /** * Begin event for a span (paired with an end event). * @property {'b'} ph - Phase indicator for begin events - * @property {'t'} s - Scope indicator (thread) + * @property {'t'} s - Scope indicator ('t' is thread) * @property {never} [dur] - Duration is not applicable for begin events */ export type BeginEvent = SpanCore & { @@ -145,7 +146,7 @@ export type TraceEvent = | InstantEvent | CompleteEvent | SpanEvent - | StartTracingEvent; + | InstantEventTracingStartedInBrowser; /** * Raw arguments format for trace events before processing. From 2fb201424b33b28cd5b8c11045cf698176ea0b51 Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 14 Jan 2026 18:34:42 +0100 Subject: [PATCH 14/15] refactor: fix lint --- packages/utils/src/lib/clock-epoch.ts | 1 - packages/utils/src/lib/trace-file.type.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/utils/src/lib/clock-epoch.ts b/packages/utils/src/lib/clock-epoch.ts index 2c8938911..19afbe065 100644 --- a/packages/utils/src/lib/clock-epoch.ts +++ b/packages/utils/src/lib/clock-epoch.ts @@ -1,4 +1,3 @@ -import type { PerformanceEntry } from 'node:perf_hooks'; import process from 'node:process'; import { threadId } from 'node:worker_threads'; diff --git a/packages/utils/src/lib/trace-file.type.ts b/packages/utils/src/lib/trace-file.type.ts index f5c585ac7..e1080dbd4 100644 --- a/packages/utils/src/lib/trace-file.type.ts +++ b/packages/utils/src/lib/trace-file.type.ts @@ -1,5 +1,4 @@ -import { getInstantEventTracingStartedInBrowser } from './trace-file-utils'; -import type { UserTimingDetail } from './user-timing-extensibility-api.type'; +import type { UserTimingDetail } from './user-timing-extensibility-api.type.js'; /** * Arguments for instant trace events. From cd33b5cbc57aa823dde01cedb2d932079e288e9c Mon Sep 17 00:00:00 2001 From: John Doe Date: Wed, 14 Jan 2026 19:05:02 +0100 Subject: [PATCH 15/15] refactor: fix docs --- packages/utils/src/lib/trace-file.type.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/utils/src/lib/trace-file.type.ts b/packages/utils/src/lib/trace-file.type.ts index e1080dbd4..e59a0d7c8 100644 --- a/packages/utils/src/lib/trace-file.type.ts +++ b/packages/utils/src/lib/trace-file.type.ts @@ -118,7 +118,6 @@ type SpanCore = Omit & { /** * Begin event for a span (paired with an end event). * @property {'b'} ph - Phase indicator for begin events - * @property {'t'} s - Scope indicator ('t' is thread) * @property {never} [dur] - Duration is not applicable for begin events */ export type BeginEvent = SpanCore & {