feat(work): added work scheduling profiler with circular buffer sampling and flamegraph visualization
- Added work scheduling profiler with always-on circular buffer sampling to track CPU scheduling patterns and task execution times. - Added `getWorkProfile()` native function to retrieve profiling data including folded stacks, markdown summary, SVG flamegraph, and metrics for the last N seconds. - Added work profile debug menu item to visualize CPU scheduling patterns via flamegraph in the coding agent. - Added work profile support to report bundles with folded stacks, summary, and flamegraph visualization. - Updated `launch_blocking()` and `launch_async()` functions to accept task name tags for profiling instrumentation. - Added dependencies on inferno, smallvec, and heapless crates for profiling infrastructure.
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
* Provides tools for debugging, bug report generation, and system diagnostics.
|
||||
*/
|
||||
import * as fs from "node:fs/promises";
|
||||
import { getWorkProfile } from "@oh-my-pi/pi-natives/work";
|
||||
import { Container, Loader, type SelectItem, SelectList, Spacer, Text } from "@oh-my-pi/pi-tui";
|
||||
import { getSessionsDir } from "../config";
|
||||
import { DynamicBorder } from "../modes/components/dynamic-border";
|
||||
@@ -17,6 +18,7 @@ import { collectSystemInfo, formatSystemInfo } from "./system-info";
|
||||
const DEBUG_MENU_ITEMS: SelectItem[] = [
|
||||
{ value: "open-artifacts", label: "Open: artifact folder", description: "Open session artifacts in file manager" },
|
||||
{ value: "performance", label: "Report: performance issue", description: "Profile CPU, reproduce, then bundle" },
|
||||
{ value: "work", label: "Profile: work scheduling", description: "Open flamegraph of last 30s" },
|
||||
{ value: "dump", label: "Report: dump session", description: "Create report bundle immediately" },
|
||||
{ value: "memory", label: "Report: memory issue", description: "Heap snapshot + bundle" },
|
||||
{ value: "logs", label: "View: recent logs", description: "Show last 50 log entries" },
|
||||
@@ -69,6 +71,9 @@ export class DebugSelectorComponent extends Container {
|
||||
case "performance":
|
||||
await this.handlePerformanceReport();
|
||||
break;
|
||||
case "work":
|
||||
await this.handleWorkReport();
|
||||
break;
|
||||
case "dump":
|
||||
await this.handleDumpReport();
|
||||
break;
|
||||
@@ -162,6 +167,39 @@ export class DebugSelectorComponent extends Container {
|
||||
this.ctx.ui.requestRender();
|
||||
}
|
||||
|
||||
private async handleWorkReport(): Promise<void> {
|
||||
try {
|
||||
const workProfile = getWorkProfile(30);
|
||||
|
||||
if (!workProfile.svg) {
|
||||
this.ctx.showWarning(`No work profile data (${workProfile.sampleCount} samples)`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write SVG to temp file and open in browser
|
||||
const tmpPath = `/tmp/work-profile-${Date.now()}.svg`;
|
||||
await Bun.write(tmpPath, workProfile.svg);
|
||||
|
||||
const openCmd =
|
||||
process.platform === "darwin"
|
||||
? ["open", tmpPath]
|
||||
: process.platform === "win32"
|
||||
? ["cmd", "/c", "start", "", tmpPath]
|
||||
: ["xdg-open", tmpPath];
|
||||
|
||||
Bun.spawn(openCmd, { stdout: "ignore", stderr: "ignore" }).unref();
|
||||
|
||||
this.ctx.chatContainer.addChild(new Spacer(1));
|
||||
this.ctx.chatContainer.addChild(
|
||||
new Text(theme.fg("dim", `Opened flamegraph (${workProfile.sampleCount} samples)`), 1, 0),
|
||||
);
|
||||
} catch (err) {
|
||||
this.ctx.showError(`Failed to open profile: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
|
||||
this.ctx.ui.requestRender();
|
||||
}
|
||||
|
||||
private async handleDumpReport(): Promise<void> {
|
||||
const loader = new Loader(
|
||||
this.ctx.ui,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { WorkProfile } from "@oh-my-pi/pi-natives/work";
|
||||
import { isEnoent } from "@oh-my-pi/pi-utils";
|
||||
import type { CpuProfile, HeapSnapshot } from "./profiler";
|
||||
import { collectSystemInfo, sanitizeEnv } from "./system-info";
|
||||
@@ -42,6 +43,8 @@ export interface ReportBundleOptions {
|
||||
cpuProfile?: CpuProfile;
|
||||
/** Heap snapshot (for memory reports) */
|
||||
heapSnapshot?: HeapSnapshot;
|
||||
/** Work profile (for work scheduling reports) */
|
||||
workProfile?: WorkProfile;
|
||||
}
|
||||
|
||||
export interface ReportBundleResult {
|
||||
@@ -63,6 +66,9 @@ export interface ReportBundleResult {
|
||||
* - profile.cpuprofile: CPU profile (performance report only)
|
||||
* - profile.md: Markdown CPU profile (performance report only)
|
||||
* - heap.heapsnapshot: Heap snapshot (memory report only)
|
||||
* - work.folded: Work profile folded stacks (work report only)
|
||||
* - work.md: Work profile summary (work report only)
|
||||
* - work.svg: Work profile flamegraph (work report only)
|
||||
*/
|
||||
export async function createReportBundle(options: ReportBundleOptions): Promise<ReportBundleResult> {
|
||||
const reportsDir = getReportsDir();
|
||||
@@ -131,6 +137,18 @@ export async function createReportBundle(options: ReportBundleOptions): Promise<
|
||||
files.push("heap.heapsnapshot");
|
||||
}
|
||||
|
||||
// Work profile
|
||||
if (options.workProfile) {
|
||||
data["work.folded"] = options.workProfile.folded;
|
||||
files.push("work.folded");
|
||||
data["work.md"] = options.workProfile.summary;
|
||||
files.push("work.md");
|
||||
if (options.workProfile.svg) {
|
||||
data["work.svg"] = options.workProfile.svg;
|
||||
files.push("work.svg");
|
||||
}
|
||||
}
|
||||
|
||||
// Write archive
|
||||
await Bun.Archive.write(outputPath, data, { compress: "gzip" });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user