diff --git a/src/drivers/devtools.ts b/src/drivers/devtools.ts index 64cfebe..7fad793 100644 --- a/src/drivers/devtools.ts +++ b/src/drivers/devtools.ts @@ -43,6 +43,28 @@ interface ParsedTrace { traceEvents: TraceEvent[]; } +function memoBuilder(fn: (trace: ParsedTrace) => T): (trace: ParsedTrace) => T { + const cache = new WeakMap(); + return (trace: ParsedTrace): T => { + let result = cache.get(trace); + if (result !== undefined) return result; + result = fn(trace); + cache.set(trace, result); + return result; + }; +} + +function memoObjectBuilder(fn: (key: K) => T): (key: K) => T { + const cache = new WeakMap(); + return (key: K): T => { + let result = cache.get(key); + if (result !== undefined) return result; + result = fn(key); + cache.set(key, result); + return result; + }; +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } @@ -103,7 +125,7 @@ function getThreadKey(pid: number, tid: number) { return `${pid}:${tid}`; } -function buildThreadMetadata(events: TraceEvent[]) { +const buildThreadMetadata = memoObjectBuilder((events: TraceEvent[]) => { const threadNames = new Map(); const processNames = new Map(); for (const event of events) { @@ -119,7 +141,8 @@ function buildThreadMetadata(events: TraceEvent[]) { if (event.name === "process_name") processNames.set(event.pid, name); } return { threadNames, processNames }; -} + +}); function getNestedString(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; @@ -131,7 +154,7 @@ function canonicalId(value: unknown): string | undefined { return undefined; } -function buildFacts(trace: ParsedTrace) { +const buildFacts = memoBuilder((trace: ParsedTrace) => { return trace.traceEvents.map((event, index) => { const data = isRecord(event.args?.data) ? (event.args!.data as Record) : undefined; const endData = isRecord(event.args?.endData) @@ -190,9 +213,11 @@ function buildFacts(trace: ParsedTrace) { provenance: { rawIds: [`evt:${index}`], layer: "devtools/facts.events" }, }; }); -} -function buildIndexes(events: TraceEvent[]) { +}); + +const buildIndexes = memoBuilder((trace: ParsedTrace) => { + const events = trace.traceEvents; const byName = new Map(); const byCategory = new Map(); const byPhase = new Map(); @@ -211,9 +236,10 @@ function buildIndexes(events: TraceEvent[]) { byThread.get(threadKey)!.push(event); } return { byName, byCategory, byPhase, byThread }; -} -function buildRequests(trace: ParsedTrace) { +}); + +const buildRequests = memoBuilder((trace: ParsedTrace) => { const requests = new Map(); const facts = buildFacts(trace); for (const fact of facts) { @@ -274,7 +300,8 @@ function buildRequests(trace: ParsedTrace) { provenance: { rawIds: row.rawEventIds, layer: "devtools/dims.requests" }, })) .sort((a, b) => a.startTimeUs - b.startTimeUs || a.requestId.localeCompare(b.requestId)); -} + +}); function getScreenshotEvents(trace: ParsedTrace) { return trace.traceEvents @@ -283,7 +310,7 @@ function getScreenshotEvents(trace: ParsedTrace) { .sort((left, right) => left.event.ts - right.event.ts); } -function buildScreenshots(trace: ParsedTrace) { +const buildScreenshots = memoBuilder((trace: ParsedTrace) => { const { minTs } = getTraceBounds(trace.traceEvents); return getScreenshotEvents(trace).map(({ event, index: rawIndex }, screenshotIndex) => { const base64 = String(event.args?.snapshot ?? ""); @@ -315,7 +342,8 @@ function buildScreenshots(trace: ParsedTrace) { }, }; }); -} + +}); function chooseInteractionType(types: string[]) { const preference = ["click", "keydown", "pointerup", "mouseup", "pointerdown", "mousedown"]; @@ -325,7 +353,7 @@ function chooseInteractionType(types: string[]) { return types[0] ?? "unknown"; } -function buildInteractions(trace: ParsedTrace) { +const buildInteractions = memoBuilder((trace: ParsedTrace) => { const eventTimingRows = trace.traceEvents .map((event, index) => ({ event, index })) .filter(({ event }) => event.name === "EventTiming" && isRecord(event.args?.data)) @@ -383,7 +411,8 @@ function buildInteractions(trace: ParsedTrace) { }; }) .sort((a, b) => b.durationMs - a.durationMs || a.startTsUs - b.startTsUs); -} + +}); function parseDetail(detail: unknown): Record { if (typeof detail !== "string") return {}; @@ -395,7 +424,7 @@ function parseDetail(detail: unknown): Record { } } -function buildRenderMeasures(trace: ParsedTrace) { +const buildRenderMeasures = memoBuilder((trace: ParsedTrace) => { const begins = new Map(); trace.traceEvents.forEach((event) => { if (splitCategories(event.cat).includes("blink.user_timing") && event.ph === "b") { @@ -435,9 +464,10 @@ function buildRenderMeasures(trace: ParsedTrace) { }; }) .filter((row) => row.componentName); -} -function buildScripts(trace: ParsedTrace) { +}); + +const buildScripts = memoBuilder((trace: ParsedTrace) => { const scripts = new Map(); const sourceMaps = Array.isArray(trace.metadata.sourceMaps) ? (trace.metadata.sourceMaps as SourceMapEntry[]) @@ -491,9 +521,10 @@ function buildScripts(trace: ParsedTrace) { }, })) .sort((a, b) => Number(a.scriptId) - Number(b.scriptId)); -} -function buildSourceMaps(trace: ParsedTrace) { +}); + +const buildSourceMaps = memoBuilder((trace: ParsedTrace) => { const sourceMaps = Array.isArray(trace.metadata.sourceMaps) ? (trace.metadata.sourceMaps as SourceMapEntry[]) : []; @@ -510,9 +541,10 @@ function buildSourceMaps(trace: ParsedTrace) { artifactIds: [`artifact:code:sourcemap:${index}`], }, })); -} -function buildSources(trace: ParsedTrace) { +}); + +const buildSources = memoBuilder((trace: ParsedTrace) => { const sourceMaps = Array.isArray(trace.metadata.sourceMaps) ? (trace.metadata.sourceMaps as SourceMapEntry[]) : []; @@ -542,9 +574,10 @@ function buildSources(trace: ParsedTrace) { }); }); return rows; -} -function buildThreadRows(trace: ParsedTrace) { +}); + +const buildThreadRows = memoBuilder((trace: ParsedTrace) => { const { threadNames, processNames } = buildThreadMetadata(trace.traceEvents); const counts = new Map(); for (const event of trace.traceEvents) { @@ -567,9 +600,10 @@ function buildThreadRows(trace: ParsedTrace) { }; }) .sort((a, b) => b.eventCount - a.eventCount || a.threadKey.localeCompare(b.threadKey)); -} -function buildProcessRows(trace: ParsedTrace) { +}); + +const buildProcessRows = memoBuilder((trace: ParsedTrace) => { const { processNames } = buildThreadMetadata(trace.traceEvents); const rows = new Map; rawIds: string[] }>(); trace.traceEvents.forEach((event, index) => { @@ -591,9 +625,10 @@ function buildProcessRows(trace: ParsedTrace) { provenance: { rawIds: row.rawIds, layer: "devtools/dims.processes" }, })) .sort((a, b) => b.eventCount - a.eventCount || a.pid - b.pid); -} -function buildFrameRows(trace: ParsedTrace) { +}); + +const buildFrameRows = memoBuilder((trace: ParsedTrace) => { const facts = buildFacts(trace).filter((fact) => fact.frameId); const groups = new Map(); for (const fact of facts) { @@ -623,9 +658,10 @@ function buildFrameRows(trace: ParsedTrace) { .sort( (a, b) => b.eventCount - a.eventCount || String(a.frameId).localeCompare(String(b.frameId)), ); -} -function buildWorkerRows(trace: ParsedTrace) { +}); + +const buildWorkerRows = memoBuilder((trace: ParsedTrace) => { const facts = buildFacts(trace); const threadRows = buildThreadRows(trace); const groups = new Map(); @@ -668,9 +704,10 @@ function buildWorkerRows(trace: ParsedTrace) { provenance: { rawIds: row.rawEventIds, layer: "devtools/dims.workers" }, })) .sort((a, b) => a.workerId.localeCompare(b.workerId)); -} -function buildLayerRows(trace: ParsedTrace) { +}); + +const buildLayerRows = memoBuilder((trace: ParsedTrace) => { const facts = buildFacts(trace).filter((fact) => fact.layerId); const groups = new Map(); for (const fact of facts) { @@ -694,27 +731,30 @@ function buildLayerRows(trace: ParsedTrace) { provenance: { rawIds: row.rawEventIds, layer: "devtools/dims.layers" }, })) .sort((a, b) => b.eventCount - a.eventCount || a.layerId.localeCompare(b.layerId)); -} -function buildInstantFacts(trace: ParsedTrace) { +}); + +const buildInstantFacts = memoBuilder((trace: ParsedTrace) => { return buildFacts(trace) .filter((fact) => ["I", "i", "M", "n"].includes(fact.phase)) .map((fact) => ({ ...fact, provenance: { rawIds: [fact.eventId], layer: "devtools/facts.instantEvents" }, })); -} -function buildSliceFacts(trace: ParsedTrace) { +}); + +const buildSliceFacts = memoBuilder((trace: ParsedTrace) => { return buildFacts(trace) .filter((fact) => fact.phase === "X") .map((fact) => ({ ...fact, provenance: { rawIds: [fact.eventId], layer: "devtools/facts.sliceEvents" }, })); -} -function buildAsyncFlowFacts(trace: ParsedTrace) { +}); + +const buildAsyncFlowFacts = memoBuilder((trace: ParsedTrace) => { return buildFacts(trace) .filter( (fact) => @@ -724,9 +764,10 @@ function buildAsyncFlowFacts(trace: ParsedTrace) { ...fact, provenance: { rawIds: [fact.eventId], layer: "devtools/facts.asyncFlows" }, })); -} -function buildObjectLifecycles(trace: ParsedTrace) { +}); + +const buildObjectLifecycles = memoBuilder((trace: ParsedTrace) => { const groups = new Map(); buildFacts(trace) .filter((fact) => fact.id || fact.flowScope) @@ -757,9 +798,10 @@ function buildObjectLifecycles(trace: ParsedTrace) { provenance: { rawIds: row.rawEventIds, layer: "devtools/facts.objectLifecycles" }, })) .sort((a, b) => b.eventCount - a.eventCount || a.firstTsUs - b.firstTsUs); -} -function buildSecondaryIndexes(trace: ParsedTrace) { +}); + +const buildSecondaryIndexes = memoBuilder((trace: ParsedTrace) => { const facts = buildFacts(trace); const build = (keyOf: (fact: ReturnType[number]) => string | undefined) => { const map = new Map(); @@ -779,9 +821,10 @@ function buildSecondaryIndexes(trace: ParsedTrace) { byNodeId: build((fact) => fact.nodeId), byUrl: build((fact) => fact.url), }; -} -function buildLayoutShifts(trace: ParsedTrace) { +}); + +const buildLayoutShifts = memoBuilder((trace: ParsedTrace) => { return trace.traceEvents .map((event, index) => ({ event, index })) .filter(({ event }) => event.name === "LayoutShift" && isRecord(event.args?.data)) @@ -815,9 +858,10 @@ function buildLayoutShifts(trace: ParsedTrace) { provenance: { rawIds: [`evt:${index}`], layer: "devtools/dims.layoutShifts" }, }; }); -} -function buildSoftNavigations(trace: ParsedTrace) { +}); + +const buildSoftNavigations = memoBuilder((trace: ParsedTrace) => { const groups = new Map< string, { @@ -873,9 +917,10 @@ function buildSoftNavigations(trace: ParsedTrace) { }; }) .sort((a, b) => a.startTsUs - b.startTsUs); -} -function buildFramePipeline(trace: ParsedTrace) { +}); + +const buildFramePipeline = memoBuilder((trace: ParsedTrace) => { const screenshotsByFrameSeq = new Map( buildScreenshots(trace) .filter((row) => row.frameSeqId) @@ -911,9 +956,10 @@ function buildFramePipeline(trace: ParsedTrace) { provenance: { rawIds: [`evt:${index}`], layer: "devtools/views.framePipeline" }, }; }); -} -function buildMainThreadTasks(trace: ParsedTrace) { +}); + +const buildMainThreadTasks = memoBuilder((trace: ParsedTrace) => { const { threadNames } = buildThreadMetadata(trace.traceEvents); const rendererMainThreads = new Set( [...threadNames.entries()] @@ -994,9 +1040,10 @@ function buildMainThreadTasks(trace: ParsedTrace) { } return rows.sort((a, b) => b.durationMs - a.durationMs || a.tsUs - b.tsUs); -} -function buildCodeHotspots(trace: ParsedTrace) { +}); + +const buildCodeHotspots = memoBuilder((trace: ParsedTrace) => { const scripts = new Map(buildScripts(trace).map((row) => [row.scriptId, row] as const)); const groups = new Map(); trace.traceEvents.forEach((event, index) => { @@ -1034,9 +1081,10 @@ function buildCodeHotspots(trace: ParsedTrace) { provenance: { rawIds: row.rawEventIds, layer: "devtools/views.codeHotspots" }, })) .sort((a, b) => b.totalDurationMs - a.totalDurationMs || b.count - a.count); -} -function buildCpuProfileModel(trace: ParsedTrace) { +}); + +const buildCpuProfileModel = memoBuilder((trace: ParsedTrace) => { const scripts = new Map(buildScripts(trace).map((row) => [row.scriptId, row] as const)); const nodes = new Map< number, @@ -1238,28 +1286,33 @@ function buildCpuProfileModel(trace: ParsedTrace) { provenance: { rawIds: bucket.rawEventIds, layer: "devtools/views.cpuTimeline" }, })), }; -} -function buildCpuSampleFacts(trace: ParsedTrace) { +}); + +const buildCpuSampleFacts = memoBuilder((trace: ParsedTrace) => { return buildCpuProfileModel(trace).samples; -} -function buildCpuNodeRows(trace: ParsedTrace) { +}); + +const buildCpuNodeRows = memoBuilder((trace: ParsedTrace) => { return buildCpuProfileModel(trace).cpuNodes; -} -function buildCpuCallTrees(trace: ParsedTrace) { +}); + +const buildCpuCallTrees = memoBuilder((trace: ParsedTrace) => { return buildCpuProfileModel(trace).foldedStacks.map((row: any) => ({ ...row, provenance: { rawIds: row.rawEventIds, layer: "devtools/views.cpuCallTrees" }, })); -} -function buildCpuTimeline(trace: ParsedTrace) { +}); + +const buildCpuTimeline = memoBuilder((trace: ParsedTrace) => { return buildCpuProfileModel(trace).cpuTimeline; -} -function buildCpuHotspots(trace: ParsedTrace) { +}); + +const buildCpuHotspots = memoBuilder((trace: ParsedTrace) => { return buildCpuNodeRows(trace) .filter((row: any) => row.selfSampleCount > 0) .map((row: any) => ({ @@ -1284,7 +1337,8 @@ function buildCpuHotspots(trace: ParsedTrace) { b.sampleCount - a.sampleCount || a.functionName.localeCompare(b.functionName), ); -} + +}); function aggregateCpuHotspotsForWindow( trace: ParsedTrace, @@ -1348,7 +1402,7 @@ function buildTaskCpuHotspots(trace: ParsedTrace) { ); } -function buildInteractionWindows(trace: ParsedTrace) { +const buildInteractionWindows = memoBuilder((trace: ParsedTrace) => { const interactions = buildInteractions(trace); const renders = buildRenderMeasures(trace); const requests = buildRequests(trace); @@ -1384,9 +1438,10 @@ function buildInteractionWindows(trace: ParsedTrace) { rawEventIds: interaction.rawEventIds, provenance: { rawIds: interaction.rawEventIds, layer: "devtools/views.interactionWindows" }, })); -} -function buildVisualChanges(trace: ParsedTrace) { +}); + +const buildVisualChanges = memoBuilder((trace: ParsedTrace) => { const rows: any[] = []; buildScreenshots(trace).forEach((row) => { rows.push({ @@ -1420,9 +1475,10 @@ function buildVisualChanges(trace: ParsedTrace) { } }); return rows.sort((a, b) => a.tsUs - b.tsUs); -} -function buildRenderComponentHotspots(trace: ParsedTrace) { +}); + +const buildRenderComponentHotspots = memoBuilder((trace: ParsedTrace) => { const groups = new Map(); buildRenderMeasures(trace).forEach((row) => { const componentName = row.componentName ?? "(unknown)"; @@ -1449,9 +1505,10 @@ function buildRenderComponentHotspots(trace: ParsedTrace) { provenance: { rawIds: row.rawEventIds, layer: "devtools/views.renderComponentHotspots" }, })) .sort((a, b) => b.totalDurationMs - a.totalDurationMs || b.renderCount - a.renderCount); -} -function buildInteractionRenders(trace: ParsedTrace) { +}); + +const buildInteractionRenders = memoBuilder((trace: ParsedTrace) => { const renders = buildRenderMeasures(trace); return buildInteractions(trace) .flatMap((interaction) => { @@ -1483,9 +1540,10 @@ function buildInteractionRenders(trace: ParsedTrace) { (a, b) => b.totalDurationMs - a.totalDurationMs || a.interactionId.localeCompare(b.interactionId), ); -} -function buildNetworkWaterfall(trace: ParsedTrace) { +}); + +const buildNetworkWaterfall = memoBuilder((trace: ParsedTrace) => { const { minTs } = getTraceBounds(trace.traceEvents); return buildRequests(trace).map((row, index) => ({ requestWaterfallId: `request-waterfall:${index}`, @@ -1500,9 +1558,10 @@ function buildNetworkWaterfall(trace: ParsedTrace) { rawEventIds: row.rawEventIds, provenance: { rawIds: row.rawEventIds, layer: "devtools/views.networkWaterfall" }, })); -} -function buildLayoutShiftClusters(trace: ParsedTrace) { +}); + +const buildLayoutShiftClusters = memoBuilder((trace: ParsedTrace) => { const shifts = buildLayoutShifts(trace).sort((a, b) => a.tsUs - b.tsUs); const clusters: any[] = []; let current: any = null; @@ -1536,9 +1595,10 @@ function buildLayoutShiftClusters(trace: ParsedTrace) { provenance: { rawIds: cluster.rawEventIds, layer: "devtools/views.layoutShiftClusters" }, })) .sort((a, b) => b.totalScore - a.totalScore || a.startTsUs - b.startTsUs); -} -function buildRequestBodies(trace: ParsedTrace) { +}); + +const buildRequestBodies = memoBuilder((trace: ParsedTrace) => { const requests = new Map(buildRequests(trace).map((row) => [row.requestId, row] as const)); const rows: any[] = []; trace.traceEvents.forEach((event, index) => { @@ -1570,7 +1630,8 @@ function buildRequestBodies(trace: ParsedTrace) { }); }); return rows; -} + +}); async function buildSummary(session: DatasetSession) { const [ @@ -2361,7 +2422,7 @@ export class DevtoolsDriver implements SourceDriver { session.layers.register({ key: "devtools/indexes.basic", deps: ["devtools/trace"], - build: async () => buildIndexes(trace.traceEvents), + build: async () => buildIndexes(trace), }); session.layers.register({ key: "devtools/indexes.secondary",