120 lines
4.4 KiB
TypeScript
120 lines
4.4 KiB
TypeScript
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import type { ProfileEntry } from "../types/profiler.types.js";
|
|
import { FlameGraphGenerator } from "./flamegraph.js";
|
|
|
|
function entry(label: string, start: number, end: number): ProfileEntry {
|
|
return { label, startTime: start, endTime: end, duration: end - start };
|
|
}
|
|
|
|
describe("FlameGraphGenerator", () => {
|
|
it("builds a containment tree", { timeout: 5000 }, () => {
|
|
const gen = new FlameGraphGenerator();
|
|
const tree = gen.generate([
|
|
entry("outer", 0, 100),
|
|
entry("middle", 10, 90),
|
|
entry("inner", 20, 50),
|
|
]);
|
|
expect(tree.name).toBe("root");
|
|
expect(tree.children).toHaveLength(1);
|
|
expect(tree.children[0]?.name).toBe("outer");
|
|
expect(tree.children[0]?.children[0]?.name).toBe("middle");
|
|
expect(tree.children[0]?.children[0]?.children[0]?.name).toBe("inner");
|
|
});
|
|
|
|
it("filters incomplete entries", { timeout: 5000 }, () => {
|
|
const gen = new FlameGraphGenerator();
|
|
const tree = gen.generate([
|
|
entry("ok", 0, 50),
|
|
{ label: "pending", startTime: 0 },
|
|
]);
|
|
expect(tree.children).toHaveLength(1);
|
|
});
|
|
|
|
it("computes selfTime as value minus children", { timeout: 5000 }, () => {
|
|
const gen = new FlameGraphGenerator();
|
|
const tree = gen.generate([
|
|
entry("parent", 0, 100),
|
|
entry("child", 10, 30),
|
|
]);
|
|
const parent = tree.children[0];
|
|
expect(parent?.value).toBe(100);
|
|
expect(parent?.selfTime).toBe(80);
|
|
});
|
|
|
|
it("entry that contains existing siblings absorbs them", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const gen = new FlameGraphGenerator();
|
|
// Insert child first, then outer that contains it. The sort puts
|
|
// outer first because earlier startTime + wider; this still covers
|
|
// the entryContainsNode path when an entry shares startTime with a
|
|
// previously inserted sibling.
|
|
const tree = gen.generate([
|
|
entry("sibling-1", 5, 10),
|
|
entry("sibling-2", 15, 20),
|
|
entry("wide", 0, 50),
|
|
]);
|
|
const wide = tree.children.find((c) => c.name === "wide");
|
|
expect(wide).toBeDefined();
|
|
expect(wide?.children.map((c) => c.name).sort()).toEqual([
|
|
"sibling-1",
|
|
"sibling-2",
|
|
]);
|
|
});
|
|
|
|
it("toD3Format emits d3-compatible JSON", { timeout: 5000 }, () => {
|
|
const gen = new FlameGraphGenerator();
|
|
const tree = gen.generate([entry("only", 0, 100)]);
|
|
const d3 = gen.toD3Format(tree);
|
|
expect(d3).toEqual({
|
|
name: "root",
|
|
value: 100,
|
|
children: [{ name: "only", value: 100, children: [] }],
|
|
});
|
|
});
|
|
|
|
it("collapseRecursive merges same-named parent/child", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const gen = new FlameGraphGenerator();
|
|
const tree = gen.generate([
|
|
entry("rec", 0, 100),
|
|
entry("rec", 10, 90),
|
|
entry("leaf", 20, 50),
|
|
]);
|
|
const collapsed = gen.collapseRecursive(tree);
|
|
// walk tree finding rec
|
|
const recNodes: number[] = [];
|
|
const walk = (n: typeof collapsed): void => {
|
|
if (n.name === "rec") recNodes.push(n.value);
|
|
for (const c of n.children) walk(c);
|
|
};
|
|
walk(collapsed);
|
|
expect(recNodes).toHaveLength(1);
|
|
});
|
|
|
|
it("late wide entry contains some siblings but not others", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const gen = new FlameGraphGenerator();
|
|
const tree = gen.generate([
|
|
entry("early", 0, 10),
|
|
entry("mid", 20, 30),
|
|
entry("late-wide", 15, 40),
|
|
]);
|
|
const names = tree.children.map((c) => c.name).sort();
|
|
expect(names).toContain("early");
|
|
expect(names).toContain("late-wide");
|
|
const late = tree.children.find((c) => c.name === "late-wide");
|
|
expect(late?.children.map((c) => c.name)).toEqual(["mid"]);
|
|
});
|
|
|
|
it("ties on startTime break by wider span first", { timeout: 5000 }, () => {
|
|
const gen = new FlameGraphGenerator();
|
|
const tree = gen.generate([entry("a", 0, 10), entry("b", 0, 50)]);
|
|
// b is wider so should appear first; a is contained within b
|
|
const b = tree.children.find((c) => c.name === "b");
|
|
expect(b?.children.map((c) => c.name)).toContain("a");
|
|
});
|
|
});
|