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
223 lines
6.1 KiB
JavaScript
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 })
|
|
};
|
|
}
|