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
+46 -3
View File
@@ -2,11 +2,11 @@
// Responsibility: Offline CLI view renderers for HWLAB web-probe observe artifacts.
import { shellQuote } from "./ssh";
export type NodeWebProbeObserveCollectView = "files" | "turn-summary" | "trace-frame" | "timeline" | "workbench-triad" | "project-summary" | "project-mdtodo-summary";
export type NodeWebProbeObserveCollectView = "files" | "turn-summary" | "trace-frame" | "timeline" | "workbench-triad" | "project-summary" | "project-mdtodo-summary" | "performance-summary";
export function parseNodeWebProbeObserveCollectView(value: string): NodeWebProbeObserveCollectView {
if (value === "files" || value === "turn-summary" || value === "trace-frame" || value === "timeline" || value === "workbench-triad" || value === "project-summary" || value === "project-mdtodo-summary") return value;
throw new Error(`web-probe observe collect --view must be files, turn-summary, trace-frame, timeline, workbench-triad, project-summary, or project-mdtodo-summary; got ${value}`);
if (value === "files" || value === "turn-summary" || value === "trace-frame" || value === "timeline" || value === "workbench-triad" || value === "project-summary" || value === "project-mdtodo-summary" || value === "performance-summary") return value;
throw new Error(`web-probe observe collect --view must be files, turn-summary, trace-frame, timeline, workbench-triad, project-summary, project-mdtodo-summary, or performance-summary; got ${value}`);
}
export function nodeWebObserveCollectViewNodeScript(input: {
@@ -44,6 +44,7 @@ const samples=readJsonl('samples.jsonl');
const control=readJsonl('control.jsonl');
const network=readJsonl('network.jsonl');
const browserProcess=readJsonl('browser-process.jsonl');
const performanceRows=readJsonl('performance-events.jsonl');
const manifest=readJson('manifest.json')||{};
const report=readJson('analysis/report.json')||{};
function unique(values){return Array.from(new Set(values.filter(Boolean)));}
@@ -723,6 +724,43 @@ function targetNodeFromStateDir(){
const index=parts.lastIndexOf('web-observe');
return index>=0&&parts[index+1]?parts[index+1]:null;
}
function performanceSummaryFromReport(){
const perf=report.frontendPerformance&&typeof report.frontendPerformance==='object'?report.frontendPerformance:{};
const summary=perf.summary&&typeof perf.summary==='object'?perf.summary:{};
const findings=Array.isArray(report.findings)?report.findings.filter((item)=>String(item?.id||item?.kind||'').match(/^frontend-(?:long|event-loop|cpu-profile|performance)/u)).slice(0,20):[];
const captureRows=Array.isArray(perf.captures)?perf.captures:[];
return {
summary:{...summary, rawPerformanceRowCount:performanceRows.length, valuesRedacted:true},
longTasks:Array.isArray(perf.longTasks)?perf.longTasks.slice(0,12):[],
longAnimationFrames:Array.isArray(perf.longAnimationFrames)?perf.longAnimationFrames.slice(0,12):[],
eventLoopGaps:Array.isArray(perf.eventLoopGaps)?perf.eventLoopGaps.slice(0,12):[],
scriptHotspots:Array.isArray(perf.scriptHotspots)?perf.scriptHotspots.slice(0,12):[],
profileHotspots:Array.isArray(perf.profileHotspots)?perf.profileHotspots.slice(0,12):[],
profileStacks:Array.isArray(perf.profileStacks)?perf.profileStacks.slice(0,8):[],
captures:captureRows.slice(-12),
findings,
valuesRedacted:true
};
}
function renderPerformanceSummary(perf){
const s=perf.summary||{};
const lines=['WEB-PROBE frontend performance '+(manifest.jobId||'-'),'======================================================='];
lines.push('events='+String(s.eventCount??0)+' rawRows='+String(s.rawPerformanceRowCount??0)+' longTask='+String(s.longTaskCount??0)+' loaf='+String(s.longAnimationFrameCount??0)+' eventLoopGap='+String(s.eventLoopGapCount??0)+' captures='+String(s.captureCount??0));
lines.push('max longTask='+String(s.maxLongTaskMs??'-')+'ms budget='+String(s.longTaskRedMs??'-')+'ms; max LoAF='+String(s.maxLongAnimationFrameMs??'-')+'ms budget='+String(s.longAnimationFrameRedMs??'-')+'ms; max gap='+String(s.maxEventLoopGapMs??'-')+'ms budget='+String(s.eventLoopGapRedMs??'-')+'ms');
lines.push('','CPU profile hotspots');
if(perf.profileHotspots.length===0) lines.push('-');
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??'-'));
lines.push('','LoAF script hotspots');
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??'-'));
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));
lines.push('','Findings');
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));
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');
}
function renderProjectSummary(project){
const s=project.summary||{};
const lines=['Project MDTODO observer '+(manifest.jobId||'-'),'=======================================================','enabled='+String(s.enabled===true)+' samples='+String(s.projectSampleCount??0)+' mdtodo='+String(s.mdtodoSampleCount??0)+' latest='+String(s.latestPageKind||'-')+' path='+String(s.latestPath||'-'),'counts source='+String(s.latestSourceCount??'-')+' file='+String(s.latestFileCount??'-')+' task='+String(s.latestTaskCount??'-')+' selectedTask='+String(s.latestSelectedTaskRefHash||'-'),'fileLabel='+short(s.latestSelectedFileLabelPreview||'-',80)+' direct='+String(s.latestSelectedFileLabelLooksDirect??'-')+' bodyVisibleSamples='+String(s.selectedTaskBodyVisibleSamples??'-')+' reportPreviewSamples='+String(s.reportPreviewVisibleSamples??'-')+' reportFullscreenSamples='+String(s.reportFullscreenVisibleSamples??'-'),'paneGap actionable='+String(s.severePaneGapSampleCount??0)+' ignoredInitialEmptyDetail='+String(s.ignoredPaneGapSampleCount??0),'commands='+String(s.projectCommandCount??0)+' mutations='+String(s.mutationCommandCount??0)+' mutationFailures='+String(s.mutationFailureCount??0),'launch commands='+String(s.launchCommandCount??0)+' success='+String(s.launchSuccessCount??0)+' failure='+String(s.launchFailureCount??0)+' nonEmpty='+String(s.launchNonEmptyCount??'-')+' empty='+String(s.launchEmptyCount??'-')+' otelTraceHeader='+String(s.launchWithOtelTraceHeaderCount??0),'api responses='+String(s.projectApiResponseCount??'-')+' failures='+String(s.projectApiFailureCount??'-')+'/'+String(s.projectApiRequestFailedCount??'-')+' slowPaths='+String(s.projectApiSlowPathCount??'-'),'','Recent samples'];
@@ -748,6 +786,11 @@ function renderProjectSummary(project){
return lines.join('\\n');
}
const rows=turnSummaryRows();
if(view==='performance-summary'){
const perf=performanceSummaryFromReport();
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,summary:perf.summary,profileHotspots:perf.profileHotspots,profileStacks:perf.profileStacks,scriptHotspots:perf.scriptHotspots,longTasks:perf.longTasks,longAnimationFrames:perf.longAnimationFrames,eventLoopGaps:perf.eventLoopGaps,captures:perf.captures,findings:perf.findings,artifactFileCount:files.length,skippedFileCount:skippedFiles.length,skippedFiles:skippedFiles.slice(0,20),renderedText:renderPerformanceSummary(perf),sourceFiles:['performance-events.jsonl','artifacts.jsonl','analysis/report.json'],valuesRedacted:true}));
process.exit(0);
}
if(view==='project-summary'||view==='project-mdtodo-summary'){
const project=projectSummaryFromSamples();
const projectSummary={...project.summary,latestSelectedTaskRefPreview:short(project.summary?.latestSelectedTaskRefPreview,80)};