From 7208f24b34b5b05d27efb941182bb8a474e7d5c3 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Fri, 23 Oct 2026 12:23:20 +0200 Subject: [PATCH] fix(vitest-plugin): prefix walltime URIs of top-level benches with the file path The walltime runner only registered URI prefixes for nested suites, so a bench declared outside a `describe` block reported `::` to the runner while its results used `::`. The profile and results URIs then no longer matched and the benchmark had no flamegraph. Register the root file suite too, and add a top-level bench to the Vitest 3 and 4 examples. Refs COD-3984 --- .../with-vitest-v3/src/fibonacci.bench.ts | 4 + .../with-vitest-v4/src/fibonacci.bench.ts | 4 + .../src/__tests__/walltime.test.ts | 114 ++++++++++++++++++ packages/vitest-plugin/src/legacy/walltime.ts | 2 +- 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 packages/vitest-plugin/src/__tests__/walltime.test.ts diff --git a/examples/with-vitest-v3/src/fibonacci.bench.ts b/examples/with-vitest-v3/src/fibonacci.bench.ts index 227a6e67..1349a1ad 100644 --- a/examples/with-vitest-v3/src/fibonacci.bench.ts +++ b/examples/with-vitest-v3/src/fibonacci.bench.ts @@ -18,3 +18,7 @@ describe("fibonacci", () => { iterativeFibonacci(20); }); }); + +bench("top-level recursive fibo 15", () => { + recursiveFibonacci(15); +}); diff --git a/examples/with-vitest-v4/src/fibonacci.bench.ts b/examples/with-vitest-v4/src/fibonacci.bench.ts index 227a6e67..1349a1ad 100644 --- a/examples/with-vitest-v4/src/fibonacci.bench.ts +++ b/examples/with-vitest-v4/src/fibonacci.bench.ts @@ -18,3 +18,7 @@ describe("fibonacci", () => { iterativeFibonacci(20); }); }); + +bench("top-level recursive fibo 15", () => { + recursiveFibonacci(15); +}); diff --git a/packages/vitest-plugin/src/__tests__/walltime.test.ts b/packages/vitest-plugin/src/__tests__/walltime.test.ts new file mode 100644 index 00000000..da60f241 --- /dev/null +++ b/packages/vitest-plugin/src/__tests__/walltime.test.ts @@ -0,0 +1,114 @@ +import { fromPartial } from "@total-typescript/shoehorn"; +import { describe, expect, it, vi, type RunnerTestSuite } from "vitest"; +import { WalltimeRunner } from "../legacy/walltime"; + +const coreMocks = vi.hoisted(() => { + return { + InstrumentHooks: { + startBenchmark: vi.fn(), + stopBenchmark: vi.fn(), + setExecutedBenchmark: vi.fn(), + addMarker: vi.fn(), + currentTimestamp: vi.fn(() => 0n), + }, + setupCore: vi.fn(), + }; +}); + +vi.mock("@codspeed/core", async (importOriginal) => { + const mod = await importOriginal(); + return { ...mod, ...coreMocks }; +}); + +console.warn = vi.fn(); + +// Fully mocked: it loads the Vitest 3/4 runner, absent from the Vitest 5 devDependency. +vi.mock("../legacy/compat", () => { + interface SuiteLike { + id: string; + type: string; + name: string; + tasks: SuiteLike[]; + } + + // Mirrors Vitest's benchmark suite execution: sub-suites run first, then + // the suite's own benches run after a `suite-prepare` event for the suite. + class NodeBenchmarkRunner { + async importTinybench() { + return import("tinybench"); + } + + async runSuite(suite: SuiteLike): Promise { + await this.runBenchmarkSuite(suite); + } + + private async runBenchmarkSuite(suite: SuiteLike): Promise { + for (const task of suite.tasks) { + if (task.type === "suite") { + await this.runBenchmarkSuite(task); + } + } + + const benches = suite.tasks.filter((task) => task.type === "test"); + if (benches.length === 0) { + return; + } + + await (this as unknown as WalltimeRunner).onTaskUpdate( + [], + [[suite.id, "suite-prepare", undefined]], + ); + const { Bench } = await this.importTinybench(); + for (const task of benches) { + const bench = new Bench({ + iterations: 1, + time: 0, + warmupIterations: 0, + warmupTime: 0, + }); + bench.add(task.name, () => {}); + await bench.run(); + } + } + } + + return { + NodeBenchmarkRunner, + getBenchOptions: vi.fn(), + }; +}); + +const FILE_URI = "packages/vitest-plugin/src/__tests__/walltime.test.ts"; + +describe("WalltimeRunner", () => { + it("prefixes top-level and nested bench URIs with the file path", async () => { + const runner = new WalltimeRunner(fromPartial({})); + const suite = fromPartial({ + id: "file", + type: "suite", + file: { filepath: __filename }, + name: "walltime.test.ts", + tasks: [ + { + id: "nested", + type: "suite", + name: "nested suite", + tasks: [ + { type: "test", name: "nested bench", meta: { benchmark: true } }, + ], + }, + { type: "test", name: "top-level bench", meta: { benchmark: true } }, + ], + }); + + await runner.runSuite(suite); + + const uris = coreMocks.InstrumentHooks.setExecutedBenchmark.mock.calls.map( + ([, uri]) => uri, + ); + expect(uris).toEqual([ + `${FILE_URI}::nested suite::nested bench`, + `${FILE_URI}::top-level bench`, + ]); + }); +}); diff --git a/packages/vitest-plugin/src/legacy/walltime.ts b/packages/vitest-plugin/src/legacy/walltime.ts index 09271df8..8a4f0d7e 100644 --- a/packages/vitest-plugin/src/legacy/walltime.ts +++ b/packages/vitest-plugin/src/legacy/walltime.ts @@ -51,10 +51,10 @@ export class WalltimeRunner extends NodeBenchmarkRunner { private populateBenchmarkUris(suite: RunnerTestSuite, parentPath = ""): void { const currentPath = parentPath !== "" ? `${parentPath}::${suite.name}` : suite.name; + this.suiteUris.set(suite.id, currentPath); for (const task of suite.tasks) { if (task.type === "suite") { - this.suiteUris.set(task.id, `${currentPath}::${task.name}`); this.populateBenchmarkUris(task, currentPath); } }