5503560d68
- Added interactive `/debug` command with menu-driven interface for bug report triage and system diagnostics. - Implemented CPU profiling workflow allowing users to capture performance profiles during issue reproduction. - Added heap snapshot generation for memory issue analysis and debugging. - Implemented debug report bundle creation with session data, logs, system info, and optional profiling artifacts. - Added system information collection and display including OS, CPU, memory, and version details. - Implemented artifact cache management with size statistics and cleanup functionality.
159 lines
4.0 KiB
TypeScript
159 lines
4.0 KiB
TypeScript
/**
|
|
* CPU and heap profiling wrappers for debug reports.
|
|
*/
|
|
|
|
export interface CpuProfile {
|
|
data: string;
|
|
markdown: string;
|
|
}
|
|
|
|
export interface ProfilerSession {
|
|
/** Stop profiling and return the profile data */
|
|
stop(): Promise<CpuProfile>;
|
|
}
|
|
|
|
/** V8 CPU Profile node structure */
|
|
interface CpuProfileNode {
|
|
id: number;
|
|
callFrame?: {
|
|
functionName?: string;
|
|
url?: string;
|
|
lineNumber?: number;
|
|
};
|
|
hitCount?: number;
|
|
children?: number[];
|
|
}
|
|
|
|
/** V8 CPU Profile structure */
|
|
interface CpuProfileData {
|
|
nodes?: CpuProfileNode[];
|
|
samples?: number[];
|
|
timeDeltas?: number[];
|
|
startTime?: number;
|
|
endTime?: number;
|
|
}
|
|
|
|
/**
|
|
* Format CPU profile data as markdown for LLM analysis.
|
|
* Extracts top functions by self time and call counts.
|
|
*/
|
|
function formatProfileAsMarkdown(profileJson: string): string {
|
|
try {
|
|
const profile = JSON.parse(profileJson) as CpuProfileData;
|
|
const nodes = profile.nodes ?? [];
|
|
|
|
interface NodeInfo {
|
|
id: number;
|
|
functionName: string;
|
|
url: string;
|
|
lineNumber: number;
|
|
selfTime: number;
|
|
hitCount: number;
|
|
}
|
|
|
|
const nodeMap = new Map<number, NodeInfo>();
|
|
for (const node of nodes) {
|
|
nodeMap.set(node.id, {
|
|
id: node.id,
|
|
functionName: node.callFrame?.functionName ?? "(anonymous)",
|
|
url: node.callFrame?.url ?? "",
|
|
lineNumber: node.callFrame?.lineNumber ?? 0,
|
|
selfTime: 0,
|
|
hitCount: node.hitCount ?? 0,
|
|
});
|
|
}
|
|
|
|
// Distribute sample times to nodes
|
|
const samples = profile.samples ?? [];
|
|
const timeDeltas = profile.timeDeltas ?? [];
|
|
for (let i = 0; i < samples.length; i++) {
|
|
const nodeId = samples[i];
|
|
const info = nodeId !== undefined ? nodeMap.get(nodeId) : undefined;
|
|
const delta = timeDeltas[i] ?? 0;
|
|
if (info) {
|
|
info.selfTime += delta;
|
|
}
|
|
}
|
|
|
|
// Sort by self time and get top functions
|
|
const sorted = Array.from(nodeMap.values())
|
|
.filter(n => n.selfTime > 0 && n.functionName !== "(root)" && n.functionName !== "(idle)")
|
|
.sort((a, b) => b.selfTime - a.selfTime)
|
|
.slice(0, 30);
|
|
|
|
if (sorted.length === 0) {
|
|
return "# CPU Profile Summary\n\nNo significant CPU activity recorded.";
|
|
}
|
|
|
|
const totalTime = sorted.reduce((sum, n) => sum + n.selfTime, 0);
|
|
|
|
const lines = ["# CPU Profile Summary", ""];
|
|
lines.push(`Total profiled time: ${(totalTime / 1000).toFixed(1)}ms`);
|
|
lines.push("");
|
|
lines.push("## Top Functions by Self Time");
|
|
lines.push("");
|
|
lines.push("| Function | Self Time (ms) | % | Location |");
|
|
lines.push("|----------|----------------|---|----------|");
|
|
|
|
for (const node of sorted) {
|
|
const selfMs = (node.selfTime / 1000).toFixed(1);
|
|
const pct = ((node.selfTime / totalTime) * 100).toFixed(1);
|
|
const location = node.url ? `${node.url}:${node.lineNumber}` : "-";
|
|
lines.push(`| ${node.functionName} | ${selfMs} | ${pct}% | ${location} |`);
|
|
}
|
|
|
|
return lines.join("\n");
|
|
} catch {
|
|
return "# CPU Profile Summary\n\nFailed to parse profile data.";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start CPU profiling.
|
|
* Returns a session that can be stopped to get the profile data.
|
|
*/
|
|
export async function startCpuProfile(): Promise<ProfilerSession> {
|
|
const v8 = await import("node:v8");
|
|
v8.setFlagsFromString("--allow-natives-syntax");
|
|
|
|
const { Session } = await import("node:inspector/promises");
|
|
const session = new Session();
|
|
session.connect();
|
|
|
|
await session.post("Profiler.enable");
|
|
await session.post("Profiler.start");
|
|
|
|
return {
|
|
async stop(): Promise<CpuProfile> {
|
|
const result = await session.post("Profiler.stop");
|
|
await session.post("Profiler.disable");
|
|
session.disconnect();
|
|
|
|
const data = JSON.stringify(result.profile, null, 2);
|
|
const markdown = formatProfileAsMarkdown(data);
|
|
|
|
return { data, markdown };
|
|
},
|
|
};
|
|
}
|
|
|
|
export interface HeapSnapshot {
|
|
data: string;
|
|
}
|
|
|
|
/**
|
|
* Generate a heap snapshot.
|
|
* Uses Bun's built-in generateHeapSnapshot.
|
|
*/
|
|
export function generateHeapSnapshotData(): HeapSnapshot {
|
|
// Force GC before snapshot
|
|
Bun.gc(true);
|
|
|
|
// Use V8 format for Chrome DevTools compatibility
|
|
const snapshot = Bun.generateHeapSnapshot("v8");
|
|
|
|
return {
|
|
data: snapshot,
|
|
};
|
|
}
|