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:
can1357
2026-02-01 22:04:15 +01:00
parent 529ba004b7
commit 951fc48eba
17 changed files with 520 additions and 60 deletions
+38
View File
@@ -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" });