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"); }); });