Files
tutti/tools/scripts/analyze-electron-trace.test.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

223 lines
6.1 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import {
analyzeElectronTrace,
renderElectronTraceMarkdown
} from "./analyze-electron-trace.mjs";
test("analyzer bounds metrics by scenario markers", async () => {
const directory = await mkdtemp(join(tmpdir(), "tutti-trace-analysis-"));
const tracePath = join(directory, "trace.json");
try {
await writeFile(
tracePath,
JSON.stringify({
traceEvents: [
metadata(1, 2, "CrRendererMain"),
component(900, "BeforeScenario"),
marker(1_000, "tutti-perf:provider-switch:start"),
component(1_100, "HeaderFrame"),
component(1_200, "Tooltip"),
component(1_225, "AgentGUIConversationRailSection2"),
component(1_240, "AgentGUIConversationRailSectionHeader3"),
component(1_250, "Render"),
complete(1_300, 20_000, "RunTask"),
complete(1_400, 5_000, "Layout"),
input(1_500, "click"),
input(1_550, "scroll", 4_000),
completeOnThread(1_600, 80_000, "RunTask", 9, 3),
timestampMarker(
2_000,
"tutti-perf:provider-switch:selected-observed"
),
marker(4_000, "tutti-perf:provider-switch:rail-stable-observed"),
marker(30_000, "tutti-perf:provider-switch:end"),
component(31_000, "AfterScenario"),
profileChunk(1_600, {
nodes: [
profileNode(1, "(root)"),
profileNode(2, "hasSelection", 1),
profileNode(3, "updateStateInner", 2)
],
samples: [2, 3]
})
]
})
);
const summary = await analyzeElectronTrace({
tracePath,
scenario: "provider-switch",
startMarker: "tutti-perf:provider-switch:start",
endMarker: "tutti-perf:provider-switch:end",
milestones: [
{
key: "selected",
label: "target selected",
marker: "tutti-perf:provider-switch:selected-observed"
},
{
key: "stable",
label: "rail stable",
marker: "tutti-perf:provider-switch:rail-stable-observed"
}
],
minimumLongEventMs: 16,
profileFunctionNames: ["hasSelection", "updateStateInner"]
});
assert.equal(summary.window.durationMs, 29);
assert.equal(summary.renders.componentMarkers, 4);
assert.equal(summary.renders.radixFamilyMarkers, 1);
assert.equal(summary.renders.headerFrameMarkers, 1);
assert.equal(summary.renders.sectionMarkers, 1);
assert.equal(summary.renders.sectionHeaderMarkers, 1);
assert.equal(summary.timing.longTaskCount, 1);
assert.equal(summary.timing.maxLongTaskMs, 20);
assert.equal(summary.timing.layoutInclusiveMs, 5);
assert.deepEqual(
summary.window.phases.map((phase) => phase.durationMs),
[1, 2, 26]
);
assert.deepEqual(summary.inputEvents, { click: 1, scroll: 1 });
assert.deepEqual(summary.inputEventTiming.scroll, {
count: 1,
totalInclusiveMs: 4,
maxMs: 4
});
assert.deepEqual(summary.cpuProfile.functionSamples, {
hasSelection: 2,
updateStateInner: 1
});
assert.equal(summary.cpuProfile.sampleCount, 2);
assert.match(renderElectronTraceMarkdown(summary), /report-only/);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("analyzer rejects traces without the scenario end marker", async () => {
const directory = await mkdtemp(join(tmpdir(), "tutti-trace-analysis-"));
const tracePath = join(directory, "trace.json");
try {
await writeFile(
tracePath,
JSON.stringify({
traceEvents: [marker(1_000, "start"), component(1_100, "Tooltip")]
})
);
await assert.rejects(
analyzeElectronTrace({
tracePath,
scenario: "missing-end",
startMarker: "start",
endMarker: "end"
}),
/trace marker not found: end/
);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("analyzer rejects traces with a missing declared milestone", async () => {
const directory = await mkdtemp(join(tmpdir(), "tutti-trace-analysis-"));
const tracePath = join(directory, "trace.json");
try {
await writeFile(
tracePath,
JSON.stringify({
traceEvents: [marker(1_000, "start"), marker(2_000, "end")]
})
);
await assert.rejects(
analyzeElectronTrace({
tracePath,
scenario: "missing-milestone",
startMarker: "start",
endMarker: "end",
milestones: [{ key: "middle", label: "middle", marker: "middle" }]
}),
/trace milestones not found: middle/
);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
function marker(ts, name) {
return { name, ph: "R", pid: 1, tid: 2, ts };
}
function timestampMarker(ts, message) {
return {
args: { data: { message } },
name: "TimeStamp",
ph: "I",
pid: 1,
tid: 2,
ts
};
}
function metadata(pid, tid, name) {
return { args: { name }, name: "thread_name", ph: "M", pid, tid, ts: 1 };
}
function component(ts, message) {
return {
args: { data: { message } },
cat: "devtools.timeline",
name: "TimeStamp",
ph: "I",
pid: 1,
tid: 2,
ts
};
}
function complete(ts, dur, name) {
return { dur, name, ph: "X", pid: 1, tid: 2, ts };
}
function completeOnThread(ts, dur, name, pid, tid) {
return { dur, name, ph: "X", pid, tid, ts };
}
function input(ts, type, dur = 10) {
return {
args: { data: { type } },
dur,
name: "EventDispatch",
ph: "X",
pid: 1,
tid: 2,
ts
};
}
function profileChunk(ts, cpuProfile) {
return {
args: {
data: { cpuProfile, timeDeltas: cpuProfile.samples.map(() => 100) }
},
id: "0x1",
name: "ProfileChunk",
ph: "P",
pid: 1,
tid: 4,
ts
};
}
function profileNode(id, functionName, parent) {
return {
callFrame: { functionName, scriptId: 1 },
id,
...(parent == null ? {} : { parent })
};
}