Files
tutti/tools/scripts/run-agent-gui-performance.mjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

917 lines
29 KiB
JavaScript

#!/usr/bin/env node
import { spawn } from "node:child_process";
import { once } from "node:events";
import { createWriteStream } from "node:fs";
import {
access,
mkdir,
mkdtemp,
readFile,
rm,
writeFile
} from "node:fs/promises";
import { createServer } from "node:net";
import { homedir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { fileURLToPath } from "node:url";
import {
CdpClient,
defaultTraceCategories,
resolveBrowserWebSocketUrl,
resolvePageWebSocketUrl,
writeStreamToFile
} from "./capture-electron-trace.mjs";
import {
analyzeElectronTrace,
renderElectronTraceMarkdown
} from "./analyze-electron-trace.mjs";
import { ensurePreparedDesktopLaunch } from "./prepared-desktop-launch.mjs";
import {
agentGuiPerformanceScenarios,
resolveAgentGuiPerformanceScenario
} from "./agent-gui-performance-scenarios.mjs";
import { startAllProcessTimeProfile } from "./all-process-time-profile.mjs";
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
const workspaceRoot = resolve(scriptDirectory, "..", "..");
const desktopReadyTimeoutMs = 120_000;
const scenarioReadyTimeoutMs = 60_000;
const desktopStartupFailurePrefix = "[tutti-desktop-startup-failed] ";
const recoverableQueueTables = [
"workspace_agent_runtime_operation_events",
"workspace_agent_runtime_operations",
"workspace_agent_goal_reconcile_inbox",
"workspace_agent_goal_control_operations",
"workspace_agent_submit_claims"
];
if (isMainModule()) {
main(process.argv.slice(2)).catch((error) => {
process.stderr.write(`[perf:agent-gui] ${error.stack ?? error.message}\n`);
process.exitCode = 1;
});
}
export async function main(argv) {
const options = parseArgs(argv);
if (options.help) {
printUsage();
return;
}
if (options.listScenarios) {
process.stdout.write(
`${agentGuiPerformanceScenarios.map((scenario) => scenario.id).join("\n")}\n`
);
return;
}
const scenario = resolveAgentGuiPerformanceScenario(options.scenario);
const outputDirectory = resolve(
options.outputDirectory ?? defaultOutputDirectory(scenario.id, new Date())
);
const runtimeParent = join(workspaceRoot, ".tmp");
await mkdir(runtimeParent, { recursive: true });
await mkdir(outputDirectory, { recursive: true });
const runtimeDirectory = await mkdtemp(
join(runtimeParent, "agent-gui-perf-runtime-")
);
const stateDirectory = join(runtimeDirectory, "state");
const userDataDirectory = join(runtimeDirectory, "electron-user-data");
const databasePath = join(stateDirectory, "tuttid.db");
const daemonPath = join(runtimeDirectory, "tuttid");
const tracePath = join(outputDirectory, "trace.json");
const reportJSONPath = join(outputDirectory, "report.json");
const reportMarkdownPath = join(outputDirectory, "report.md");
const desktopLogPath = join(outputDirectory, "desktop.log");
const timeProfilePath = join(outputDirectory, "time-profile.trace");
let desktopProcess = null;
let pageClient = null;
let browserClient = null;
let timeProfileCapture = null;
let primaryError = null;
log(`source snapshot: ${basename(options.sourceDatabase)}`);
log(`output: ${outputDirectory}`);
try {
await access(options.sourceDatabase);
await mkdir(stateDirectory, { recursive: true });
await snapshotSQLiteDatabase(options.sourceDatabase, databasePath);
const snapshotInfo = await prepareDatabaseSnapshot(databasePath);
const scenarioSnapshot = scenario.prepareSnapshot
? await scenario.prepareSnapshot({
databasePath,
runtimeDirectory,
sqliteExec,
sqliteJSON,
workspaceRoot
})
: null;
log(
`snapshot ready: ${snapshotInfo.sessionCount} sessions, ${snapshotInfo.projectCount} projects`
);
await buildDaemon(daemonPath);
const cdpPort = await reservePort();
const desktopLaunch = await ensurePreparedDesktopLaunch({
log,
workspaceRoot
});
desktopProcess = startDesktop({
...desktopLaunch,
cdpPort,
daemonPath,
desktopLogPath,
environment: scenarioSnapshot?.environment,
stateDirectory,
userDataDirectory
});
const pageWebSocket = await waitForPageWebSocket(
cdpPort,
desktopProcess,
desktopReadyTimeoutMs
);
pageClient = await CdpClient.connect(pageWebSocket);
await pageClient.send("Runtime.enable");
browserClient = await CdpClient.connect(
await resolveBrowserWebSocketUrl(cdpPort)
);
const context = {
browserClient,
outputDirectory,
pageClient,
scenarioData: scenarioSnapshot?.data ?? null,
targetID: targetIDFromWebSocket(pageWebSocket)
};
const prepared = await scenario.prepare(context, {
fromTargetID: options.fromTargetID,
toTargetID: options.toTargetID,
timeoutMs: scenarioReadyTimeoutMs
});
log(`scenario ${scenario.id}: ${scenario.describe(prepared)}`);
if (options.allProcessTimeProfile) {
log("starting all-process Time Profiler");
timeProfileCapture = await startAllProcessTimeProfile({
cwd: workspaceRoot,
outputPath: timeProfilePath,
timeoutMs: 30_000
});
}
await browserClient.send("Tracing.start", {
categories: defaultTraceCategories,
options: "sampling-frequency=10000",
transferMode: "ReturnAsStream"
});
let scenarioResult;
let scenarioError = null;
let captureError = null;
try {
scenarioResult = await scenario.execute(context, prepared, {
timeoutMs: scenarioReadyTimeoutMs
});
} catch (error) {
scenarioError = error;
} finally {
try {
await stopTrace(browserClient, tracePath);
} catch (error) {
captureError = error;
}
if (timeProfileCapture) {
try {
await timeProfileCapture.stop();
} catch (error) {
captureError ??= error;
}
}
}
if (scenarioError) throw scenarioError;
if (captureError) throw captureError;
const summary = await analyzeElectronTrace({
tracePath,
scenario: scenario.id,
startMarker: scenario.markers.start,
endMarker: scenario.markers.end,
milestones: scenario.milestones,
minimumLongEventMs: options.minimumLongEventMs,
profileFunctionNames: scenario.profileFunctionNames,
sourceRoot: workspaceRoot
});
summary.run = {
...scenario.summarize(prepared, scenarioResult),
sourceDatabase: basename(options.sourceDatabase),
sourceSessionCount: snapshotInfo.sessionCount,
sourceProjectCount: snapshotInfo.projectCount,
clearedRecoveryRows: snapshotInfo.clearedRecoveryRows
};
if (timeProfileCapture) {
summary.run.details.push({
label: "All-process Time Profiler",
value: basename(timeProfilePath)
});
}
applyScenarioAssessment(summary, scenario.assessTrace?.(summary));
await writeFile(reportJSONPath, `${JSON.stringify(summary, null, 2)}\n`);
await writeFile(
reportMarkdownPath,
renderRunMarkdown(
summary,
renderElectronTraceMarkdown(summary, { sourceRoot: workspaceRoot })
)
);
log(
`${summary.mode}: ${summary.renders.componentMarkers} component markers, ${summary.renders.radixFamilyMarkers} Radix-family markers, ${summary.timing.longTaskCount} renderer long tasks`
);
log(`report: ${reportMarkdownPath}`);
log(`trace: ${tracePath}`);
if (timeProfileCapture) log(`time profile: ${timeProfilePath}`);
const failureReasons = performanceRunFailureReasons(summary);
if (failureReasons.length > 0) {
throw new Error(
`${scenario.id} performance gate failed: ${failureReasons.join(", ")}`
);
}
} catch (error) {
primaryError = error;
throw error;
} finally {
pageClient?.close();
browserClient?.close();
if (timeProfileCapture) {
try {
await timeProfileCapture.stop();
} catch (error) {
if (!primaryError) process.exitCode = 1;
log(`Time Profiler cleanup warning: ${error.message}`);
}
}
if (desktopProcess) {
await stopProcessTree(desktopProcess);
}
if (!options.keepRuntime) {
try {
await rm(runtimeDirectory, {
recursive: true,
force: true,
maxRetries: 10,
retryDelay: 200
});
} catch (error) {
if (!primaryError) process.exitCode = 1;
log(`cleanup warning: ${error.message}`);
}
} else {
log(`runtime kept: ${runtimeDirectory}`);
}
}
}
export async function snapshotSQLiteDatabase(sourcePath, destinationPath) {
const escapedDestination = destinationPath.replaceAll("'", "''");
await runCommand("sqlite3", [
sourcePath,
"PRAGMA query_only=ON;",
".timeout 10000",
`.backup '${escapedDestination}'`
]);
}
export async function prepareDatabaseSnapshot(databasePath) {
const counts = await sqliteJSON(
databasePath,
`
SELECT
(SELECT COUNT(*) FROM workspace_agent_sessions) AS sessionCount,
(SELECT COUNT(*) FROM user_projects) AS projectCount;
`
);
const tableRows = await sqliteJSON(
databasePath,
`SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name;`
);
const existingTables = new Set(tableRows.map((row) => row.name));
if (existingTables.has("agent_store_schema_migrations")) {
const migrationRows = await sqliteJSON(
databasePath,
`SELECT id FROM agent_store_schema_migrations ORDER BY id;`
);
const migrationSource = await readFile(
join(workspaceRoot, "packages", "agent", "store-sqlite", "migrations.go"),
"utf8"
);
const unknownMigrationIDs = findUnknownAgentTargetMigrationIDs(
migrationRows.map((row) => row.id),
migrationSource
);
if (unknownMigrationIDs.length > 0) {
throw new Error(
`source dev DB has newer Agent target migrations (${unknownMigrationIDs.join(", ")}); SQLite migrations are forward-only, so switch to a compatible checkout or pass --source-db for a compatible snapshot`
);
}
}
const queueCounts = {};
for (const table of recoverableQueueTables) {
if (!existingTables.has(table)) continue;
const rows = await sqliteJSON(
databasePath,
`SELECT COUNT(*) AS count FROM ${table};`
);
queueCounts[table] = Number(rows[0]?.count ?? 0);
}
const startupRows = await sqliteJSON(
databasePath,
`
SELECT workspace_id AS workspaceId, snapshot_json AS snapshotJSON
FROM workspace_workbench_snapshots
WHERE workspace_id = (
SELECT id
FROM workspaces
ORDER BY COALESCE(last_opened_at_unix_ms, 0) DESC, updated_at_unix_ms DESC, id ASC
LIMIT 1
)
LIMIT 1;
`
);
const startup = startupRows[0];
if (!startup?.snapshotJSON) {
throw new Error("startup workspace has no durable Workbench snapshot");
}
const preparedSnapshot = prepareWorkbenchSnapshotForPerformance(
JSON.parse(startup.snapshotJSON)
);
const deleteStatements = recoverableQueueTables
.filter((table) => existingTables.has(table))
.map((table) => `DELETE FROM ${table};`)
.join("\n");
try {
await sqliteExec(
databasePath,
`
PRAGMA foreign_keys = ON;
${deleteStatements}
UPDATE workspace_workbench_snapshots
SET snapshot_json = '${sqlString(JSON.stringify(preparedSnapshot))}'
WHERE workspace_id = '${sqlString(startup.workspaceId)}';
`
);
} catch (error) {
throw new Error(`failed to sanitize isolated database: ${error.message}`);
}
return {
sessionCount: Number(counts[0]?.sessionCount ?? 0),
projectCount: Number(counts[0]?.projectCount ?? 0),
clearedRecoveryRows: Object.values(queueCounts).reduce(
(total, count) => total + count,
0
)
};
}
export function findUnknownAgentTargetMigrationIDs(
appliedMigrationIDs,
migrationSource
) {
const knownMigrationIDs = new Set(
[
...migrationSource.matchAll(/const schemaMigration\w+\s*=\s*"([^"]+)"/gu)
].map((match) => match[1])
);
return appliedMigrationIDs
.filter(
(migrationID) =>
migrationID.startsWith("agent_targets_") &&
!knownMigrationIDs.has(migrationID)
)
.sort();
}
export function prepareWorkbenchSnapshotForPerformance(snapshot) {
if (
!snapshot ||
typeof snapshot !== "object" ||
!Array.isArray(snapshot.nodes)
) {
throw new Error("invalid durable Workbench snapshot");
}
const nodes = snapshot.nodes.map((node) => structuredClone(node));
const candidates = nodes.filter((node) => node?.data?.typeId === "agent-gui");
const selected =
candidates.find((node) => node.id === snapshot.activeNodeId) ??
candidates.find((node) => node.isMinimized !== true) ??
candidates[0];
if (!selected) {
throw new Error("startup Workbench snapshot has no AgentGUI node");
}
const existingState =
selected.data?.snapshotNodeState &&
typeof selected.data.snapshotNodeState === "object"
? selected.data.snapshotNodeState
: {};
selected.data = {
...selected.data,
snapshotNodeState: {
...existingState,
conversationRailCollapsed: false,
lastActiveAgentSessionId: null,
lastActiveAgentSessionIdByAgentTargetId: {}
}
};
selected.isMinimized = false;
const nodeStack = Array.isArray(snapshot.nodeStack)
? snapshot.nodeStack.filter((nodeID) => nodeID !== selected.id)
: [];
nodeStack.push(selected.id);
return {
...snapshot,
nodes,
nodeStack,
activeNodeId: selected.id
};
}
export function applyScenarioAssessment(summary, assessment) {
if (!assessment) return summary;
summary.run.assertions.push(...(assessment.assertions ?? []));
summary.run.details.push(...(assessment.details ?? []));
const failedAssertions = summary.run.assertions.filter(
(assertion) => !assertion.passed
);
summary.run.outcome = failedAssertions.length === 0 ? "passed" : "failed";
summary.mode = "scenario-thresholds";
summary.verdict =
failedAssertions.length === 0
? {
status: "passed",
reason: `${summary.run.assertions.length} scenario assertions passed`
}
: {
status: "failed",
reason: `${failedAssertions.length} of ${summary.run.assertions.length} scenario assertions failed`
};
return summary;
}
export function performanceRunFailureReasons(summary) {
const failedAssertions = (summary.run?.assertions ?? [])
.filter((assertion) => !assertion.passed)
.map((assertion) => assertion.name);
if (failedAssertions.length > 0) {
return failedAssertions;
}
if (summary.verdict?.status === "failed") {
return [summary.verdict.reason ?? "trace assessment failed"];
}
return [];
}
export async function buildDaemon(outputPath) {
log("building isolated tuttid");
await runCommand(
"go",
["build", "-buildvcs=false", "-o", outputPath, "."],
join(workspaceRoot, "services", "tuttid")
);
}
export function startDesktop(input) {
const headless = input.headless !== false;
const command = input.command ?? "pnpm";
const args = input.args ?? ["dev:desktop"];
const launchMode = input.launchMode ? `${input.launchMode} ` : "";
log(
`starting ${headless ? "headless " : ""}${launchMode}isolated Desktop on CDP ${input.cdpPort}`
);
const logStream = createWriteStream(input.desktopLogPath, { flags: "w" });
const environment = { ...process.env };
if (input.launchMode === "prebuilt") {
delete environment.ELECTRON_ENTRY;
delete environment.ELECTRON_RENDERER_URL;
delete environment.NODE_ENV_ELECTRON_VITE;
}
const child = spawn(command, args, {
cwd: workspaceRoot,
detached: process.platform !== "win32",
env: {
...environment,
...input.environment,
TUTTI_ANALYTICS_DISABLED: "1",
TUTTI_DESKTOP_LOG_OUTPUT: "tee",
...(headless ? { TUTTI_DESKTOP_PERFORMANCE_HEADLESS: "1" } : {}),
TUTTI_DESKTOP_USER_DATA_DIR: input.userDataDirectory,
TUTTI_ELECTRON_JS_FLAGS: "--max-old-space-size=8192",
TUTTI_ELECTRON_REMOTE_DEBUGGING_PORT: String(input.cdpPort),
TUTTI_ENV: "development",
TUTTI_STATE_DIR: input.stateDirectory,
TUTTID_ADDR: "127.0.0.1:0",
TUTTID_BIN: input.daemonPath,
TUTTID_LOG_OUTPUT: "tee",
VITE_TUTTI_REACT_PROFILER: "0",
VITE_TUTTI_WHY_DID_YOU_RENDER: "0"
},
stdio: ["ignore", "pipe", "pipe"]
});
const tail = createBoundedLogTail();
for (const stream of [child.stdout, child.stderr]) {
stream?.on("data", (chunk) => {
logStream.write(chunk);
tail.append(chunk.toString());
});
}
child.once("close", () => logStream.end());
child.performanceLogTail = tail;
// Detached Electron descendants may leave the process group; stopProcessTree
// sweeps leftovers by isolated user-data / state dir only (not shared TUTTID_BIN).
child.userDataDirectory = input.userDataDirectory;
child.stateDirectory = input.stateDirectory;
child.daemonPath = input.daemonPath;
return child;
}
export async function waitForPageWebSocket(port, child, timeoutMs) {
const deadline = Date.now() + timeoutMs;
let lastError;
while (Date.now() < deadline) {
if (child.exitCode !== null || child.signalCode !== null) {
const startupFailure = parseDesktopStartupFailure(
child.performanceLogTail.read()
);
if (startupFailure) {
throw new Error(startupFailure.message, {
...(startupFailure.cause ? { cause: startupFailure.cause } : {})
});
}
throw new Error(
`Desktop exited before CDP became ready\n${child.performanceLogTail.read()}`
);
}
try {
return await resolvePageWebSocketUrl(port);
} catch (error) {
lastError = error;
}
await delay(250);
}
throw new Error(
`timed out waiting for Desktop CDP: ${lastError?.message ?? "unknown error"}\n${child.performanceLogTail.read()}`
);
}
export function parseDesktopStartupFailure(log) {
const lines = String(log).split(/\r?\n/u);
for (let index = lines.length - 1; index >= 0; index -= 1) {
const line = lines[index]?.trim() ?? "";
const marker = line.indexOf(desktopStartupFailurePrefix);
if (marker < 0) continue;
try {
const parsed = JSON.parse(
line.slice(marker + desktopStartupFailurePrefix.length)
);
if (typeof parsed?.message !== "string" || !parsed.message.trim()) {
continue;
}
const cause =
typeof parsed.cause?.code === "string" &&
parsed.cause.code.trim() &&
typeof parsed.cause?.message === "string" &&
parsed.cause.message.trim()
? {
code: parsed.cause.code.trim(),
message: parsed.cause.message.trim()
}
: null;
return {
...(cause ? { cause } : {}),
message: parsed.message.trim()
};
} catch {
// Ignore malformed protocol lines and retain the ordinary log fallback.
}
}
return null;
}
async function stopTrace(client, tracePath) {
const complete = client.waitForEvent("Tracing.tracingComplete");
await client.send("Tracing.end");
const event = await complete;
const stream = event.params?.stream;
if (!stream) {
throw new Error("Tracing.tracingComplete did not include a stream handle");
}
await writeStreamToFile(client, stream, tracePath);
}
async function sqliteJSON(databasePath, sql) {
const output = await runCommand("sqlite3", ["-json", databasePath, sql]);
return output.trim() ? JSON.parse(output) : [];
}
async function sqliteExec(databasePath, sql) {
await runCommand("sqlite3", [databasePath, sql]);
}
function runCommand(command, args, cwd = workspaceRoot) {
return new Promise((resolveCommand, rejectCommand) => {
const child = spawn(command, args, {
cwd,
env: process.env,
stdio: ["ignore", "pipe", "pipe"]
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.once("error", rejectCommand);
child.once("exit", (code, signal) => {
if (code === 0) {
resolveCommand(stdout);
} else {
rejectCommand(
new Error(
`${command} failed (${code ?? signal ?? "unknown"})${stderr.trim() ? `: ${stderr.trim()}` : ""}`
)
);
}
});
});
}
export async function reservePort() {
const server = createServer();
await new Promise((resolveListen, rejectListen) => {
server.once("error", rejectListen);
server.listen(0, "127.0.0.1", resolveListen);
});
const address = server.address();
const port = typeof address === "object" && address ? address.port : null;
await new Promise((resolveClose) => server.close(resolveClose));
if (!port) throw new Error("failed to reserve a CDP port");
return port;
}
export async function stopProcessTree(child) {
if (child?.pid && child.exitCode === null && child.signalCode === null) {
if (process.platform !== "win32") {
try {
process.kill(-child.pid, "SIGINT");
} catch {
try {
child.kill("SIGINT");
} catch {
// Already gone.
}
}
} else {
try {
child.kill("SIGINT");
} catch {
// Already gone.
}
}
await Promise.race([onceExit(child), delay(5_000)]);
if (child.exitCode === null && child.signalCode === null) {
if (process.platform !== "win32") {
try {
process.kill(-child.pid, "SIGKILL");
} catch {
try {
child.kill("SIGKILL");
} catch {
// Already gone.
}
}
} else {
try {
child.kill("SIGKILL");
} catch {
// Already gone.
}
}
await Promise.race([onceExit(child), delay(2_000)]);
}
}
// Detached Electron/tuttid can survive process-group signals; sweep by the
// isolated paths this Desktop was launched with.
await sweepDetachedDesktopLeftovers(child);
}
async function sweepDetachedDesktopLeftovers(child) {
if (process.platform === "win32" || typeof child !== "object" || !child) {
return;
}
// Only match run-isolated paths. Never use daemonPath: parallel record/replay
// workers share a warmed tuttids binary (TUTTID_BIN), so sweeping by that path
// SIGTERMs every concurrent managed tuttids when one Desktop exits.
const markers = [child.userDataDirectory, child.stateDirectory].filter(
(value) => typeof value === "string" && value.trim()
);
if (markers.length === 0) return;
const pids = await listPidsMatchingMarkers(markers);
for (const pid of pids) {
if (pid === process.pid || pid === child.pid) continue;
try {
process.kill(pid, "SIGTERM");
} catch {
// Already gone.
}
}
if (pids.length === 0) return;
await delay(500);
for (const pid of pids) {
if (pid === process.pid || pid === child.pid) continue;
try {
process.kill(pid, 0);
process.kill(pid, "SIGKILL");
} catch {
// Already gone.
}
}
}
async function listPidsMatchingMarkers(markers) {
const child = spawn("ps", ["-axo", "pid=,command="], {
stdio: ["ignore", "pipe", "ignore"]
});
let stdout = "";
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
try {
await once(child, "close");
} catch {
return [];
}
const pids = [];
for (const line of stdout.split("\n")) {
const trimmed = line.trim();
if (!trimmed) continue;
const match = /^(\d+)\s+(.*)$/.exec(trimmed);
if (!match) continue;
const pid = Number.parseInt(match[1], 10);
const command = match[2];
if (!Number.isSafeInteger(pid) || pid <= 0) continue;
if (!markers.some((marker) => command.includes(marker))) continue;
// Only sweep Tutti desktop / daemon leftovers, not unrelated tools that
// happen to mention a path in their argv.
if (
!/Electron|tuttid|electron-vite|dev:desktop/i.test(command) &&
!command.includes("electron-user-data")
) {
continue;
}
pids.push(pid);
}
return pids;
}
function onceExit(child) {
return new Promise((resolveExit) => child.once("exit", resolveExit));
}
function createBoundedLogTail(limit = 20_000) {
let value = "";
return {
append(chunk) {
value = (value + chunk).slice(-limit);
},
read() {
return value;
}
};
}
function renderRunMarkdown(summary, traceMarkdown) {
const runLines = [
"## Scenario contract",
"",
`- Outcome: ${summary.run.outcome}`,
`- Source DB snapshot: ${summary.run.sourceDatabase}`,
`- Source sessions: ${summary.run.sourceSessionCount}`,
`- Source projects: ${summary.run.sourceProjectCount}`,
...summary.run.details.map(
(detail) => `- ${detail.label}: ${detail.value}`
),
`- Stability criterion: ${summary.run.stabilityCriterion}`,
`- Recovery rows removed from isolated copy: ${summary.run.clearedRecoveryRows}`,
"",
"| Assertion | Result |",
"| --- | --- |",
...summary.run.assertions.map(
(assertion) =>
`| ${assertion.name} | ${assertion.passed ? "pass" : "fail"} |`
),
""
].join("\n");
return traceMarkdown.replace(
"## Phase timings",
`${runLines}\n## Phase timings`
);
}
function sqlString(value) {
return String(value).replaceAll("'", "''");
}
function defaultOutputDirectory(scenarioID, date) {
const stamp = date
.toISOString()
.replaceAll(":", "")
.replaceAll("-", "")
.replace(/\.\d{3}Z$/u, "Z");
return join(workspaceRoot, ".tmp", "perf", "agent-gui", scenarioID, stamp);
}
function parseArgs(argv) {
const options = {
sourceDatabase: join(homedir(), ".tutti-dev", "tuttid.db"),
minimumLongEventMs: 16,
scenario: "provider-switch"
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--") continue;
if (arg === "--help" || arg === "-h") {
options.help = true;
} else if (arg === "--source-db") {
options.sourceDatabase = resolve(requiredValue(argv, (index += 1), arg));
} else if (arg === "--output") {
options.outputDirectory = requiredValue(argv, (index += 1), arg);
} else if (arg === "--scenario") {
options.scenario = requiredValue(argv, (index += 1), arg);
} else if (arg === "--list-scenarios") {
options.listScenarios = true;
} else if (arg === "--from-target-id") {
options.fromTargetID = requiredValue(argv, (index += 1), arg);
} else if (arg === "--to-target-id") {
options.toTargetID = requiredValue(argv, (index += 1), arg);
} else if (arg === "--min-ms") {
options.minimumLongEventMs = positiveNumber(
requiredValue(argv, (index += 1), arg),
arg
);
} else if (arg === "--keep-runtime") {
options.keepRuntime = true;
} else if (arg === "--all-process-time-profile") {
options.allProcessTimeProfile = true;
} else {
throw new Error(`unknown option: ${arg}`);
}
}
return options;
}
function requiredValue(argv, index, option) {
const value = argv[index];
if (!value || value.startsWith("--")) {
throw new Error(`${option} requires a value`);
}
return value;
}
function positiveNumber(value, option) {
const number = Number(value);
if (!Number.isFinite(number) || number <= 0) {
throw new Error(`${option} must be a positive number`);
}
return number;
}
function isMainModule() {
return process.argv[1]
? fileURLToPath(import.meta.url) === resolve(process.argv[1])
: false;
}
function targetIDFromWebSocket(webSocketURL) {
const targetID = new URL(webSocketURL).pathname
.split("/")
.filter(Boolean)
.at(-1);
if (!targetID) throw new Error("could not resolve CDP page target id");
return targetID;
}
function log(message) {
process.stderr.write(`[perf:agent-gui] ${message}\n`);
}
function printUsage() {
process.stdout.write(
`Run an isolated, report-only AgentGUI performance scenario.\n\nUsage:\n pnpm perf:agent-gui\n pnpm perf:agent-gui -- --scenario session-switch\n pnpm perf:agent-gui -- --list-scenarios\n\nOptions:\n --scenario <id> Scenario. Default: provider-switch\n --list-scenarios Print available scenario ids\n --source-db <path> Source dev DB. Default: ~/.tutti-dev/tuttid.db\n --output <directory> Report/trace output directory under .tmp by default\n --from-target-id <id> Source Agent target for provider scenarios\n --to-target-id <id> Target Agent target for provider scenarios\n --min-ms <milliseconds> Long-event threshold. Default: 16\n --all-process-time-profile\n Also record macOS Time Profiler for all processes\n --keep-runtime Keep isolated DB and Electron userData for debugging\n`
);
process.stdout.write(
"\n--min-ms applies to renderer-main RunTask duration, not all trace events.\n"
);
}