fix(web-probe): correlate CPU samples with performance windows

This commit is contained in:
Codex
2026-07-02 15:41:39 +00:00
parent d9b3fc79e1
commit 225e3e6836
5 changed files with 725 additions and 31 deletions
@@ -3,10 +3,14 @@
export function nodeWebObserveAnalyzerPerformanceSource(): string {
return String.raw`
function buildFrontendPerformanceReport(rows, artifacts, samples, network) {
var frontendPerformanceSourceMapper = null;
async function buildFrontendPerformanceReport(rows, artifacts, samples, network) {
const sourceRows = Array.isArray(rows) ? rows : [];
const sourceSamples = Array.isArray(samples) ? samples : [];
const sourceNetwork = Array.isArray(network) ? network : [];
const sourceMapState = await buildFrontendPerformanceSourceMapState(artifacts);
frontendPerformanceSourceMapper = createFrontendPerformanceSourceMapper(sourceMapState.maps);
const events = [];
const drainErrors = [];
const captures = [];
@@ -33,6 +37,7 @@ function buildFrontendPerformanceReport(rows, artifacts, samples, network) {
events.push(event);
for (const script of Array.isArray(perf.scripts) ? perf.scripts : []) mergeLoafScript(scriptGroups, script, event);
}
await attachCpuProfileTimelines(captures);
const longTasks = events.filter((item) => item.kind === "longtask");
const loafs = events.filter((item) => item.kind === "long-animation-frame");
const gaps = events.filter((item) => item.kind === "event-loop-gap");
@@ -49,7 +54,7 @@ function buildFrontendPerformanceReport(rows, artifacts, samples, network) {
.sort((left, right) => Number(right.selfTimeMs ?? 0) - Number(left.selfTimeMs ?? 0))
.slice(0, 30);
const performanceWindows = buildFrontendPerformanceWindows({ events, captures, profileHotspots, profileStacks, scriptHotspots, samples: sourceSamples, network: sourceNetwork });
const sourceAttribution = buildFrontendSourceAttribution({ scriptHotspots, profileHotspots, profileStacks });
const sourceAttribution = buildFrontendSourceAttribution({ scriptHotspots, profileHotspots, profileStacks, sourceMapState });
const attribution = frontendPerformanceAttributionStatus({ captures, profileHotspots, profileStacks, scriptHotspots, longTasks, loafs, gaps, performanceWindows });
return {
summary: {
@@ -280,6 +285,12 @@ function compactPerformanceCapture(row) {
windowSource: startEpochMs === null ? "missing-capture-window" : "completed-event-ts-minus-artifact-duration",
profileTotalTimeMs: numberOrNull(summary.totalTimeMs),
profileSampleCount: numberOrNull(summary.sampleCount),
profileTimelineStatus: "not-loaded",
profileTimelineSampleCount: null,
profileTimelineTotalTimeMs: null,
profileTimelineStartAt: null,
profileTimelineEndAt: null,
profileTimeline: null,
path: artifact.path ?? null,
summaryPath: artifact.summaryPath ?? null,
sha256: artifact.sha256 ?? null,
@@ -396,6 +407,225 @@ function performanceCaptureArtifacts(artifacts) {
.map((item) => ({ ts: item.ts ?? null, captureId: item.captureId ?? null, commandId: item.commandId ?? null, path: item.path ?? null, summaryPath: item.summaryPath ?? null, sha256: item.sha256 ?? null, summarySha256: item.summarySha256 ?? null, valuesRedacted: true }));
}
async function attachCpuProfileTimelines(captures) {
for (const capture of Array.isArray(captures) ? captures : []) {
const profilePath = typeof capture?.path === "string" && capture.path ? capture.path : null;
if (!profilePath) {
capture.profileTimelineStatus = "missing-profile-path";
continue;
}
const profile = await readJson(profilePath);
const timeline = buildCpuProfileSampleTimeline(profile, capture);
capture.profileTimelineStatus = timeline.status;
capture.profileTimelineSampleCount = timeline.sampleCount;
capture.profileTimelineTotalTimeMs = timeline.totalTimeMs;
capture.profileTimelineStartAt = timeline.startAt;
capture.profileTimelineEndAt = timeline.endAt;
capture.profileTimeline = timeline.samples;
}
}
function buildCpuProfileSampleTimeline(profile, capture) {
if (!profile || typeof profile !== "object") return emptyCpuProfileTimeline("profile-read-failed");
const nodes = Array.isArray(profile.nodes) ? profile.nodes : [];
const sampleIds = Array.isArray(profile.samples) ? profile.samples : [];
if (nodes.length === 0 || sampleIds.length === 0) return emptyCpuProfileTimeline("profile-has-no-samples");
const byId = new Map();
const parentById = new Map();
for (const node of nodes) {
const id = Number(node?.id);
if (!Number.isFinite(id)) continue;
byId.set(id, node);
for (const childId of Array.isArray(node?.children) ? node.children : []) {
const child = Number(childId);
if (Number.isFinite(child)) parentById.set(child, id);
}
}
const profileStart = Number(profile.startTime);
const profileEnd = Number(profile.endTime);
const profileDurationMs = Number.isFinite(profileStart) && Number.isFinite(profileEnd) && profileEnd > profileStart ? (profileEnd - profileStart) / 1000 : null;
const captureStart = Number(capture?.startEpochMs);
const captureEnd = Number(capture?.endEpochMs);
const baseEpochMs = Number.isFinite(captureStart)
? captureStart
: Number.isFinite(captureEnd) && Number.isFinite(profileDurationMs)
? captureEnd - Number(profileDurationMs)
: null;
if (!Number.isFinite(baseEpochMs)) return emptyCpuProfileTimeline("missing-capture-timebase");
const deltas = Array.isArray(profile.timeDeltas) ? profile.timeDeltas : [];
const fallbackDeltaMs = Number.isFinite(profileDurationMs) && sampleIds.length > 0 ? Number(profileDurationMs) / sampleIds.length : 0;
const samples = [];
let cursor = Number(baseEpochMs);
for (let index = 0; index < sampleIds.length; index += 1) {
const nodeId = Number(sampleIds[index]);
const rawDelta = Number(deltas[index]);
const durationMs = Number.isFinite(rawDelta) && rawDelta > 0 ? rawDelta / 1000 : fallbackDeltaMs;
const startEpochMs = cursor;
const endEpochMs = cursor + (Number.isFinite(durationMs) && durationMs > 0 ? durationMs : 0);
cursor = endEpochMs;
const frames = cpuProfileFramesForNode(nodeId, byId, parentById).map(annotateProfileFrameSource).filter(Boolean);
samples.push({
index,
nodeId: Number.isFinite(nodeId) ? nodeId : null,
startEpochMs: roundMs(startEpochMs),
endEpochMs: roundMs(endEpochMs),
durationMs: roundMs(endEpochMs - startEpochMs),
leaf: frames[frames.length - 1] || null,
frames,
valuesRedacted: true,
});
}
const totalTimeMs = samples.reduce((sum, sample) => sum + Number(sample.durationMs || 0), 0);
const startAt = samples.length > 0 ? new Date(Number(samples[0].startEpochMs)).toISOString() : null;
const endAt = samples.length > 0 ? new Date(Number(samples[samples.length - 1].endEpochMs)).toISOString() : null;
return {
status: "loaded",
sampleCount: samples.length,
totalTimeMs: roundMs(totalTimeMs),
startAt,
endAt,
samples,
valuesRedacted: true,
};
}
function emptyCpuProfileTimeline(status) {
return { status, sampleCount: 0, totalTimeMs: null, startAt: null, endAt: null, samples: [], valuesRedacted: true };
}
function cpuProfileFramesForNode(nodeId, byId, parentById) {
const frames = [];
const seen = new Set();
let current = nodeId;
while (Number.isFinite(current) && byId.has(current) && !seen.has(current)) {
seen.add(current);
const node = byId.get(current);
const callFrame = node?.callFrame && typeof node.callFrame === "object" ? node.callFrame : {};
frames.push(compactCpuProfileTimelineFrame(callFrame, node));
current = parentById.get(current);
}
return frames.reverse();
}
function compactCpuProfileTimelineFrame(callFrame, node) {
return {
functionName: limitText(callFrame?.functionName || "(anonymous)", 160),
url: safeReportUrl(callFrame?.url || ""),
scriptId: callFrame?.scriptId ?? node?.callFrame?.scriptId ?? null,
lineNumber: numberOrNull(callFrame?.lineNumber),
columnNumber: numberOrNull(callFrame?.columnNumber),
valuesRedacted: true,
};
}
function sameWindowProfileEvidence(event, capture, input) {
const timeline = Array.isArray(capture?.profileTimeline) ? capture.profileTimeline : [];
const eventStart = Number(event?.startEpochMs);
const eventEnd = Number(event?.endEpochMs);
if (!capture) return null;
if (timeline.length === 0 || ![eventStart, eventEnd].every(Number.isFinite)) {
return {
captureId: capture.captureId ?? null,
commandId: capture.commandId ?? null,
evidenceScope: "same-capture-aggregate-context",
timelineStatus: capture.profileTimelineStatus || "missing-profile-timeline",
topFunctions: (Array.isArray(input?.profileHotspots) ? input.profileHotspots : []).slice(0, 5),
topStacks: (Array.isArray(input?.profileStacks) ? input.profileStacks : []).slice(0, 3),
note: "CPU profile sample timeline is unavailable; hotspot rows are capture-level context, not same-window evidence.",
valuesRedacted: true,
};
}
const functionGroups = new Map();
const stackGroups = new Map();
let sampleCount = 0;
let totalTimeMs = 0;
for (const sample of timeline) {
const start = Number(sample?.startEpochMs);
const end = Number(sample?.endEpochMs);
if (![start, end].every(Number.isFinite)) continue;
const overlapMs = Math.max(0, Math.min(end, eventEnd) - Math.max(start, eventStart));
if (overlapMs <= 0) continue;
sampleCount += 1;
totalTimeMs += overlapMs;
const frames = Array.isArray(sample?.frames) ? sample.frames : [];
const leaf = sample?.leaf || frames[frames.length - 1] || null;
mergeWindowProfileFunction(functionGroups, leaf, overlapMs, overlapMs);
for (const frame of frames) mergeWindowProfileFunction(functionGroups, frame, 0, overlapMs);
mergeWindowProfileStack(stackGroups, frames, leaf, overlapMs);
}
const topFunctions = Array.from(functionGroups.values())
.map(finalizeWindowProfileFunction)
.sort((left, right) => Number(right.selfTimeMs ?? 0) - Number(left.selfTimeMs ?? 0) || Number(right.totalTimeMs ?? 0) - Number(left.totalTimeMs ?? 0))
.slice(0, 8);
const topStacks = Array.from(stackGroups.values())
.map(finalizeWindowProfileStack)
.sort((left, right) => Number(right.selfTimeMs ?? 0) - Number(left.selfTimeMs ?? 0))
.slice(0, 5);
return {
captureId: capture.captureId ?? null,
commandId: capture.commandId ?? null,
evidenceScope: "same-window-cpu-samples",
timelineStatus: sampleCount > 0 ? "window-samples" : "no-window-samples",
sampleCount,
totalTimeMs: roundMs(totalTimeMs),
windowStartAt: Number.isFinite(eventStart) ? new Date(eventStart).toISOString() : null,
windowEndAt: Number.isFinite(eventEnd) ? new Date(eventEnd).toISOString() : null,
topFunctions,
topStacks,
valuesRedacted: true,
};
}
function mergeWindowProfileFunction(groups, frame, selfTimeMs, totalTimeMs) {
if (!frame || typeof frame !== "object") return;
const source = frame.sourceMapStatus ? {
sourceFile: frame.sourceFile ?? sourceFileFromUrl(frame.url),
sourceLine: frame.sourceLine ?? numberOrNull(frame.lineNumber),
sourceColumn: frame.sourceColumn ?? numberOrNull(frame.columnNumber),
sourceMapStatus: frame.sourceMapStatus,
sourceAttributionMode: frame.sourceAttributionMode ?? "browser-raw-url-line-column",
} : sourceAttributionFor(frame.url, frame.functionName, frame.lineNumber, frame.columnNumber, null);
const key = [frame.functionName || "(anonymous)", frame.url || frame.scriptId || "", frame.lineNumber ?? "", frame.columnNumber ?? ""].join("@");
const group = groups.get(key) || {
functionName: limitText(frame.functionName || "(anonymous)", 160),
url: safeReportUrl(frame.url || ""),
scriptId: frame.scriptId ?? null,
sourceFile: source.sourceFile,
sourceLine: source.sourceLine,
sourceColumn: source.sourceColumn,
sourceMapStatus: source.sourceMapStatus,
sourceAttributionMode: source.sourceAttributionMode,
lineNumber: numberOrNull(frame.lineNumber),
columnNumber: numberOrNull(frame.columnNumber),
sampleCount: 0,
selfTimeMs: 0,
totalTimeMs: 0,
valuesRedacted: true,
};
group.sampleCount += selfTimeMs > 0 ? 1 : 0;
group.selfTimeMs += Number(selfTimeMs || 0);
group.totalTimeMs += Number(totalTimeMs || 0);
groups.set(key, group);
}
function finalizeWindowProfileFunction(group) {
return { ...group, selfTimeMs: roundMs(group.selfTimeMs), totalTimeMs: roundMs(group.totalTimeMs), valuesRedacted: true };
}
function mergeWindowProfileStack(groups, frames, leaf, selfTimeMs) {
const stackFrames = Array.isArray(frames) ? frames.filter(Boolean) : [];
if (stackFrames.length === 0) return;
const key = stackFrames.map((frame) => [frame.functionName || "(anonymous)", frame.url || frame.scriptId || "", frame.lineNumber ?? "", frame.columnNumber ?? ""].join("@")).join(" <- ");
const group = groups.get(key) || { key, leaf: annotateProfileFrameSource(leaf), frames: stackFrames.slice(-10).map(annotateProfileFrameSource), sampleCount: 0, selfTimeMs: 0, valuesRedacted: true };
group.sampleCount += 1;
group.selfTimeMs += Number(selfTimeMs || 0);
groups.set(key, group);
}
function finalizeWindowProfileStack(group) {
return { ...group, selfTimeMs: roundMs(group.selfTimeMs), valuesRedacted: true };
}
function buildFrontendPerformanceWindows(input) {
const aggregatedLoafHotspotKeys = new Set(
(Array.isArray(input?.scriptHotspots) ? input.scriptHotspots : [])
@@ -428,12 +658,9 @@ function correlateFrontendPerformanceWindow(event, input) {
const coverageStatus = selectedCapture
? covering.length > 0 ? "covered" : overlapping.length > 0 ? "overlapped" : "missed"
: "no-cpu-profile";
const profileEvidence = coverageStatus === "covered" || coverageStatus === "overlapped" ? {
topFunctions: (Array.isArray(input?.profileHotspots) ? input.profileHotspots : []).slice(0, 5),
topStacks: (Array.isArray(input?.profileStacks) ? input.profileStacks : []).slice(0, 3),
evidenceScope: "same-capture-window-candidate",
valuesRedacted: true,
} : null;
const profileEvidence = coverageStatus === "covered" || coverageStatus === "overlapped"
? sameWindowProfileEvidence(event, selectedCapture, input)
: null;
const sourceHint = event.kind === "long-animation-frame" ? topLoafScriptHint(event) : null;
return {
kind: event.kind,
@@ -570,6 +797,11 @@ function compactWindowCapture(capture, event) {
windowDistanceMs: Number.isFinite(distanceMs) ? roundMs(distanceMs) : null,
profileSampleCount: capture.profileSampleCount ?? null,
profileTotalTimeMs: capture.profileTotalTimeMs ?? null,
profileTimelineStatus: capture.profileTimelineStatus ?? null,
profileTimelineSampleCount: capture.profileTimelineSampleCount ?? null,
profileTimelineTotalTimeMs: capture.profileTimelineTotalTimeMs ?? null,
profileTimelineStartAt: capture.profileTimelineStartAt ?? null,
profileTimelineEndAt: capture.profileTimelineEndAt ?? null,
valuesRedacted: true,
};
}
@@ -650,6 +882,183 @@ function topLoafScriptHint(event) {
.sort((left, right) => Number(right.durationMs ?? 0) - Number(left.durationMs ?? 0))[0] || null;
}
async function buildFrontendPerformanceSourceMapState(artifacts) {
const candidates = sourceMapArtifactCandidates(artifacts);
const maps = [];
let failedCount = 0;
for (const candidate of candidates) {
const parsed = await readJson(candidate.path);
const map = parseFrontendSourceMap(parsed, candidate);
if (map) maps.push(map);
else failedCount += 1;
}
return { artifactCount: candidates.length, loadedCount: maps.length, failedCount, maps, valuesRedacted: true };
}
function sourceMapArtifactCandidates(artifacts) {
const rows = [];
const seen = new Set();
for (const item of Array.isArray(artifacts) ? artifacts : []) {
if (!item || typeof item !== "object") continue;
const paths = [item.path, item.sourceMapPath, item.mapPath, item.summaryPath].filter((value) => typeof value === "string" && value);
for (const candidatePath of paths) {
const kind = String(item.kind || item.type || "").toLowerCase();
const url = String(item.url || item.sourceURL || item.sourceMapUrl || "");
const pathText = String(candidatePath);
const looksLikeMap = kind.includes("source-map") || kind.includes("sourcemap") || pathText.endsWith(".map") || url.endsWith(".map");
if (!looksLikeMap || seen.has(pathText)) continue;
seen.add(pathText);
rows.push({ path: pathText, url: url || null, kind: item.kind ?? null, ts: item.ts ?? null, valuesRedacted: true });
}
}
return rows.slice(-40);
}
function parseFrontendSourceMap(raw, candidate) {
if (!raw || typeof raw !== "object" || typeof raw.mappings !== "string" || !Array.isArray(raw.sources)) return null;
const parsedMappings = parseSourceMapMappings(raw.mappings, raw.sources, Array.isArray(raw.names) ? raw.names : []);
return {
path: candidate.path,
url: candidate.url ?? null,
file: typeof raw.file === "string" ? raw.file : null,
sources: raw.sources,
names: Array.isArray(raw.names) ? raw.names : [],
lines: parsedMappings.lines,
mappingCount: parsedMappings.mappingCount,
valuesRedacted: true,
};
}
function parseSourceMapMappings(mappings, sources, names) {
const lines = [];
let sourceIndex = 0;
let originalLine = 0;
let originalColumn = 0;
let nameIndex = 0;
const rawLines = String(mappings || "").split(";");
for (let generatedLine = 0; generatedLine < rawLines.length; generatedLine += 1) {
let generatedColumn = 0;
const segments = [];
for (const segmentText of rawLines[generatedLine].split(",")) {
if (!segmentText) continue;
const values = decodeSourceMapVlqSegment(segmentText);
if (values.length === 0) continue;
generatedColumn += Number(values[0] || 0);
if (values.length >= 4) {
sourceIndex += Number(values[1] || 0);
originalLine += Number(values[2] || 0);
originalColumn += Number(values[3] || 0);
if (values.length >= 5) nameIndex += Number(values[4] || 0);
if (sources[sourceIndex]) {
segments.push({
generatedColumn,
source: String(sources[sourceIndex]),
originalLine,
originalColumn,
name: names[nameIndex] ? String(names[nameIndex]) : null,
valuesRedacted: true,
});
}
}
}
lines[generatedLine] = segments;
}
return { lines, mappingCount: lines.reduce((sum, line) => sum + (Array.isArray(line) ? line.length : 0), 0), valuesRedacted: true };
}
function decodeSourceMapVlqSegment(segmentText) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const values = [];
let value = 0;
let shift = 0;
for (const char of String(segmentText || "")) {
const digit = chars.indexOf(char);
if (digit < 0) return [];
const continuation = (digit & 32) !== 0;
value += (digit & 31) << shift;
if (continuation) {
shift += 5;
continue;
}
const negative = (value & 1) === 1;
const decoded = value >> 1;
values.push(negative ? -decoded : decoded);
value = 0;
shift = 0;
}
return values;
}
function createFrontendPerformanceSourceMapper(maps) {
const loadedMaps = Array.isArray(maps) ? maps.filter((item) => item && Array.isArray(item.lines)) : [];
return {
loadedCount: loadedMaps.length,
resolve(url, lineNumber, columnNumber, sourceCharPosition) {
if (loadedMaps.length === 0) return null;
const matching = loadedMaps.filter((map) => sourceMapMatchesUrl(map, url));
if (matching.length === 0) return { status: "missing", mapped: null };
const generatedLine = sourceMapGeneratedLine(lineNumber);
const generatedColumn = sourceMapGeneratedColumn(columnNumber, sourceCharPosition);
if (![generatedLine, generatedColumn].every(Number.isFinite)) return { status: "unmapped", mapped: null };
for (const map of matching) {
const mapped = lookupSourceMapPosition(map, generatedLine, generatedColumn);
if (mapped) return { status: "mapped", mapped, map };
}
return { status: "unmapped", mapped: null };
},
};
}
function sourceMapMatchesUrl(map, url) {
const raw = String(url || "");
if (!raw) return false;
const urlBase = sourceFileFromUrl(raw);
const mapFile = sourceFileFromUrl(map?.file || "");
const mapPathBase = sourceFileFromUrl(map?.path || "");
const mapUrlBase = sourceFileFromUrl(map?.url || "");
const expectedMapBase = urlBase ? urlBase + ".map" : "";
return Boolean(
(mapFile && urlBase && mapFile === urlBase) ||
(mapPathBase && expectedMapBase && mapPathBase === expectedMapBase) ||
(mapUrlBase && expectedMapBase && mapUrlBase === expectedMapBase) ||
(mapPathBase && urlBase && mapPathBase.replace(/\.map$/u, "") === urlBase) ||
(mapUrlBase && urlBase && mapUrlBase.replace(/\.map$/u, "") === urlBase)
);
}
function sourceMapGeneratedLine(lineNumber) {
const line = Number(lineNumber);
return Number.isFinite(line) && line >= 0 ? Math.floor(line) : 0;
}
function sourceMapGeneratedColumn(columnNumber, sourceCharPosition) {
if (sourceCharPosition !== null && sourceCharPosition !== undefined) {
const charPosition = Number(sourceCharPosition);
if (Number.isFinite(charPosition) && charPosition >= 0) return Math.floor(charPosition);
}
const column = Number(columnNumber);
return Number.isFinite(column) && column >= 0 ? Math.floor(column) : null;
}
function lookupSourceMapPosition(map, generatedLine, generatedColumn) {
const line = Array.isArray(map?.lines?.[generatedLine]) ? map.lines[generatedLine] : [];
let selected = null;
for (const segment of line) {
if (Number(segment?.generatedColumn) <= generatedColumn) selected = segment;
else break;
}
if (!selected) return null;
return {
sourceFile: limitText(selected.source || "", 180),
sourceLine: Number.isFinite(Number(selected.originalLine)) ? Number(selected.originalLine) + 1 : null,
sourceColumn: numberOrNull(selected.originalColumn),
sourceName: selected.name ? limitText(selected.name, 160) : null,
generatedLine,
generatedColumn,
valuesRedacted: true,
};
}
function buildFrontendSourceAttribution(input) {
const sourceFiles = new Map();
const add = (row) => {
@@ -665,10 +1074,24 @@ function buildFrontendSourceAttribution(input) {
for (const stack of Array.isArray(input?.profileStacks) ? input.profileStacks : []) {
for (const frame of Array.isArray(stack?.frames) ? stack.frames : []) add(frame);
}
const rows = Array.from(sourceFiles.values());
const mappedCount = rows.filter((item) => item.sourceMapStatus === "mapped").length;
const unmappedCount = rows.filter((item) => item.sourceMapStatus === "unmapped").length;
const state = input?.sourceMapState && typeof input.sourceMapState === "object" ? input.sourceMapState : {};
const loadedCount = Number(state.loadedCount ?? 0);
const status = mappedCount > 0 ? "mapped" : loadedCount > 0 || unmappedCount > 0 ? "unmapped" : "missing";
const note = status === "mapped"
? "Source-map artifact was loaded; mapped rows include original source file and line while raw browser locations remain present."
: loadedCount > 0
? "Source-map artifact was loaded, but observed browser locations did not resolve to original source positions."
: "No source-map artifact is loaded by this analyzer; function/file/line fields are raw browser URLs or CPU profile callFrame locations.";
return {
sourceMapStatus: "missing",
note: "No source-map artifact is loaded by this analyzer; function/file/line fields are raw browser URLs or CPU profile callFrame locations.",
sourceFiles: Array.from(sourceFiles.values()).sort((left, right) => Number(right.count ?? 0) - Number(left.count ?? 0)).slice(0, 20),
sourceMapStatus: status,
note,
sourceMapArtifactCount: numberOrNull(state.artifactCount),
sourceMapLoadedCount: numberOrNull(state.loadedCount),
sourceMapFailedCount: numberOrNull(state.failedCount),
sourceFiles: rows.sort((left, right) => Number(right.count ?? 0) - Number(left.count ?? 0)).slice(0, 20),
valuesRedacted: true,
};
}
@@ -680,14 +1103,30 @@ function annotateProfileFrameSource(frame) {
}
function sourceAttributionFor(url, functionName, lineNumber, columnNumber, sourceCharPosition) {
const resolved = frontendPerformanceSourceMapper?.resolve ? frontendPerformanceSourceMapper.resolve(url, lineNumber, columnNumber, sourceCharPosition) : null;
if (resolved?.status === "mapped" && resolved.mapped) {
return {
functionName: limitText(resolved.mapped.sourceName || functionName || "", 160),
sourceFile: resolved.mapped.sourceFile,
sourceLine: resolved.mapped.sourceLine,
sourceColumn: resolved.mapped.sourceColumn,
sourceCharPosition: numberOrNull(sourceCharPosition),
sourceMapStatus: "mapped",
sourceAttributionMode: "source-map-original-position",
rawSourceFile: sourceFileFromUrl(url),
rawLineNumber: numberOrNull(lineNumber),
rawColumnNumber: numberOrNull(columnNumber),
valuesRedacted: true,
};
}
return {
functionName: limitText(functionName ?? "", 160),
sourceFile: sourceFileFromUrl(url),
sourceLine: numberOrNull(lineNumber),
sourceColumn: numberOrNull(columnNumber),
sourceCharPosition: numberOrNull(sourceCharPosition),
sourceMapStatus: "missing",
sourceAttributionMode: "browser-raw-url-line-column",
sourceMapStatus: resolved?.status === "unmapped" ? "unmapped" : "missing",
sourceAttributionMode: resolved?.status === "unmapped" ? "source-map-loaded-no-match" : "browser-raw-url-line-column",
valuesRedacted: true,
};
}