feat: add web probe performance hotspot analysis

This commit is contained in:
Codex
2026-07-02 07:10:21 +00:00
parent bcaff08ad7
commit 88024dd251
30 changed files with 12674 additions and 11614 deletions
@@ -0,0 +1,303 @@
// SPEC: pikasTech/unidesk#1436 WEB-PROBE performance analyzer.
// Responsibility: Source string for offline frontend long-task/LoAF/profile hotspot analysis.
export function nodeWebObserveAnalyzerPerformanceSource(): string {
return String.raw`
function buildFrontendPerformanceReport(rows, artifacts) {
const sourceRows = Array.isArray(rows) ? rows : [];
const events = [];
const drainErrors = [];
const captures = [];
const scriptGroups = new Map();
const profileFunctionGroups = new Map();
const profileStackGroups = new Map();
for (const row of sourceRows) {
if (!row || typeof row !== "object") continue;
if (row.type === "performance-drain-error") {
drainErrors.push(compactPerformanceDrainError(row));
continue;
}
if (row.type === "performance-capture-completed") {
const capture = compactPerformanceCapture(row);
captures.push(capture);
for (const fn of Array.isArray(row.summary?.topFunctions) ? row.summary.topFunctions : []) mergeProfileFunction(profileFunctionGroups, fn, capture);
for (const stack of Array.isArray(row.summary?.topStacks) ? row.summary.topStacks : []) mergeProfileStack(profileStackGroups, stack, capture);
continue;
}
if (row.type !== "performance-event") continue;
const perf = row.performance && typeof row.performance === "object" ? row.performance : {};
const event = compactPerformanceEventRow(row, perf);
if (!event.kind) continue;
events.push(event);
for (const script of Array.isArray(perf.scripts) ? perf.scripts : []) mergeLoafScript(scriptGroups, script, event);
}
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");
const scriptHotspots = Array.from(scriptGroups.values())
.map(finalizeScriptHotspot)
.sort((left, right) => Number(right.totalDurationMs ?? 0) - Number(left.totalDurationMs ?? 0) || Number(right.count ?? 0) - Number(left.count ?? 0))
.slice(0, 40);
const profileHotspots = Array.from(profileFunctionGroups.values())
.map(finalizeProfileFunctionHotspot)
.sort((left, right) => Number(right.selfTimeMs ?? 0) - Number(left.selfTimeMs ?? 0) || Number(right.totalTimeMs ?? 0) - Number(left.totalTimeMs ?? 0))
.slice(0, 40);
const profileStacks = Array.from(profileStackGroups.values())
.map(finalizeProfileStackHotspot)
.sort((left, right) => Number(right.selfTimeMs ?? 0) - Number(left.selfTimeMs ?? 0))
.slice(0, 30);
return {
summary: {
rowCount: sourceRows.length,
eventCount: events.length,
longTaskCount: longTasks.length,
longAnimationFrameCount: loafs.length,
eventLoopGapCount: gaps.length,
drainErrorCount: drainErrors.length,
captureCount: captures.length,
profileHotspotCount: profileHotspots.length,
scriptHotspotCount: scriptHotspots.length,
longTaskRedMs: alertThresholds.longTaskRedMs,
longAnimationFrameRedMs: alertThresholds.longAnimationFrameRedMs,
eventLoopGapRedMs: alertThresholds.eventLoopGapRedMs,
maxLongTaskMs: frontendPerformanceMaxNumber(longTasks, (item) => item.durationMs),
maxLongAnimationFrameMs: frontendPerformanceMaxNumber(loafs, (item) => item.durationMs),
maxEventLoopGapMs: frontendPerformanceMaxNumber(gaps, (item) => item.durationMs),
captureArtifacts: performanceCaptureArtifacts(artifacts),
valuesRedacted: true,
},
longTasks: longTasks.sort(descDuration).slice(0, 40),
longAnimationFrames: loafs.sort(descDuration).slice(0, 40),
eventLoopGaps: gaps.sort(descDuration).slice(0, 40),
scriptHotspots,
profileHotspots,
profileStacks,
captures: captures.slice(-20),
drainErrors: drainErrors.slice(-20),
valuesRedacted: true,
};
}
function buildFrontendPerformanceFindings(report) {
const findings = [];
const summary = report?.summary || {};
const severeLongTasks = (report?.longTasks || []).filter((item) => Number(item.durationMs ?? 0) >= alertThresholds.longTaskRedMs);
if (severeLongTasks.length > 0) findings.push({
id: "frontend-longtask-red",
severity: "red",
summary: "PerformanceObserver captured long tasks over YAML budget; use LoAF scripts or CPU profile hotspots for function attribution",
count: severeLongTasks.length,
budgetMs: alertThresholds.longTaskRedMs,
maxDurationMs: summary.maxLongTaskMs,
events: severeLongTasks.slice(0, 20),
topScripts: (report?.scriptHotspots || []).slice(0, 8),
topProfileFunctions: (report?.profileHotspots || []).slice(0, 8),
valuesRedacted: true,
});
const severeLoafs = (report?.longAnimationFrames || []).filter((item) => Number(item.durationMs ?? 0) >= alertThresholds.longAnimationFrameRedMs || Number(item.blockingDurationMs ?? 0) >= alertThresholds.longAnimationFrameRedMs);
if (severeLoafs.length > 0) findings.push({
id: "frontend-long-animation-frame-red",
severity: "red",
summary: "Long Animation Frame entries exceeded YAML budget and include script attribution when Chromium exposes it",
count: severeLoafs.length,
budgetMs: alertThresholds.longAnimationFrameRedMs,
maxDurationMs: summary.maxLongAnimationFrameMs,
events: severeLoafs.slice(0, 20),
topScripts: (report?.scriptHotspots || []).slice(0, 12),
valuesRedacted: true,
});
const severeGaps = (report?.eventLoopGaps || []).filter((item) => Number(item.durationMs ?? 0) >= alertThresholds.eventLoopGapRedMs);
if (severeGaps.length > 0) findings.push({
id: "frontend-event-loop-gap-red",
severity: "red",
summary: "web-probe page-side event loop gap probe observed main-thread stalls over YAML budget",
count: severeGaps.length,
budgetMs: alertThresholds.eventLoopGapRedMs,
maxDurationMs: summary.maxEventLoopGapMs,
events: severeGaps.slice(0, 20),
valuesRedacted: true,
});
if ((report?.profileHotspots || []).length > 0) findings.push({
id: "frontend-cpu-profile-hotspots",
severity: severeLongTasks.length > 0 || severeLoafs.length > 0 || severeGaps.length > 0 ? "red" : "info",
summary: "CDP CPU profile capture produced function-level hotspots for offline attribution",
count: report.profileHotspots.length,
captures: (report?.captures || []).slice(-8),
topFunctions: report.profileHotspots.slice(0, 12),
topStacks: (report?.profileStacks || []).slice(0, 8),
valuesRedacted: true,
});
if ((report?.drainErrors || []).length > 0) findings.push({
id: "frontend-performance-probe-drain-errors",
severity: "amber",
summary: "page-side performance probe had drain errors; improve probe visibility before relying on absence of long-task events",
count: report.drainErrors.length,
errors: report.drainErrors.slice(0, 12),
valuesRedacted: true,
});
return findings;
}
function compactPerformanceEventRow(row, perf) {
return {
ts: row.ts ?? null,
seq: row.seq ?? null,
sampleSeq: row.sampleSeq ?? null,
sampleGroupSeq: row.sampleGroupSeq ?? null,
commandId: row.commandId ?? null,
pageRole: row.pageRole ?? null,
pageId: row.pageId ?? null,
pageEpoch: numberOrNull(row.pageEpoch),
kind: perf.kind ?? null,
name: limitText(perf.name ?? "", 120),
durationMs: numberOrNull(perf.duration),
blockingDurationMs: numberOrNull(perf.blockingDuration),
startTime: numberOrNull(perf.startTime),
thresholdMs: numberOrNull(perf.thresholdMs),
path: limitText(perf.path ?? "", 160),
url: safeReportUrl(perf.url),
scriptCount: Array.isArray(perf.scripts) ? perf.scripts.length : 0,
attributionCount: Array.isArray(perf.attribution) ? perf.attribution.length : 0,
scripts: (Array.isArray(perf.scripts) ? perf.scripts : []).slice(0, 8).map(compactLoafScript),
valuesRedacted: true,
};
}
function compactPerformanceDrainError(row) {
return {
ts: row?.ts ?? null,
sampleSeq: row?.sampleSeq ?? null,
pageRole: row?.pageRole ?? null,
pageId: row?.pageId ?? null,
reason: row?.drain?.reason ?? row?.reason ?? null,
message: limitText(row?.drain?.error?.message ?? row?.error?.message ?? "", 200),
valuesRedacted: true,
};
}
function compactPerformanceCapture(row) {
const artifact = row.artifact && typeof row.artifact === "object" ? row.artifact : {};
const summary = row.summary && typeof row.summary === "object" ? row.summary : {};
return {
ts: row.ts ?? null,
commandId: row.commandId ?? null,
captureId: row.captureId ?? artifact.captureId ?? null,
pageRole: row.pageRole ?? artifact.pageRole ?? null,
pageId: row.pageId ?? artifact.pageId ?? null,
durationMs: numberOrNull(artifact.durationMs),
profileTotalTimeMs: numberOrNull(summary.totalTimeMs),
profileSampleCount: numberOrNull(summary.sampleCount),
path: artifact.path ?? null,
summaryPath: artifact.summaryPath ?? null,
sha256: artifact.sha256 ?? null,
summarySha256: artifact.summarySha256 ?? null,
valuesRedacted: true,
};
}
function compactLoafScript(script) {
const value = script && typeof script === "object" ? script : {};
return {
invoker: limitText(value.invoker ?? "", 160),
invokerType: limitText(value.invokerType ?? "", 80),
sourceURL: safeReportUrl(value.sourceURL),
sourceFunctionName: limitText(value.sourceFunctionName ?? "", 160),
lineNumber: numberOrNull(value.lineNumber),
columnNumber: numberOrNull(value.columnNumber),
durationMs: numberOrNull(value.duration),
forcedStyleAndLayoutDurationMs: numberOrNull(value.forcedStyleAndLayoutDuration),
pauseDurationMs: numberOrNull(value.pauseDuration),
valuesRedacted: true,
};
}
function mergeLoafScript(groups, script, event) {
const compact = compactLoafScript(script);
const key = [compact.sourceFunctionName || compact.invoker || "(anonymous)", compact.sourceURL || "", compact.lineNumber ?? "", compact.columnNumber ?? ""].join("@");
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);
group.count += 1;
group.totalDurationMs += duration;
group.maxDurationMs = Math.max(group.maxDurationMs, duration);
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 });
groups.set(key, group);
}
function finalizeScriptHotspot(group) {
return {
...group,
totalDurationMs: roundMs(group.totalDurationMs),
maxDurationMs: roundMs(group.maxDurationMs),
valuesRedacted: true,
};
}
function mergeProfileFunction(groups, fn, capture) {
const value = fn && typeof fn === "object" ? fn : {};
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 selfTime = Number(value.selfTimeMs || 0);
const totalTime = Number(value.totalTimeMs || 0);
group.captureCount += 1;
group.sampleCount += Number(value.sampleCount || 0);
group.selfTimeMs += selfTime;
group.totalTimeMs += totalTime;
group.maxSelfTimeMs = Math.max(group.maxSelfTimeMs, selfTime);
if (group.captures.length < 8) group.captures.push({ captureId: capture.captureId, commandId: capture.commandId, selfTimeMs: roundMs(selfTime), totalTimeMs: roundMs(totalTime), valuesRedacted: true });
groups.set(key, group);
}
function finalizeProfileFunctionHotspot(group) {
return {
...group,
selfTimeMs: roundMs(group.selfTimeMs),
totalTimeMs: roundMs(group.totalTimeMs),
maxSelfTimeMs: roundMs(group.maxSelfTimeMs),
valuesRedacted: true,
};
}
function mergeProfileStack(groups, stack, capture) {
const value = stack && typeof stack === "object" ? stack : {};
const key = String(value.key || "").slice(0, 800);
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 selfTime = Number(value.selfTimeMs || 0);
group.sampleCount += Number(value.sampleCount || 0);
group.selfTimeMs += selfTime;
group.maxSelfTimeMs = Math.max(group.maxSelfTimeMs, selfTime);
if (group.captures.length < 6) group.captures.push({ captureId: capture.captureId, commandId: capture.commandId, selfTimeMs: roundMs(selfTime), valuesRedacted: true });
groups.set(key, group);
}
function finalizeProfileStackHotspot(group) {
return {
...group,
selfTimeMs: roundMs(group.selfTimeMs),
maxSelfTimeMs: roundMs(group.maxSelfTimeMs),
valuesRedacted: true,
};
}
function performanceCaptureArtifacts(artifacts) {
return (Array.isArray(artifacts) ? artifacts : [])
.filter((item) => item && item.kind === "performance-cpu-profile")
.slice(-20)
.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 frontendPerformanceMaxNumber(items, getter) {
const selected = maxByNumber(items, getter);
return selected ? numberOrNull(getter(selected)) : null;
}
function descDuration(left, right) {
return Number(right.durationMs ?? 0) - Number(left.durationMs ?? 0) || String(right.ts || "").localeCompare(String(left.ts || ""));
}
function roundMs(value) {
return Number((Number(value || 0)).toFixed(2));
}
`;
}