fix: correlate webprobe performance windows
This commit is contained in:
@@ -3,8 +3,10 @@
|
|||||||
|
|
||||||
export function nodeWebObserveAnalyzerPerformanceSource(): string {
|
export function nodeWebObserveAnalyzerPerformanceSource(): string {
|
||||||
return String.raw`
|
return String.raw`
|
||||||
function buildFrontendPerformanceReport(rows, artifacts) {
|
function buildFrontendPerformanceReport(rows, artifacts, samples, network) {
|
||||||
const sourceRows = Array.isArray(rows) ? rows : [];
|
const sourceRows = Array.isArray(rows) ? rows : [];
|
||||||
|
const sourceSamples = Array.isArray(samples) ? samples : [];
|
||||||
|
const sourceNetwork = Array.isArray(network) ? network : [];
|
||||||
const events = [];
|
const events = [];
|
||||||
const drainErrors = [];
|
const drainErrors = [];
|
||||||
const captures = [];
|
const captures = [];
|
||||||
@@ -46,7 +48,9 @@ function buildFrontendPerformanceReport(rows, artifacts) {
|
|||||||
.map(finalizeProfileStackHotspot)
|
.map(finalizeProfileStackHotspot)
|
||||||
.sort((left, right) => Number(right.selfTimeMs ?? 0) - Number(left.selfTimeMs ?? 0))
|
.sort((left, right) => Number(right.selfTimeMs ?? 0) - Number(left.selfTimeMs ?? 0))
|
||||||
.slice(0, 30);
|
.slice(0, 30);
|
||||||
const attribution = frontendPerformanceAttributionStatus({ captures, profileHotspots, profileStacks, scriptHotspots, longTasks, loafs, gaps });
|
const performanceWindows = buildFrontendPerformanceWindows({ events, captures, profileHotspots, profileStacks, scriptHotspots, samples: sourceSamples, network: sourceNetwork });
|
||||||
|
const sourceAttribution = buildFrontendSourceAttribution({ scriptHotspots, profileHotspots, profileStacks });
|
||||||
|
const attribution = frontendPerformanceAttributionStatus({ captures, profileHotspots, profileStacks, scriptHotspots, longTasks, loafs, gaps, performanceWindows });
|
||||||
return {
|
return {
|
||||||
summary: {
|
summary: {
|
||||||
rowCount: sourceRows.length,
|
rowCount: sourceRows.length,
|
||||||
@@ -70,6 +74,12 @@ function buildFrontendPerformanceReport(rows, artifacts) {
|
|||||||
loafOnly: attribution.loafOnly,
|
loafOnly: attribution.loafOnly,
|
||||||
cpuProfileHotspotEvidence: attribution.cpuProfileHotspotEvidence,
|
cpuProfileHotspotEvidence: attribution.cpuProfileHotspotEvidence,
|
||||||
evidenceNote: attribution.evidenceNote,
|
evidenceNote: attribution.evidenceNote,
|
||||||
|
windowCorrelationCount: performanceWindows.length,
|
||||||
|
cpuProfileWindowCoveredCount: performanceWindows.filter((item) => item.cpuProfileCoverageStatus === "covered").length,
|
||||||
|
cpuProfileWindowOverlappedCount: performanceWindows.filter((item) => item.cpuProfileCoverageStatus === "overlapped").length,
|
||||||
|
cpuProfileWindowMissedCount: performanceWindows.filter((item) => item.cpuProfileCoverageStatus === "missed").length,
|
||||||
|
cpuProfileWindowMissingCount: performanceWindows.filter((item) => item.cpuProfileCoverageStatus === "no-cpu-profile").length,
|
||||||
|
sourceMapStatus: sourceAttribution.sourceMapStatus,
|
||||||
captureArtifacts: performanceCaptureArtifacts(artifacts),
|
captureArtifacts: performanceCaptureArtifacts(artifacts),
|
||||||
valuesRedacted: true,
|
valuesRedacted: true,
|
||||||
},
|
},
|
||||||
@@ -79,6 +89,8 @@ function buildFrontendPerformanceReport(rows, artifacts) {
|
|||||||
scriptHotspots,
|
scriptHotspots,
|
||||||
profileHotspots,
|
profileHotspots,
|
||||||
profileStacks,
|
profileStacks,
|
||||||
|
performanceWindows,
|
||||||
|
sourceAttribution,
|
||||||
captures: captures.slice(-20),
|
captures: captures.slice(-20),
|
||||||
drainErrors: drainErrors.slice(-20),
|
drainErrors: drainErrors.slice(-20),
|
||||||
valuesRedacted: true,
|
valuesRedacted: true,
|
||||||
@@ -142,6 +154,7 @@ function buildFrontendPerformanceFindings(report) {
|
|||||||
attributionMode: summary.attributionMode || "loaf-only-no-cpu-profile",
|
attributionMode: summary.attributionMode || "loaf-only-no-cpu-profile",
|
||||||
cpuProfileStatus: summary.cpuProfileStatus || "missing",
|
cpuProfileStatus: summary.cpuProfileStatus || "missing",
|
||||||
captureCount: summary.captureCount ?? 0,
|
captureCount: summary.captureCount ?? 0,
|
||||||
|
windowCorrelations: (report?.performanceWindows || []).slice(0, 8),
|
||||||
longTaskCount: summary.longTaskCount ?? 0,
|
longTaskCount: summary.longTaskCount ?? 0,
|
||||||
longAnimationFrameCount: summary.longAnimationFrameCount ?? 0,
|
longAnimationFrameCount: summary.longAnimationFrameCount ?? 0,
|
||||||
eventLoopGapCount: summary.eventLoopGapCount ?? 0,
|
eventLoopGapCount: summary.eventLoopGapCount ?? 0,
|
||||||
@@ -171,15 +184,20 @@ function frontendPerformanceAttributionStatus(input) {
|
|||||||
(Array.isArray(input?.gaps) ? input.gaps.length : 0);
|
(Array.isArray(input?.gaps) ? input.gaps.length : 0);
|
||||||
const hasCpuProfile = captureCount > 0;
|
const hasCpuProfile = captureCount > 0;
|
||||||
const hasCpuProfileHotspots = profileHotspotCount > 0 || profileStackCount > 0;
|
const hasCpuProfileHotspots = profileHotspotCount > 0 || profileStackCount > 0;
|
||||||
|
const windowRows = Array.isArray(input?.performanceWindows) ? input.performanceWindows : [];
|
||||||
|
const coveredWindowCount = windowRows.filter((item) => item?.cpuProfileCoverageStatus === "covered" || item?.cpuProfileCoverageStatus === "overlapped").length;
|
||||||
|
const missedWindowCount = windowRows.filter((item) => item?.cpuProfileCoverageStatus === "missed").length;
|
||||||
if (hasCpuProfile) {
|
if (hasCpuProfile) {
|
||||||
return {
|
return {
|
||||||
cpuProfileStatus: hasCpuProfileHotspots ? "captured-with-hotspots" : "captured-no-hotspots",
|
cpuProfileStatus: hasCpuProfileHotspots ? (coveredWindowCount > 0 ? "captured-with-window-hotspots" : "captured-hotspots-outside-performance-window") : "captured-no-hotspots",
|
||||||
attributionMode: "cpu-profile-and-performance-observer",
|
attributionMode: missedWindowCount > 0 && coveredWindowCount === 0 ? "cpu-profile-missed-performance-window" : "cpu-profile-and-performance-observer",
|
||||||
noCpuProfile: false,
|
noCpuProfile: false,
|
||||||
loafOnly: false,
|
loafOnly: false,
|
||||||
cpuProfileHotspotEvidence: hasCpuProfileHotspots,
|
cpuProfileHotspotEvidence: hasCpuProfileHotspots,
|
||||||
evidenceNote: hasCpuProfileHotspots
|
evidenceNote: hasCpuProfileHotspots
|
||||||
? "completed performanceCapture artifacts produced CPU profile hotspot evidence"
|
? coveredWindowCount > 0
|
||||||
|
? "completed performanceCapture artifacts overlap severe frontend performance windows; attached CPU hotspots are same-capture candidates"
|
||||||
|
: "completed performanceCapture artifacts produced CPU profile hotspots, but none overlap the severe frontend performance windows"
|
||||||
: "completed performanceCapture artifacts exist, but no CPU profile hotspots were extracted",
|
: "completed performanceCapture artifacts exist, but no CPU profile hotspots were extracted",
|
||||||
valuesRedacted: true,
|
valuesRedacted: true,
|
||||||
};
|
};
|
||||||
@@ -198,6 +216,7 @@ function frontendPerformanceAttributionStatus(input) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function compactPerformanceEventRow(row, perf) {
|
function compactPerformanceEventRow(row, perf) {
|
||||||
|
const window = performanceEventWindow(row, perf);
|
||||||
return {
|
return {
|
||||||
ts: row.ts ?? null,
|
ts: row.ts ?? null,
|
||||||
seq: row.seq ?? null,
|
seq: row.seq ?? null,
|
||||||
@@ -213,6 +232,13 @@ function compactPerformanceEventRow(row, perf) {
|
|||||||
blockingDurationMs: numberOrNull(perf.blockingDuration),
|
blockingDurationMs: numberOrNull(perf.blockingDuration),
|
||||||
startTime: numberOrNull(perf.startTime),
|
startTime: numberOrNull(perf.startTime),
|
||||||
thresholdMs: numberOrNull(perf.thresholdMs),
|
thresholdMs: numberOrNull(perf.thresholdMs),
|
||||||
|
timeOrigin: numberOrNull(perf.timeOrigin),
|
||||||
|
observedAt: numberOrNull(perf.observedAt),
|
||||||
|
startEpochMs: window.startEpochMs,
|
||||||
|
endEpochMs: window.endEpochMs,
|
||||||
|
startAt: window.startAt,
|
||||||
|
endAt: window.endAt,
|
||||||
|
windowSource: window.source,
|
||||||
path: limitText(perf.path ?? "", 160),
|
path: limitText(perf.path ?? "", 160),
|
||||||
url: safeReportUrl(perf.url),
|
url: safeReportUrl(perf.url),
|
||||||
scriptCount: Array.isArray(perf.scripts) ? perf.scripts.length : 0,
|
scriptCount: Array.isArray(perf.scripts) ? perf.scripts.length : 0,
|
||||||
@@ -237,13 +263,21 @@ function compactPerformanceDrainError(row) {
|
|||||||
function compactPerformanceCapture(row) {
|
function compactPerformanceCapture(row) {
|
||||||
const artifact = row.artifact && typeof row.artifact === "object" ? row.artifact : {};
|
const artifact = row.artifact && typeof row.artifact === "object" ? row.artifact : {};
|
||||||
const summary = row.summary && typeof row.summary === "object" ? row.summary : {};
|
const summary = row.summary && typeof row.summary === "object" ? row.summary : {};
|
||||||
|
const durationMs = numberOrNull(artifact.durationMs ?? summary.durationMs);
|
||||||
|
const endEpochMs = performanceTimestampMs(row.ts);
|
||||||
|
const startEpochMs = Number.isFinite(endEpochMs) && Number.isFinite(durationMs) ? endEpochMs - Number(durationMs) : null;
|
||||||
return {
|
return {
|
||||||
ts: row.ts ?? null,
|
ts: row.ts ?? null,
|
||||||
commandId: row.commandId ?? null,
|
commandId: row.commandId ?? null,
|
||||||
captureId: row.captureId ?? artifact.captureId ?? null,
|
captureId: row.captureId ?? artifact.captureId ?? null,
|
||||||
pageRole: row.pageRole ?? artifact.pageRole ?? null,
|
pageRole: row.pageRole ?? artifact.pageRole ?? null,
|
||||||
pageId: row.pageId ?? artifact.pageId ?? null,
|
pageId: row.pageId ?? artifact.pageId ?? null,
|
||||||
durationMs: numberOrNull(artifact.durationMs),
|
durationMs,
|
||||||
|
startEpochMs,
|
||||||
|
endEpochMs,
|
||||||
|
startAt: startEpochMs === null ? null : new Date(startEpochMs).toISOString(),
|
||||||
|
endAt: endEpochMs === null ? null : new Date(endEpochMs).toISOString(),
|
||||||
|
windowSource: startEpochMs === null ? "missing-capture-window" : "completed-event-ts-minus-artifact-duration",
|
||||||
profileTotalTimeMs: numberOrNull(summary.totalTimeMs),
|
profileTotalTimeMs: numberOrNull(summary.totalTimeMs),
|
||||||
profileSampleCount: numberOrNull(summary.sampleCount),
|
profileSampleCount: numberOrNull(summary.sampleCount),
|
||||||
path: artifact.path ?? null,
|
path: artifact.path ?? null,
|
||||||
@@ -256,13 +290,22 @@ function compactPerformanceCapture(row) {
|
|||||||
|
|
||||||
function compactLoafScript(script) {
|
function compactLoafScript(script) {
|
||||||
const value = script && typeof script === "object" ? script : {};
|
const value = script && typeof script === "object" ? script : {};
|
||||||
|
const source = sourceAttributionFor(value.sourceURL, value.sourceFunctionName, value.lineNumber, value.columnNumber, value.sourceCharPosition);
|
||||||
return {
|
return {
|
||||||
invoker: limitText(value.invoker ?? "", 160),
|
invoker: limitText(value.invoker ?? "", 160),
|
||||||
invokerType: limitText(value.invokerType ?? "", 80),
|
invokerType: limitText(value.invokerType ?? "", 80),
|
||||||
sourceURL: safeReportUrl(value.sourceURL),
|
sourceURL: safeReportUrl(value.sourceURL),
|
||||||
sourceFunctionName: limitText(value.sourceFunctionName ?? "", 160),
|
sourceFunctionName: limitText(value.sourceFunctionName ?? "", 160),
|
||||||
|
sourceFile: source.sourceFile,
|
||||||
|
sourceLine: source.sourceLine,
|
||||||
|
sourceColumn: source.sourceColumn,
|
||||||
|
sourceCharPosition: numberOrNull(value.sourceCharPosition),
|
||||||
|
sourceMapStatus: source.sourceMapStatus,
|
||||||
|
sourceAttributionMode: source.sourceAttributionMode,
|
||||||
lineNumber: numberOrNull(value.lineNumber),
|
lineNumber: numberOrNull(value.lineNumber),
|
||||||
columnNumber: numberOrNull(value.columnNumber),
|
columnNumber: numberOrNull(value.columnNumber),
|
||||||
|
startTime: numberOrNull(value.startTime),
|
||||||
|
executionStart: numberOrNull(value.executionStart),
|
||||||
durationMs: numberOrNull(value.duration),
|
durationMs: numberOrNull(value.duration),
|
||||||
forcedStyleAndLayoutDurationMs: numberOrNull(value.forcedStyleAndLayoutDuration),
|
forcedStyleAndLayoutDurationMs: numberOrNull(value.forcedStyleAndLayoutDuration),
|
||||||
pauseDurationMs: numberOrNull(value.pauseDuration),
|
pauseDurationMs: numberOrNull(value.pauseDuration),
|
||||||
@@ -272,17 +315,22 @@ function compactLoafScript(script) {
|
|||||||
|
|
||||||
function mergeLoafScript(groups, script, event) {
|
function mergeLoafScript(groups, script, event) {
|
||||||
const compact = compactLoafScript(script);
|
const compact = compactLoafScript(script);
|
||||||
const key = [compact.sourceFunctionName || compact.invoker || "(anonymous)", compact.sourceURL || "", compact.lineNumber ?? "", compact.columnNumber ?? ""].join("@");
|
const key = loafScriptKey(compact);
|
||||||
const group = groups.get(key) || { ...compact, key, count: 0, totalDurationMs: 0, maxDurationMs: 0, firstAt: event.ts, lastAt: event.ts, examples: [], valuesRedacted: true };
|
const group = groups.get(key) || { ...compact, key, count: 0, totalDurationMs: 0, maxDurationMs: 0, firstAt: event.ts, lastAt: event.ts, examples: [], valuesRedacted: true };
|
||||||
const duration = Number(compact.durationMs || 0);
|
const duration = Number(compact.durationMs || 0);
|
||||||
group.count += 1;
|
group.count += 1;
|
||||||
group.totalDurationMs += duration;
|
group.totalDurationMs += duration;
|
||||||
group.maxDurationMs = Math.max(group.maxDurationMs, duration);
|
group.maxDurationMs = Math.max(group.maxDurationMs, duration);
|
||||||
group.lastAt = event.ts;
|
group.lastAt = event.ts;
|
||||||
if (group.examples.length < 8) group.examples.push({ ts: event.ts, sampleSeq: event.sampleSeq, durationMs: event.durationMs, scriptDurationMs: compact.durationMs, pageRole: event.pageRole, valuesRedacted: true });
|
if (group.examples.length < 8) group.examples.push({ ts: event.ts, startAt: event.startAt, endAt: event.endAt, sampleSeq: event.sampleSeq, durationMs: event.durationMs, scriptDurationMs: compact.durationMs, pageRole: event.pageRole, sourceCharPosition: compact.sourceCharPosition, valuesRedacted: true });
|
||||||
groups.set(key, group);
|
groups.set(key, group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loafScriptKey(script) {
|
||||||
|
if (!script || typeof script !== "object") return "";
|
||||||
|
return [script.sourceFunctionName || script.invoker || "(anonymous)", script.sourceURL || "", script.lineNumber ?? "", script.columnNumber ?? ""].join("@");
|
||||||
|
}
|
||||||
|
|
||||||
function finalizeScriptHotspot(group) {
|
function finalizeScriptHotspot(group) {
|
||||||
return {
|
return {
|
||||||
...group,
|
...group,
|
||||||
@@ -294,8 +342,9 @@ function finalizeScriptHotspot(group) {
|
|||||||
|
|
||||||
function mergeProfileFunction(groups, fn, capture) {
|
function mergeProfileFunction(groups, fn, capture) {
|
||||||
const value = fn && typeof fn === "object" ? fn : {};
|
const value = fn && typeof fn === "object" ? fn : {};
|
||||||
|
const source = sourceAttributionFor(value.url, value.functionName, value.lineNumber, value.columnNumber, null);
|
||||||
const key = [value.functionName || "(anonymous)", value.url || value.scriptId || "", value.lineNumber ?? "", value.columnNumber ?? ""].join("@");
|
const key = [value.functionName || "(anonymous)", value.url || value.scriptId || "", value.lineNumber ?? "", value.columnNumber ?? ""].join("@");
|
||||||
const group = groups.get(key) || { functionName: value.functionName || "(anonymous)", url: safeReportUrl(value.url), scriptId: value.scriptId ?? null, lineNumber: numberOrNull(value.lineNumber), columnNumber: numberOrNull(value.columnNumber), key, captureCount: 0, sampleCount: 0, selfTimeMs: 0, totalTimeMs: 0, maxSelfTimeMs: 0, captures: [], valuesRedacted: true };
|
const group = groups.get(key) || { functionName: value.functionName || "(anonymous)", url: safeReportUrl(value.url), scriptId: value.scriptId ?? null, sourceFile: source.sourceFile, sourceLine: source.sourceLine, sourceColumn: source.sourceColumn, sourceMapStatus: source.sourceMapStatus, sourceAttributionMode: source.sourceAttributionMode, lineNumber: numberOrNull(value.lineNumber), columnNumber: numberOrNull(value.columnNumber), key, captureCount: 0, sampleCount: 0, selfTimeMs: 0, totalTimeMs: 0, maxSelfTimeMs: 0, captures: [], valuesRedacted: true };
|
||||||
const selfTime = Number(value.selfTimeMs || 0);
|
const selfTime = Number(value.selfTimeMs || 0);
|
||||||
const totalTime = Number(value.totalTimeMs || 0);
|
const totalTime = Number(value.totalTimeMs || 0);
|
||||||
group.captureCount += 1;
|
group.captureCount += 1;
|
||||||
@@ -321,7 +370,8 @@ function mergeProfileStack(groups, stack, capture) {
|
|||||||
const value = stack && typeof stack === "object" ? stack : {};
|
const value = stack && typeof stack === "object" ? stack : {};
|
||||||
const key = String(value.key || "").slice(0, 800);
|
const key = String(value.key || "").slice(0, 800);
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
const group = groups.get(key) || { key, sampleCount: 0, selfTimeMs: 0, maxSelfTimeMs: 0, leaf: value.leaf ?? null, frames: Array.isArray(value.frames) ? value.frames.slice(-10) : [], captures: [], valuesRedacted: true };
|
const frames = Array.isArray(value.frames) ? value.frames.slice(-10).map(annotateProfileFrameSource) : [];
|
||||||
|
const group = groups.get(key) || { key, sampleCount: 0, selfTimeMs: 0, maxSelfTimeMs: 0, leaf: annotateProfileFrameSource(value.leaf), frames, captures: [], valuesRedacted: true };
|
||||||
const selfTime = Number(value.selfTimeMs || 0);
|
const selfTime = Number(value.selfTimeMs || 0);
|
||||||
group.sampleCount += Number(value.sampleCount || 0);
|
group.sampleCount += Number(value.sampleCount || 0);
|
||||||
group.selfTimeMs += selfTime;
|
group.selfTimeMs += selfTime;
|
||||||
@@ -346,6 +396,328 @@ 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 }));
|
.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 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildFrontendPerformanceWindows(input) {
|
||||||
|
const aggregatedLoafHotspotKeys = new Set(
|
||||||
|
(Array.isArray(input?.scriptHotspots) ? input.scriptHotspots : [])
|
||||||
|
.filter((item) => Number(item?.totalDurationMs ?? 0) >= alertThresholds.longAnimationFrameRedMs)
|
||||||
|
.map(loafScriptKey)
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
const events = (Array.isArray(input?.events) ? input.events : [])
|
||||||
|
.filter((item) => item && (item.kind === "longtask" || item.kind === "long-animation-frame" || item.kind === "event-loop-gap"))
|
||||||
|
.filter((item) => Number(item.durationMs ?? 0) >= frontendPerformanceWindowBudget(item) || hasAggregatedLoafHotspot(item, aggregatedLoafHotspotKeys))
|
||||||
|
.sort(descDuration)
|
||||||
|
.slice(0, 20);
|
||||||
|
return events.map((event) => correlateFrontendPerformanceWindow(event, input)).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAggregatedLoafHotspot(event, keys) {
|
||||||
|
if (event?.kind !== "long-animation-frame" || !(keys instanceof Set) || keys.size === 0) return false;
|
||||||
|
for (const script of Array.isArray(event?.scripts) ? event.scripts : []) {
|
||||||
|
if (keys.has(loafScriptKey(script))) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function correlateFrontendPerformanceWindow(event, input) {
|
||||||
|
const captures = Array.isArray(input?.captures) ? input.captures : [];
|
||||||
|
const matchingCaptures = captures.filter((capture) => samePageContext(event, capture));
|
||||||
|
const overlapping = matchingCaptures.filter((capture) => windowsOverlap(event, capture));
|
||||||
|
const covering = overlapping.filter((capture) => windowCovers(capture, event));
|
||||||
|
const selectedCapture = covering[0] || overlapping[0] || nearestCapture(event, matchingCaptures) || nearestCapture(event, captures);
|
||||||
|
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 sourceHint = event.kind === "long-animation-frame" ? topLoafScriptHint(event) : null;
|
||||||
|
return {
|
||||||
|
kind: event.kind,
|
||||||
|
ts: event.ts ?? null,
|
||||||
|
startAt: event.startAt ?? null,
|
||||||
|
endAt: event.endAt ?? null,
|
||||||
|
startEpochMs: event.startEpochMs ?? null,
|
||||||
|
endEpochMs: event.endEpochMs ?? null,
|
||||||
|
durationMs: event.durationMs ?? null,
|
||||||
|
blockingDurationMs: event.blockingDurationMs ?? null,
|
||||||
|
sampleSeq: event.sampleSeq ?? null,
|
||||||
|
pageRole: event.pageRole ?? null,
|
||||||
|
pageId: event.pageId ?? null,
|
||||||
|
pageEpoch: event.pageEpoch ?? null,
|
||||||
|
path: event.path ?? null,
|
||||||
|
url: event.url ?? null,
|
||||||
|
scriptCount: event.scriptCount ?? null,
|
||||||
|
topLoafScript: sourceHint,
|
||||||
|
cpuProfileCoverageStatus: coverageStatus,
|
||||||
|
coverageReason: cpuProfileCoverageReason(event, selectedCapture, coverageStatus),
|
||||||
|
capture: selectedCapture ? compactWindowCapture(selectedCapture, event) : null,
|
||||||
|
relatedNetwork: relatedNetworkRows(event, input?.network),
|
||||||
|
relatedSamples: relatedSampleRows(event, input?.samples),
|
||||||
|
profileEvidence,
|
||||||
|
valuesRedacted: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function frontendPerformanceWindowBudget(event) {
|
||||||
|
if (event?.kind === "longtask") return alertThresholds.longTaskRedMs;
|
||||||
|
if (event?.kind === "long-animation-frame") return alertThresholds.longAnimationFrameRedMs;
|
||||||
|
if (event?.kind === "event-loop-gap") return alertThresholds.eventLoopGapRedMs;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function performanceEventWindow(row, perf) {
|
||||||
|
const timeOrigin = Number(perf?.timeOrigin);
|
||||||
|
const startTime = Number(perf?.startTime);
|
||||||
|
const duration = Number(perf?.duration);
|
||||||
|
if (Number.isFinite(timeOrigin) && timeOrigin > 0 && Number.isFinite(startTime)) {
|
||||||
|
const startEpochMs = timeOrigin + startTime;
|
||||||
|
const endEpochMs = startEpochMs + (Number.isFinite(duration) ? duration : 0);
|
||||||
|
return { startEpochMs: roundMs(startEpochMs), endEpochMs: roundMs(endEpochMs), startAt: new Date(startEpochMs).toISOString(), endAt: new Date(endEpochMs).toISOString(), source: "performance-timeOrigin-startTime" };
|
||||||
|
}
|
||||||
|
const observedAt = Number(perf?.observedAt);
|
||||||
|
if (Number.isFinite(observedAt) && observedAt > 0) {
|
||||||
|
const endEpochMs = observedAt;
|
||||||
|
const startEpochMs = endEpochMs - (Number.isFinite(duration) ? duration : 0);
|
||||||
|
return { startEpochMs: roundMs(startEpochMs), endEpochMs: roundMs(endEpochMs), startAt: new Date(startEpochMs).toISOString(), endAt: new Date(endEpochMs).toISOString(), source: "observedAt-minus-duration" };
|
||||||
|
}
|
||||||
|
const rowMs = performanceTimestampMs(row?.ts);
|
||||||
|
if (Number.isFinite(rowMs)) {
|
||||||
|
const endEpochMs = rowMs;
|
||||||
|
const startEpochMs = endEpochMs - (Number.isFinite(duration) ? duration : 0);
|
||||||
|
return { startEpochMs: roundMs(startEpochMs), endEpochMs: roundMs(endEpochMs), startAt: new Date(startEpochMs).toISOString(), endAt: new Date(endEpochMs).toISOString(), source: "row-ts-minus-duration" };
|
||||||
|
}
|
||||||
|
return { startEpochMs: null, endEpochMs: null, startAt: null, endAt: null, source: "missing-window" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function performanceTimestampMs(value) {
|
||||||
|
const ms = Date.parse(String(value || ""));
|
||||||
|
return Number.isFinite(ms) ? ms : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function samePageContext(event, capture) {
|
||||||
|
if (!capture) return false;
|
||||||
|
if (event.pageRole && capture.pageRole && event.pageRole !== capture.pageRole) return false;
|
||||||
|
if (event.pageId && capture.pageId && event.pageId !== capture.pageId) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function windowsOverlap(left, right) {
|
||||||
|
const leftStart = Number(left?.startEpochMs);
|
||||||
|
const leftEnd = Number(left?.endEpochMs);
|
||||||
|
const rightStart = Number(right?.startEpochMs);
|
||||||
|
const rightEnd = Number(right?.endEpochMs);
|
||||||
|
if (![leftStart, leftEnd, rightStart, rightEnd].every(Number.isFinite)) return false;
|
||||||
|
return leftStart <= rightEnd && rightStart <= leftEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function windowCovers(outer, inner) {
|
||||||
|
const outerStart = Number(outer?.startEpochMs);
|
||||||
|
const outerEnd = Number(outer?.endEpochMs);
|
||||||
|
const innerStart = Number(inner?.startEpochMs);
|
||||||
|
const innerEnd = Number(inner?.endEpochMs);
|
||||||
|
if (![outerStart, outerEnd, innerStart, innerEnd].every(Number.isFinite)) return false;
|
||||||
|
return outerStart <= innerStart && outerEnd >= innerEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nearestCapture(event, captures) {
|
||||||
|
const rows = Array.isArray(captures) ? captures : [];
|
||||||
|
const eventStart = Number(event?.startEpochMs);
|
||||||
|
const eventEnd = Number(event?.endEpochMs);
|
||||||
|
if (![eventStart, eventEnd].every(Number.isFinite) || rows.length === 0) return null;
|
||||||
|
return rows
|
||||||
|
.map((capture) => ({ capture, distanceMs: captureWindowDistanceMs(eventStart, eventEnd, capture) }))
|
||||||
|
.filter((item) => Number.isFinite(item.distanceMs))
|
||||||
|
.sort((left, right) => left.distanceMs - right.distanceMs)[0]?.capture || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureWindowDistanceMs(eventStart, eventEnd, capture) {
|
||||||
|
const start = Number(capture?.startEpochMs);
|
||||||
|
const end = Number(capture?.endEpochMs);
|
||||||
|
if (![start, end].every(Number.isFinite)) return Number.POSITIVE_INFINITY;
|
||||||
|
if (eventEnd < start) return start - eventEnd;
|
||||||
|
if (end < eventStart) return eventStart - end;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cpuProfileCoverageReason(event, capture, status) {
|
||||||
|
if (status === "no-cpu-profile") return "no completed performanceCapture artifact is present";
|
||||||
|
if (!capture) return "no comparable capture window is present";
|
||||||
|
if (status === "covered") return "performanceCapture window fully covers this frontend performance event";
|
||||||
|
if (status === "overlapped") return "performanceCapture window overlaps this frontend performance event, but does not fully cover it";
|
||||||
|
const eventStart = Number(event?.startEpochMs);
|
||||||
|
const eventEnd = Number(event?.endEpochMs);
|
||||||
|
const captureStart = Number(capture?.startEpochMs);
|
||||||
|
const captureEnd = Number(capture?.endEpochMs);
|
||||||
|
if (Number.isFinite(captureStart) && Number.isFinite(eventEnd) && captureStart > eventEnd) return "nearest performanceCapture started after this frontend performance window ended";
|
||||||
|
if (Number.isFinite(captureEnd) && Number.isFinite(eventStart) && captureEnd < eventStart) return "nearest performanceCapture ended before this frontend performance window started";
|
||||||
|
return "completed performanceCapture exists but its window could not be matched to this frontend event";
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactWindowCapture(capture, event) {
|
||||||
|
const distanceMs = captureWindowDistanceMs(Number(event?.startEpochMs), Number(event?.endEpochMs), capture);
|
||||||
|
return {
|
||||||
|
captureId: capture.captureId ?? null,
|
||||||
|
commandId: capture.commandId ?? null,
|
||||||
|
pageRole: capture.pageRole ?? null,
|
||||||
|
pageId: capture.pageId ?? null,
|
||||||
|
startAt: capture.startAt ?? null,
|
||||||
|
endAt: capture.endAt ?? null,
|
||||||
|
durationMs: capture.durationMs ?? null,
|
||||||
|
windowDistanceMs: Number.isFinite(distanceMs) ? roundMs(distanceMs) : null,
|
||||||
|
profileSampleCount: capture.profileSampleCount ?? null,
|
||||||
|
profileTotalTimeMs: capture.profileTotalTimeMs ?? null,
|
||||||
|
valuesRedacted: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function relatedNetworkRows(event, network) {
|
||||||
|
const rows = Array.isArray(network) ? network : [];
|
||||||
|
const start = Number(event?.startEpochMs);
|
||||||
|
const end = Number(event?.endEpochMs);
|
||||||
|
const toleranceMs = Math.max(1000, Math.min(10000, Number(event?.durationMs || 0) + 1000));
|
||||||
|
if (![start, end].every(Number.isFinite)) return [];
|
||||||
|
return rows
|
||||||
|
.filter((item) => {
|
||||||
|
const ms = performanceTimestampMs(item?.ts);
|
||||||
|
if (!Number.isFinite(ms)) return false;
|
||||||
|
return ms >= start - toleranceMs && ms <= end + toleranceMs;
|
||||||
|
})
|
||||||
|
.sort((left, right) => Math.abs(Number(performanceTimestampMs(left?.ts)) - start) - Math.abs(Number(performanceTimestampMs(right?.ts)) - start))
|
||||||
|
.slice(0, 8)
|
||||||
|
.map(compactRelatedNetworkRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compactRelatedNetworkRow(item) {
|
||||||
|
const url = item?.url ?? item?.request?.url ?? item?.response?.url ?? item?.detail?.url ?? "";
|
||||||
|
return {
|
||||||
|
ts: item?.ts ?? null,
|
||||||
|
sampleSeq: item?.sampleSeq ?? null,
|
||||||
|
phase: item?.phase ?? item?.type ?? null,
|
||||||
|
method: item?.method ?? item?.request?.method ?? null,
|
||||||
|
status: item?.status ?? item?.response?.status ?? null,
|
||||||
|
urlPath: urlPathOnly(url),
|
||||||
|
bodyByteCount: numberOrNull(item?.bodyByteCount ?? item?.response?.bodyByteCount ?? item?.bodySummary?.byteCount ?? item?.bodySummary?.bodyByteCount),
|
||||||
|
durationMs: numberOrNull(item?.durationMs ?? item?.response?.durationMs),
|
||||||
|
routeSessionId: item?.routeSessionId ?? item?.sessionId ?? null,
|
||||||
|
traceId: item?.traceId ?? item?.bodySummary?.traceId ?? null,
|
||||||
|
valuesRedacted: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function relatedSampleRows(event, samples) {
|
||||||
|
const rows = Array.isArray(samples) ? samples : [];
|
||||||
|
const start = Number(event?.startEpochMs);
|
||||||
|
const end = Number(event?.endEpochMs);
|
||||||
|
const toleranceMs = Math.max(1000, Math.min(10000, Number(event?.durationMs || 0) + 1000));
|
||||||
|
if (![start, end].every(Number.isFinite)) return [];
|
||||||
|
return rows
|
||||||
|
.filter((item) => {
|
||||||
|
const ms = performanceTimestampMs(item?.ts);
|
||||||
|
if (!Number.isFinite(ms)) return false;
|
||||||
|
return ms >= start - toleranceMs && ms <= end + toleranceMs;
|
||||||
|
})
|
||||||
|
.slice(-6)
|
||||||
|
.map((sample) => ({
|
||||||
|
ts: sample?.ts ?? null,
|
||||||
|
sampleSeq: sample?.seq ?? sample?.sampleSeq ?? null,
|
||||||
|
pageRole: sample?.pageRole ?? null,
|
||||||
|
path: limitText(sample?.path ?? "", 160),
|
||||||
|
routeSessionId: sample?.routeSessionId ?? null,
|
||||||
|
activeSessionId: sample?.activeSessionId ?? null,
|
||||||
|
traceIds: sampleTraceIdsForPerformance(sample).slice(0, 6),
|
||||||
|
valuesRedacted: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sampleTraceIdsForPerformance(sample) {
|
||||||
|
const ids = new Set();
|
||||||
|
for (const group of [sample?.messages, sample?.traceRows, sample?.turns, sample?.diagnostics]) {
|
||||||
|
if (!Array.isArray(group)) continue;
|
||||||
|
for (const item of group) if (item?.traceId) ids.add(String(item.traceId));
|
||||||
|
}
|
||||||
|
if (sample?.traceId) ids.add(String(sample.traceId));
|
||||||
|
return Array.from(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
function topLoafScriptHint(event) {
|
||||||
|
const scripts = Array.isArray(event?.scripts) ? event.scripts : [];
|
||||||
|
return scripts
|
||||||
|
.slice()
|
||||||
|
.sort((left, right) => Number(right.durationMs ?? 0) - Number(left.durationMs ?? 0))[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFrontendSourceAttribution(input) {
|
||||||
|
const sourceFiles = new Map();
|
||||||
|
const add = (row) => {
|
||||||
|
const file = row?.sourceFile || sourceFileFromUrl(row?.sourceURL || row?.url);
|
||||||
|
if (!file) return;
|
||||||
|
const existing = sourceFiles.get(file) || { sourceFile: file, count: 0, sourceMapStatus: row?.sourceMapStatus || "missing", valuesRedacted: true };
|
||||||
|
existing.count += 1;
|
||||||
|
if (existing.sourceMapStatus !== "mapped") existing.sourceMapStatus = row?.sourceMapStatus || existing.sourceMapStatus;
|
||||||
|
sourceFiles.set(file, existing);
|
||||||
|
};
|
||||||
|
for (const row of Array.isArray(input?.scriptHotspots) ? input.scriptHotspots : []) add(row);
|
||||||
|
for (const row of Array.isArray(input?.profileHotspots) ? input.profileHotspots : []) add(row);
|
||||||
|
for (const stack of Array.isArray(input?.profileStacks) ? input.profileStacks : []) {
|
||||||
|
for (const frame of Array.isArray(stack?.frames) ? stack.frames : []) add(frame);
|
||||||
|
}
|
||||||
|
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),
|
||||||
|
valuesRedacted: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function annotateProfileFrameSource(frame) {
|
||||||
|
if (!frame || typeof frame !== "object") return frame ?? null;
|
||||||
|
const source = sourceAttributionFor(frame.url, frame.functionName, frame.lineNumber, frame.columnNumber, null);
|
||||||
|
return { ...frame, sourceFile: source.sourceFile, sourceLine: source.sourceLine, sourceColumn: source.sourceColumn, sourceMapStatus: source.sourceMapStatus, sourceAttributionMode: source.sourceAttributionMode, valuesRedacted: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceAttributionFor(url, functionName, lineNumber, columnNumber, sourceCharPosition) {
|
||||||
|
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",
|
||||||
|
valuesRedacted: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceFileFromUrl(value) {
|
||||||
|
const text = String(value || "");
|
||||||
|
if (!text) return null;
|
||||||
|
try {
|
||||||
|
const parsed = new URL(text, "http://local.invalid");
|
||||||
|
const path = parsed.pathname || "";
|
||||||
|
const parts = path.split("/").filter(Boolean);
|
||||||
|
return limitText(parts[parts.length - 1] || path || text, 160);
|
||||||
|
} catch {
|
||||||
|
const clean = text.split(/[?#]/u)[0];
|
||||||
|
const parts = clean.split("/").filter(Boolean);
|
||||||
|
return limitText(parts[parts.length - 1] || clean, 160);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function urlPathOnly(value) {
|
||||||
|
const text = String(value || "");
|
||||||
|
if (!text) return null;
|
||||||
|
try {
|
||||||
|
const parsed = new URL(text, "http://local.invalid");
|
||||||
|
return limitText(parsed.pathname || "/", 180);
|
||||||
|
} catch {
|
||||||
|
return limitText(text.split("?")[0] || text, 180);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function frontendPerformanceMaxNumber(items, getter) {
|
function frontendPerformanceMaxNumber(items, getter) {
|
||||||
const selected = maxByNumber(items, getter);
|
const selected = maxByNumber(items, getter);
|
||||||
return selected ? numberOrNull(getter(selected)) : null;
|
return selected ? numberOrNull(getter(selected)) : null;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const promptNetwork = buildPromptNetworkReport(control, promptNetworkRows);
|
|||||||
const runtimeAlerts = buildRuntimeAlerts(samples, control, network, consoleEvents, errors);
|
const runtimeAlerts = buildRuntimeAlerts(samples, control, network, consoleEvents, errors);
|
||||||
const apiDomLag = buildApiDomLagReport(samples, network);
|
const apiDomLag = buildApiDomLagReport(samples, network);
|
||||||
const browserProcess = buildBrowserProcessReport(browserProcessRows);
|
const browserProcess = buildBrowserProcessReport(browserProcessRows);
|
||||||
const frontendPerformance = buildFrontendPerformanceReport(performanceRows, artifacts);
|
const frontendPerformance = buildFrontendPerformanceReport(performanceRows, artifacts, samples, network);
|
||||||
const projectManagement = buildProjectManagementReport(samples, control, network, pagePerformance, projectManagementConfig);
|
const projectManagement = buildProjectManagementReport(samples, control, network, pagePerformance, projectManagementConfig);
|
||||||
const runnerErrors = errors.slice(-8).map((item) => {
|
const runnerErrors = errors.slice(-8).map((item) => {
|
||||||
const details = item.error?.details && typeof item.error.details === "object" ? item.error.details : {};
|
const details = item.error?.details && typeof item.error.details === "object" ? item.error.details : {};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function nodeWebObserveAnalyzerWindowPageSource(): string {
|
|||||||
const runtimeAlerts = buildRuntimeAlerts(windowSamples, control, windowNetwork, windowConsole, windowErrors);
|
const runtimeAlerts = buildRuntimeAlerts(windowSamples, control, windowNetwork, windowConsole, windowErrors);
|
||||||
const apiDomLag = buildApiDomLagReport(windowSamples, windowNetwork);
|
const apiDomLag = buildApiDomLagReport(windowSamples, windowNetwork);
|
||||||
const browserProcess = buildBrowserProcessReport(windowBrowserProcessRows);
|
const browserProcess = buildBrowserProcessReport(windowBrowserProcessRows);
|
||||||
const frontendPerformance = buildFrontendPerformanceReport(windowPerformanceRows, windowArtifacts);
|
const frontendPerformance = buildFrontendPerformanceReport(windowPerformanceRows, windowArtifacts, windowSamples, windowNetwork);
|
||||||
const findings = buildFindings(windowSamples, control, windowNetwork, windowErrors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, requestRate, pageProvenance, [], {}, apiDomLag, browserProcess, frontendPerformance);
|
const findings = buildFindings(windowSamples, control, windowNetwork, windowErrors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, requestRate, pageProvenance, [], {}, apiDomLag, browserProcess, frontendPerformance);
|
||||||
return {
|
return {
|
||||||
summary: {
|
summary: {
|
||||||
|
|||||||
@@ -726,23 +726,28 @@ function targetNodeFromStateDir(){
|
|||||||
return index>=0&&parts[index+1]?parts[index+1]:null;
|
return index>=0&&parts[index+1]?parts[index+1]:null;
|
||||||
}
|
}
|
||||||
function perfFrameKey(frame){
|
function perfFrameKey(frame){
|
||||||
return {functionName:short(frame?.functionName||frame?.name||'(anonymous)',48),url:short(frame?.url||frame?.sourceURL||frame?.scriptUrl||'',72),scriptId:frame?.scriptId??null,lineNumber:frame?.lineNumber??frame?.line??null,columnNumber:frame?.columnNumber??frame?.column??null,valuesRedacted:true};
|
return {functionName:short(frame?.functionName||frame?.name||'(anonymous)',48),url:short(frame?.url||frame?.sourceURL||frame?.scriptUrl||'',72),sourceFile:short(frame?.sourceFile||'',48),sourceMapStatus:frame?.sourceMapStatus??null,scriptId:frame?.scriptId??null,lineNumber:frame?.lineNumber??frame?.line??null,columnNumber:frame?.columnNumber??frame?.column??null,valuesRedacted:true};
|
||||||
}
|
}
|
||||||
function compactProfileHotspot(item){
|
function compactProfileHotspot(item){
|
||||||
return {functionName:short(item?.functionName||item?.name||'(anonymous)',64),url:short(item?.url||item?.sourceURL||item?.scriptUrl||'',88),scriptId:item?.scriptId??null,lineNumber:item?.lineNumber??item?.line??null,columnNumber:item?.columnNumber??item?.column??null,selfTimeMs:item?.selfTimeMs??item?.selfMs??null,totalTimeMs:item?.totalTimeMs??item?.totalMs??null,hitCount:item?.hitCount??item?.sampleCount??null,captureCount:item?.captureCount??null,valuesRedacted:true};
|
return {functionName:short(item?.functionName||item?.name||'(anonymous)',64),url:short(item?.url||item?.sourceURL||item?.scriptUrl||'',88),sourceFile:short(item?.sourceFile||'',48),sourceMapStatus:item?.sourceMapStatus??null,scriptId:item?.scriptId??null,lineNumber:item?.lineNumber??item?.line??null,columnNumber:item?.columnNumber??item?.column??null,selfTimeMs:item?.selfTimeMs??item?.selfMs??null,totalTimeMs:item?.totalTimeMs??item?.totalMs??null,hitCount:item?.hitCount??item?.sampleCount??null,captureCount:item?.captureCount??null,valuesRedacted:true};
|
||||||
}
|
}
|
||||||
function compactProfileStack(item){
|
function compactProfileStack(item){
|
||||||
const frames=Array.isArray(item?.frames)?item.frames.slice(0,3).map(perfFrameKey):[];
|
const frames=Array.isArray(item?.frames)?item.frames.slice(0,3).map(perfFrameKey):[];
|
||||||
return {functionName:short(item?.functionName||item?.name||'(anonymous)',64),url:short(item?.url||item?.sourceURL||item?.scriptUrl||'',88),scriptId:item?.scriptId??null,lineNumber:item?.lineNumber??item?.line??null,columnNumber:item?.columnNumber??item?.column??null,selfTimeMs:item?.selfTimeMs??item?.selfMs??null,totalTimeMs:item?.totalTimeMs??item?.totalMs??null,hitCount:item?.hitCount??item?.sampleCount??null,frameCount:Array.isArray(item?.frames)?item.frames.length:null,frames,framesOmitted:Math.max(0,(Array.isArray(item?.frames)?item.frames.length:0)-frames.length),valuesRedacted:true};
|
return {functionName:short(item?.functionName||item?.name||'(anonymous)',64),url:short(item?.url||item?.sourceURL||item?.scriptUrl||'',88),scriptId:item?.scriptId??null,lineNumber:item?.lineNumber??item?.line??null,columnNumber:item?.columnNumber??item?.column??null,selfTimeMs:item?.selfTimeMs??item?.selfMs??null,totalTimeMs:item?.totalTimeMs??item?.totalMs??null,hitCount:item?.hitCount??item?.sampleCount??null,frameCount:Array.isArray(item?.frames)?item.frames.length:null,frames,framesOmitted:Math.max(0,(Array.isArray(item?.frames)?item.frames.length:0)-frames.length),valuesRedacted:true};
|
||||||
}
|
}
|
||||||
function compactScriptHotspot(item){
|
function compactScriptHotspot(item){
|
||||||
return {sourceFunctionName:short(item?.sourceFunctionName||item?.functionName||item?.invoker||'(anonymous)',64),sourceURL:short(item?.sourceURL||item?.url||'',88),lineNumber:item?.lineNumber??item?.line??null,columnNumber:item?.columnNumber??item?.column??null,totalDurationMs:item?.totalDurationMs??item?.durationMs??null,count:item?.count??item?.hitCount??null,invoker:short(item?.invoker||'',64),valuesRedacted:true};
|
return {sourceFunctionName:short(item?.sourceFunctionName||item?.functionName||item?.invoker||'(anonymous)',64),sourceURL:short(item?.sourceURL||item?.url||'',88),sourceFile:short(item?.sourceFile||'',48),sourceMapStatus:item?.sourceMapStatus??null,sourceCharPosition:item?.sourceCharPosition??null,lineNumber:item?.lineNumber??item?.line??null,columnNumber:item?.columnNumber??item?.column??null,totalDurationMs:item?.totalDurationMs??item?.durationMs??null,count:item?.count??item?.hitCount??null,invoker:short(item?.invoker||'',64),valuesRedacted:true};
|
||||||
}
|
}
|
||||||
function compactPerfEvent(item){
|
function compactPerfEvent(item){
|
||||||
return {ts:item?.ts??null,kind:item?.kind??null,durationMs:item?.durationMs??null,pageRole:item?.pageRole??null,pageId:item?.pageId??null,sampleSeq:item?.sampleSeq??item?.seq??null,scriptCount:item?.scriptCount??(Array.isArray(item?.scripts)?item.scripts.length:null),url:short(item?.url||item?.sourceURL||'',88),valuesRedacted:true};
|
return {ts:item?.ts??null,startAt:item?.startAt??null,endAt:item?.endAt??null,kind:item?.kind??null,durationMs:item?.durationMs??null,pageRole:item?.pageRole??null,pageId:item?.pageId??null,sampleSeq:item?.sampleSeq??item?.seq??null,scriptCount:item?.scriptCount??(Array.isArray(item?.scripts)?item.scripts.length:null),url:short(item?.url||item?.sourceURL||'',88),valuesRedacted:true};
|
||||||
}
|
}
|
||||||
function compactPerfCapture(item){
|
function compactPerfCapture(item){
|
||||||
return {ts:item?.ts??null,commandId:item?.commandId??null,status:item?.status??null,durationMs:item?.durationMs??item?.profileDurationMs??null,sampleCount:item?.sampleCount??item?.samples??null,nodeCount:item?.nodeCount??item?.nodes??null,file:short(item?.file||item?.path||item?.relativePath||'',88),valuesRedacted:true};
|
return {ts:item?.ts??null,startAt:item?.startAt??null,endAt:item?.endAt??null,captureId:item?.captureId??null,commandId:item?.commandId??null,status:item?.status??null,durationMs:item?.durationMs??item?.profileDurationMs??null,sampleCount:item?.sampleCount??item?.profileSampleCount??item?.samples??null,nodeCount:item?.nodeCount??item?.nodes??null,file:short(item?.file||item?.path||item?.relativePath||'',88),valuesRedacted:true};
|
||||||
|
}
|
||||||
|
function compactPerformanceWindow(item){
|
||||||
|
const capture=item?.capture&&typeof item.capture==='object'?item.capture:null;
|
||||||
|
const script=item?.topLoafScript&&typeof item.topLoafScript==='object'?item.topLoafScript:null;
|
||||||
|
return {kind:item?.kind??null,ts:item?.ts??null,startAt:item?.startAt??null,endAt:item?.endAt??null,durationMs:item?.durationMs??null,blockingDurationMs:item?.blockingDurationMs??null,pageRole:item?.pageRole??null,pageId:item?.pageId??null,sampleSeq:item?.sampleSeq??null,cpuProfileCoverageStatus:item?.cpuProfileCoverageStatus??null,coverageReason:short(item?.coverageReason||'',90),capture:capture?{captureId:capture.captureId??null,commandId:capture.commandId??null,startAt:capture.startAt??null,endAt:capture.endAt??null,durationMs:capture.durationMs??null,windowDistanceMs:capture.windowDistanceMs??null,profileSampleCount:capture.profileSampleCount??null,valuesRedacted:true}:null,topLoafScript:script?compactScriptHotspot(script):null,relatedNetwork:Array.isArray(item?.relatedNetwork)?item.relatedNetwork.slice(0,1).map((row)=>({ts:row?.ts??null,sampleSeq:row?.sampleSeq??null,phase:row?.phase??null,method:row?.method??null,status:row?.status??null,urlPath:short(row?.urlPath||'',56),bodyByteCount:row?.bodyByteCount??null,traceId:short(row?.traceId||'',32),routeSessionId:short(row?.routeSessionId||'',32),valuesRedacted:true})):[],relatedSamples:Array.isArray(item?.relatedSamples)?item.relatedSamples.slice(0,1).map((row)=>({ts:row?.ts??null,sampleSeq:row?.sampleSeq??null,pageRole:row?.pageRole??null,path:short(row?.path||'',40),routeSessionId:short(row?.routeSessionId||'',32),activeSessionId:short(row?.activeSessionId||'',32),traceIds:Array.isArray(row?.traceIds)?row.traceIds.slice(0,1).map((id)=>short(id,32)):[],valuesRedacted:true})):[],profileEvidence:item?.profileEvidence?{evidenceScope:item.profileEvidence.evidenceScope??null,topFunctions:Array.isArray(item.profileEvidence.topFunctions)?item.profileEvidence.topFunctions.slice(0,1).map(compactProfileHotspot):[],valuesRedacted:true}:null,valuesRedacted:true};
|
||||||
}
|
}
|
||||||
function compactPerfFinding(item){
|
function compactPerfFinding(item){
|
||||||
return {severity:item?.severity??item?.level??null,id:short(item?.id||item?.kind||item?.code||'',56),count:item?.count??item?.sampleCount??null,rootCause:item?.rootCause??item?.rootCauseStatus??null,summary:short(item?.summary||item?.message||'',110),valuesRedacted:true};
|
return {severity:item?.severity??item?.level??null,id:short(item?.id||item?.kind||item?.code||'',56),count:item?.count??item?.sampleCount??null,rootCause:item?.rootCause??item?.rootCauseStatus??null,summary:short(item?.summary||item?.message||'',110),valuesRedacted:true};
|
||||||
@@ -812,10 +817,12 @@ function performanceSummaryFromReport(){
|
|||||||
longTasks:Array.isArray(perf.longTasks)?perf.longTasks.slice(0,3).map(compactPerfEvent):[],
|
longTasks:Array.isArray(perf.longTasks)?perf.longTasks.slice(0,3).map(compactPerfEvent):[],
|
||||||
longAnimationFrames:Array.isArray(perf.longAnimationFrames)?perf.longAnimationFrames.slice(0,3).map(compactPerfEvent):[],
|
longAnimationFrames:Array.isArray(perf.longAnimationFrames)?perf.longAnimationFrames.slice(0,3).map(compactPerfEvent):[],
|
||||||
eventLoopGaps:Array.isArray(perf.eventLoopGaps)?perf.eventLoopGaps.slice(0,3).map(compactPerfEvent):[],
|
eventLoopGaps:Array.isArray(perf.eventLoopGaps)?perf.eventLoopGaps.slice(0,3).map(compactPerfEvent):[],
|
||||||
scriptHotspots:Array.isArray(perf.scriptHotspots)?perf.scriptHotspots.slice(0,5).map(compactScriptHotspot):[],
|
performanceWindows:Array.isArray(perf.performanceWindows)?perf.performanceWindows.slice(0,3).map(compactPerformanceWindow):[],
|
||||||
profileHotspots:Array.isArray(perf.profileHotspots)?perf.profileHotspots.slice(0,5).map(compactProfileHotspot):[],
|
scriptHotspots:Array.isArray(perf.scriptHotspots)?perf.scriptHotspots.slice(0,4).map(compactScriptHotspot):[],
|
||||||
profileStacks:Array.isArray(perf.profileStacks)?perf.profileStacks.slice(0,3).map(compactProfileStack):[],
|
profileHotspots:Array.isArray(perf.profileHotspots)?perf.profileHotspots.slice(0,4).map(compactProfileHotspot):[],
|
||||||
|
profileStacks:Array.isArray(perf.profileStacks)?perf.profileStacks.slice(0,2).map(compactProfileStack):[],
|
||||||
captures:captureRows.slice(-3).map(compactPerfCapture),
|
captures:captureRows.slice(-3).map(compactPerfCapture),
|
||||||
|
sourceAttribution:perf.sourceAttribution&&typeof perf.sourceAttribution==='object'?{sourceMapStatus:perf.sourceAttribution.sourceMapStatus??null,note:short(perf.sourceAttribution.note||'',120),sourceFiles:Array.isArray(perf.sourceAttribution.sourceFiles)?perf.sourceAttribution.sourceFiles.slice(0,4):[],valuesRedacted:true}:null,
|
||||||
findings,
|
findings,
|
||||||
toolFindings:performanceToolFindings(),
|
toolFindings:performanceToolFindings(),
|
||||||
valuesRedacted:true
|
valuesRedacted:true
|
||||||
@@ -840,26 +847,41 @@ function renderPerformanceSummary(perf){
|
|||||||
lines.push('pending performanceCapture: '+(pending||'-'));
|
lines.push('pending performanceCapture: '+(pending||'-'));
|
||||||
}
|
}
|
||||||
if(runner.notRunningOrFailed===true) lines.push('runner state: not-running/failed for performance evidence; treat missing CPU profile as tool state, not as proof that no CPU hotspot exists.');
|
if(runner.notRunningOrFailed===true) lines.push('runner state: not-running/failed for performance evidence; treat missing CPU profile as tool state, not as proof that no CPU hotspot exists.');
|
||||||
|
lines.push('window correlation='+String(s.windowCorrelationCount??perf.performanceWindows.length??0)+' covered='+String(s.cpuProfileWindowCoveredCount??0)+' overlapped='+String(s.cpuProfileWindowOverlappedCount??0)+' missed='+String(s.cpuProfileWindowMissedCount??0)+' missing='+String(s.cpuProfileWindowMissingCount??0)+' sourceMap='+String(s.sourceMapStatus||perf.sourceAttribution?.sourceMapStatus||'-'));
|
||||||
|
if(perf.sourceAttribution?.note) lines.push('source attribution: '+perf.sourceAttribution.note);
|
||||||
lines.push('','CPU profile hotspots');
|
lines.push('','CPU profile hotspots');
|
||||||
if(perf.profileHotspots.length===0) lines.push(mode.noCpuProfile===true?'(no CPU profile hotspots; no completed performanceCapture artifact in this run)':'-');
|
if(perf.profileHotspots.length===0) lines.push(mode.noCpuProfile===true?'(no CPU profile hotspots; no completed performanceCapture artifact in this run)':'-');
|
||||||
for(const item of perf.profileHotspots.slice(0,10)) lines.push(String(item.selfTimeMs??0)+'ms self '+short(item.functionName||'(anonymous)',44)+' '+short(item.url||item.scriptId||'-',92)+' line='+String(item.lineNumber??'-')+' captures='+String(item.captureCount??'-'));
|
for(const item of perf.profileHotspots.slice(0,4)) lines.push(String(item.selfTimeMs??0)+'ms self '+short(item.functionName||'(anonymous)',40)+' '+short(item.sourceFile||item.url||item.scriptId||'-',70)+' line='+String(item.lineNumber??'-')+' sourceMap='+String(item.sourceMapStatus||'-')+' captures='+String(item.captureCount??'-'));
|
||||||
lines.push('','CPU profile stacks');
|
lines.push('','CPU profile stacks');
|
||||||
if(perf.profileStacks.length===0) lines.push('-');
|
if(perf.profileStacks.length===0) lines.push('-');
|
||||||
for(const item of perf.profileStacks.slice(0,5)){
|
for(const item of perf.profileStacks.slice(0,2)){
|
||||||
const frames=Array.isArray(item.frames)?item.frames.slice(0,5).map((frame)=>short(frame.functionName||'(anonymous)',36)+'@'+short(frame.url||frame.scriptId||'-',54)+':'+String(frame.lineNumber??'-')).join(' <- '):'-';
|
const frames=Array.isArray(item.frames)?item.frames.slice(0,3).map((frame)=>short(frame.functionName||'(anonymous)',28)+'@'+short(frame.sourceFile||frame.url||frame.scriptId||'-',36)+':'+String(frame.lineNumber??'-')).join(' <- '):'-';
|
||||||
lines.push(String(item.selfTimeMs??0)+'ms self '+short(item.functionName||'(anonymous)',44)+' line='+String(item.lineNumber??'-')+' frames='+frames+(item.framesOmitted?(' omitted='+String(item.framesOmitted)):''));
|
lines.push(String(item.selfTimeMs??0)+'ms self '+short(item.functionName||'(anonymous)',44)+' line='+String(item.lineNumber??'-')+' frames='+frames+(item.framesOmitted?(' omitted='+String(item.framesOmitted)):''));
|
||||||
}
|
}
|
||||||
lines.push('','LoAF script hotspots');
|
lines.push('','LoAF script hotspots');
|
||||||
if(perf.scriptHotspots.length===0) lines.push('-');
|
if(perf.scriptHotspots.length===0) lines.push('-');
|
||||||
for(const item of perf.scriptHotspots.slice(0,10)) lines.push(String(item.totalDurationMs??0)+'ms total count='+String(item.count??0)+' '+short(item.sourceFunctionName||item.invoker||'(anonymous)',44)+' '+short(item.sourceURL||'-',92)+' line='+String(item.lineNumber??'-'));
|
for(const item of perf.scriptHotspots.slice(0,4)) lines.push(String(item.totalDurationMs??0)+'ms total count='+String(item.count??0)+' '+short(item.sourceFunctionName||item.invoker||'(anonymous)',40)+' '+short(item.sourceFile||item.sourceURL||'-',70)+' line='+String(item.lineNumber??'-')+' char='+String(item.sourceCharPosition??'-')+' sourceMap='+String(item.sourceMapStatus||'-'));
|
||||||
|
lines.push('','Time-window correlation');
|
||||||
|
if(!Array.isArray(perf.performanceWindows)||perf.performanceWindows.length===0) lines.push('-');
|
||||||
|
for(const item of (perf.performanceWindows||[]).slice(0,3)){
|
||||||
|
const capture=item.capture?String(item.capture.captureId||item.capture.commandId||'-')+' dist='+String(item.capture.windowDistanceMs??'-')+'ms':'-';
|
||||||
|
const script=item.topLoafScript?short(item.topLoafScript.sourceFunctionName||item.topLoafScript.invoker||'(anonymous)',40)+'@'+short(item.topLoafScript.sourceFile||item.topLoafScript.sourceURL||'-',48)+':'+String(item.topLoafScript.lineNumber??'-'):'-';
|
||||||
|
const samples=(item.relatedSamples||[]).map((row)=>'#'+String(row.sampleSeq??'-')+' session='+String(row.activeSessionId||row.routeSessionId||'-')+' trace='+String((row.traceIds||[])[0]||'-')).join('; ');
|
||||||
|
const network=(item.relatedNetwork||[]).map((row)=>String(row.status??'-')+' '+String(row.method||'-')+' '+short(row.urlPath||'-',44)+' bytes='+String(row.bodyByteCount??'-')).join('; ');
|
||||||
|
lines.push(String(item.startAt||item.ts||'-')+' '+String(item.kind||'-')+' duration='+String(item.durationMs??'-')+'ms sample='+String(item.sampleSeq??'-')+' cpuWindow='+String(item.cpuProfileCoverageStatus||'-')+' capture='+capture);
|
||||||
|
lines.push(' reason='+String(item.coverageReason||'-')+' script='+script);
|
||||||
|
if(samples) lines.push(' samples='+samples);
|
||||||
|
if(network) lines.push(' network='+network);
|
||||||
|
if(item.profileEvidence?.topFunctions?.length>0) lines.push(' same-capture topFunction='+item.profileEvidence.topFunctions.map((fn)=>String(fn.selfTimeMs??0)+'ms '+short(fn.functionName||'(anonymous)',28)+'@'+short(fn.sourceFile||fn.url||'-',32)).join('; '));
|
||||||
|
}
|
||||||
lines.push('','Longest events');
|
lines.push('','Longest events');
|
||||||
for(const item of [...perf.longTasks.slice(0,4),...perf.longAnimationFrames.slice(0,4),...perf.eventLoopGaps.slice(0,4)].sort((a,b)=>Number(b.durationMs??0)-Number(a.durationMs??0)).slice(0,10)) lines.push(String(item.ts||'-')+' '+String(item.kind||'-')+' duration='+String(item.durationMs??'-')+'ms role='+String(item.pageRole||'-')+' sample='+String(item.sampleSeq??'-')+' scripts='+String(item.scriptCount??0));
|
for(const item of [...perf.longTasks.slice(0,3),...perf.longAnimationFrames.slice(0,3),...perf.eventLoopGaps.slice(0,3)].sort((a,b)=>Number(b.durationMs??0)-Number(a.durationMs??0)).slice(0,6)) lines.push(String(item.ts||'-')+' '+String(item.kind||'-')+' duration='+String(item.durationMs??'-')+'ms role='+String(item.pageRole||'-')+' sample='+String(item.sampleSeq??'-')+' scripts='+String(item.scriptCount??0));
|
||||||
lines.push('','Findings');
|
lines.push('','Findings');
|
||||||
if(perf.findings.length===0) lines.push('-');
|
if(perf.findings.length===0) lines.push('-');
|
||||||
for(const item of perf.findings.slice(0,12)) lines.push(String(item.severity||'-')+': '+String(item.id||item.kind||'-')+' count='+String(item.count??'-')+' '+short(item.summary||item.message||'',150));
|
for(const item of perf.findings.slice(0,6)) lines.push(String(item.severity||'-')+': '+String(item.id||item.kind||'-')+' count='+String(item.count??'-')+' '+short(item.summary||item.message||'',120));
|
||||||
lines.push('','Tool status');
|
lines.push('','Tool status');
|
||||||
if(!Array.isArray(perf.toolFindings)||perf.toolFindings.length===0) lines.push('-');
|
if(!Array.isArray(perf.toolFindings)||perf.toolFindings.length===0) lines.push('-');
|
||||||
for(const item of (perf.toolFindings||[]).slice(0,10)) lines.push(String(item.severity||'-')+': '+String(item.id||'-')+' count='+String(item.count??'-')+' '+short(item.summary||'',150));
|
for(const item of (perf.toolFindings||[]).slice(0,6)) lines.push(String(item.severity||'-')+': '+String(item.id||'-')+' count='+String(item.count??'-')+' '+short(item.summary||'',120));
|
||||||
lines.push('','NEXT',' capture: bun scripts/cli.ts web-probe observe command '+(manifest.jobId||'<observer>')+' --type performanceCapture --duration-ms 5000 --wait-ms 8000',' analyze: bun scripts/cli.ts web-probe observe analyze '+(manifest.jobId||'<observer>'),'DISCLOSURE source=existing artifacts valuesRedacted=true; this view does not start browser/probe or mutate runtime.');
|
lines.push('','NEXT',' capture: bun scripts/cli.ts web-probe observe command '+(manifest.jobId||'<observer>')+' --type performanceCapture --duration-ms 5000 --wait-ms 8000',' analyze: bun scripts/cli.ts web-probe observe analyze '+(manifest.jobId||'<observer>'),'DISCLOSURE source=existing artifacts valuesRedacted=true; this view does not start browser/probe or mutate runtime.');
|
||||||
return lines.join('\\n');
|
return lines.join('\\n');
|
||||||
}
|
}
|
||||||
@@ -890,7 +912,7 @@ function renderProjectSummary(project){
|
|||||||
const rows=turnSummaryRows();
|
const rows=turnSummaryRows();
|
||||||
if(view==='performance-summary'){
|
if(view==='performance-summary'){
|
||||||
const perf=performanceSummaryFromReport();
|
const perf=performanceSummaryFromReport();
|
||||||
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,summary:perf.summary,evidenceMode:perf.evidenceMode,artifactFileCount:files.length,skippedFileCount:skippedFiles.length,renderedText:renderPerformanceSummary(perf),sourceFiles:['performance-events.jsonl','artifacts.jsonl','analysis/report.json','commands/pending/*.json','commands/processing/*.json','heartbeat.json','manifest.json'],drillDown:'bun scripts/cli.ts web-probe observe collect '+String(manifest.jobId||'<observer>')+' --view files --file analysis/report.json',valuesRedacted:true}));
|
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,summary:perf.summary,evidenceMode:perf.evidenceMode,artifactFileCount:files.length,skippedFileCount:skippedFiles.length,renderedText:renderPerformanceSummary(perf),sourceFiles:['performance-events.jsonl','network.jsonl','samples.jsonl','artifacts.jsonl','analysis/report.json','commands/pending/*.json','commands/processing/*.json','heartbeat.json','manifest.json'],drillDown:'bun scripts/cli.ts web-probe observe collect '+String(manifest.jobId||'<observer>')+' --view files --file analysis/report.json',valuesRedacted:true}));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
if(view==='project-summary'||view==='project-mdtodo-summary'){
|
if(view==='project-summary'||view==='project-mdtodo-summary'){
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ test("performance-summary labels LoAF-only evidence when CPU profile capture is
|
|||||||
scriptHotspots: [{
|
scriptHotspots: [{
|
||||||
sourceFunctionName: "Response.json.then",
|
sourceFunctionName: "Response.json.then",
|
||||||
sourceURL: "https://hwlab.example.test/app.js",
|
sourceURL: "https://hwlab.example.test/app.js",
|
||||||
|
sourceFile: "app.js",
|
||||||
|
sourceCharPosition: 123456,
|
||||||
|
sourceMapStatus: "missing",
|
||||||
totalDurationMs: 2108,
|
totalDurationMs: 2108,
|
||||||
count: 2,
|
count: 2,
|
||||||
valuesRedacted: true,
|
valuesRedacted: true,
|
||||||
@@ -99,4 +102,159 @@ test("performance-summary labels LoAF-only evidence when CPU profile capture is
|
|||||||
assert.match(text, /pending performanceCapture: cmd-perf\(pending\)/u);
|
assert.match(text, /pending performanceCapture: cmd-perf\(pending\)/u);
|
||||||
assert.match(text, /runner state: not-running\/failed/u);
|
assert.match(text, /runner state: not-running\/failed/u);
|
||||||
assert.match(text, /no CPU profile hotspots; no completed performanceCapture artifact/u);
|
assert.match(text, /no CPU profile hotspots; no completed performanceCapture artifact/u);
|
||||||
|
assert.match(text, /sourceMap=missing/u);
|
||||||
|
assert.match(text, /char=123456/u);
|
||||||
|
}, 20_000);
|
||||||
|
|
||||||
|
test("performance-summary renders CPU profile window misses and source attribution context", async () => {
|
||||||
|
const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-performance-window-"));
|
||||||
|
await mkdir(join(stateDir, "analysis"), { recursive: true });
|
||||||
|
await writeFile(join(stateDir, "manifest.json"), JSON.stringify({ jobId: "webobs-perf-window-test", status: "completed" }) + "\n");
|
||||||
|
await writeFile(join(stateDir, "heartbeat.json"), JSON.stringify({ status: "completed", updatedAt: "2026-07-02T12:08:06Z" }) + "\n");
|
||||||
|
await writeFile(join(stateDir, "performance-events.jsonl"), "");
|
||||||
|
await writeFile(join(stateDir, "analysis", "report.json"), JSON.stringify({
|
||||||
|
manifest: { status: "completed" },
|
||||||
|
heartbeat: { status: "completed", updatedAt: "2026-07-02T12:08:06Z" },
|
||||||
|
frontendPerformance: {
|
||||||
|
summary: {
|
||||||
|
eventCount: 2,
|
||||||
|
longTaskCount: 0,
|
||||||
|
longAnimationFrameCount: 1,
|
||||||
|
eventLoopGapCount: 1,
|
||||||
|
captureCount: 1,
|
||||||
|
profileHotspotCount: 1,
|
||||||
|
scriptHotspotCount: 1,
|
||||||
|
windowCorrelationCount: 1,
|
||||||
|
cpuProfileWindowCoveredCount: 0,
|
||||||
|
cpuProfileWindowOverlappedCount: 0,
|
||||||
|
cpuProfileWindowMissedCount: 1,
|
||||||
|
cpuProfileWindowMissingCount: 0,
|
||||||
|
maxLongAnimationFrameMs: 1811,
|
||||||
|
maxEventLoopGapMs: 4490,
|
||||||
|
longAnimationFrameRedMs: 200,
|
||||||
|
eventLoopGapRedMs: 1000,
|
||||||
|
cpuProfileStatus: "captured-hotspots-outside-performance-window",
|
||||||
|
attributionMode: "cpu-profile-missed-performance-window",
|
||||||
|
sourceMapStatus: "missing",
|
||||||
|
valuesRedacted: true,
|
||||||
|
},
|
||||||
|
performanceWindows: [{
|
||||||
|
kind: "long-animation-frame",
|
||||||
|
ts: "2026-07-02T12:00:00Z",
|
||||||
|
startAt: "2026-07-02T12:00:00.000Z",
|
||||||
|
endAt: "2026-07-02T12:00:01.811Z",
|
||||||
|
durationMs: 1811,
|
||||||
|
sampleSeq: 7,
|
||||||
|
pageRole: "control",
|
||||||
|
pageId: "page-1",
|
||||||
|
cpuProfileCoverageStatus: "missed",
|
||||||
|
coverageReason: "nearest performanceCapture started after this frontend performance window ended",
|
||||||
|
capture: {
|
||||||
|
captureId: "perf-late",
|
||||||
|
commandId: "cmd-late",
|
||||||
|
startAt: "2026-07-02T12:00:10.000Z",
|
||||||
|
endAt: "2026-07-02T12:00:18.158Z",
|
||||||
|
durationMs: 8158,
|
||||||
|
windowDistanceMs: 8189,
|
||||||
|
profileSampleCount: 812,
|
||||||
|
valuesRedacted: true,
|
||||||
|
},
|
||||||
|
topLoafScript: {
|
||||||
|
sourceFunctionName: "Response.json.then",
|
||||||
|
sourceURL: "https://hwlab.example.test/assets/CodeWorkbenchView.js",
|
||||||
|
sourceFile: "CodeWorkbenchView.js",
|
||||||
|
sourceCharPosition: 456789,
|
||||||
|
lineNumber: 91,
|
||||||
|
sourceMapStatus: "missing",
|
||||||
|
totalDurationMs: 1811,
|
||||||
|
count: 5,
|
||||||
|
valuesRedacted: true,
|
||||||
|
},
|
||||||
|
relatedSamples: [{
|
||||||
|
ts: "2026-07-02T12:00:01Z",
|
||||||
|
sampleSeq: 7,
|
||||||
|
pageRole: "control",
|
||||||
|
activeSessionId: "ses_test",
|
||||||
|
traceIds: ["trc_test"],
|
||||||
|
valuesRedacted: true,
|
||||||
|
}],
|
||||||
|
relatedNetwork: [{
|
||||||
|
ts: "2026-07-02T12:00:00.500Z",
|
||||||
|
sampleSeq: 7,
|
||||||
|
phase: "response",
|
||||||
|
method: "GET",
|
||||||
|
status: 200,
|
||||||
|
urlPath: "/v1/workbench/sessions/ses_test/turns",
|
||||||
|
bodyByteCount: 1048576,
|
||||||
|
traceId: "trc_test",
|
||||||
|
valuesRedacted: true,
|
||||||
|
}],
|
||||||
|
valuesRedacted: true,
|
||||||
|
}],
|
||||||
|
scriptHotspots: [{
|
||||||
|
sourceFunctionName: "Response.json.then",
|
||||||
|
sourceURL: "https://hwlab.example.test/assets/CodeWorkbenchView.js",
|
||||||
|
sourceFile: "CodeWorkbenchView.js",
|
||||||
|
sourceCharPosition: 456789,
|
||||||
|
lineNumber: 91,
|
||||||
|
sourceMapStatus: "missing",
|
||||||
|
totalDurationMs: 1811,
|
||||||
|
count: 5,
|
||||||
|
valuesRedacted: true,
|
||||||
|
}],
|
||||||
|
profileHotspots: [{
|
||||||
|
functionName: "(program)",
|
||||||
|
sourceFile: "CodeWorkbenchView.js",
|
||||||
|
sourceMapStatus: "missing",
|
||||||
|
selfTimeMs: 120,
|
||||||
|
totalTimeMs: 200,
|
||||||
|
captureCount: 1,
|
||||||
|
valuesRedacted: true,
|
||||||
|
}],
|
||||||
|
profileStacks: [],
|
||||||
|
captures: [{
|
||||||
|
captureId: "perf-late",
|
||||||
|
commandId: "cmd-late",
|
||||||
|
startAt: "2026-07-02T12:00:10.000Z",
|
||||||
|
endAt: "2026-07-02T12:00:18.158Z",
|
||||||
|
durationMs: 8158,
|
||||||
|
profileSampleCount: 812,
|
||||||
|
valuesRedacted: true,
|
||||||
|
}],
|
||||||
|
longAnimationFrames: [{ ts: "2026-07-02T12:00:00Z", startAt: "2026-07-02T12:00:00.000Z", endAt: "2026-07-02T12:00:01.811Z", kind: "long-animation-frame", durationMs: 1811, sampleSeq: 7, pageRole: "control", scriptCount: 1 }],
|
||||||
|
sourceAttribution: {
|
||||||
|
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: [{ sourceFile: "CodeWorkbenchView.js", count: 2, sourceMapStatus: "missing", valuesRedacted: true }],
|
||||||
|
valuesRedacted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
findings: [],
|
||||||
|
}) + "\n");
|
||||||
|
|
||||||
|
const script = nodeWebObserveCollectViewNodeScript({
|
||||||
|
maxFiles: 100,
|
||||||
|
view: "performance-summary",
|
||||||
|
traceId: null,
|
||||||
|
sampleSeq: null,
|
||||||
|
timestamp: null,
|
||||||
|
turn: null,
|
||||||
|
commandId: null,
|
||||||
|
windowMs: null,
|
||||||
|
});
|
||||||
|
const result = spawnSync("bash", ["-lc", `state_dir=${shellQuote(stateDir)}\n${script}`], {
|
||||||
|
cwd: join(import.meta.dir, "../../.."),
|
||||||
|
encoding: "utf8",
|
||||||
|
});
|
||||||
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||||
|
|
||||||
|
const output = JSON.parse(result.stdout);
|
||||||
|
const text = String(output.renderedText ?? "");
|
||||||
|
assert.equal(output.summary.cpuProfileWindowMissedCount, 1);
|
||||||
|
assert.match(text, /window correlation=1 covered=0 overlapped=0 missed=1 missing=0 sourceMap=missing/u);
|
||||||
|
assert.match(text, /cpuWindow=missed/u);
|
||||||
|
assert.match(text, /nearest performanceCapture started after/u);
|
||||||
|
assert.match(text, /CodeWorkbenchView\.js/u);
|
||||||
|
assert.match(text, /bytes=1048576/u);
|
||||||
|
assert.match(text, /session=ses_test trace=trc_test/u);
|
||||||
}, 20_000);
|
}, 20_000);
|
||||||
|
|||||||
Reference in New Issue
Block a user