Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-benchmark-reindex-exit-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stainless-code/codemap": patch
---

Fail benchmark reindex runs when the spawned indexer exits non-zero instead of recording misleading timings.
54 changes: 54 additions & 0 deletions src/benchmark-reindex.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from "bun:test";

import { runBenchmarkReindex } from "./benchmark-reindex";

describe("runBenchmarkReindex", () => {
it("throws when the indexer exits non-zero instead of recording a timing", async () => {
let spawnCalls = 0;
await expect(
runBenchmarkReindex("fail case", ["--full"], {
runs: 1,
spawnIndexer: async () => {
spawnCalls++;
return {
exitCode: 1,
stderr: "indexer exploded",
stdout: "",
};
},
}),
).rejects.toThrow(/benchmark reindex "fail case" failed \(exit 1\)/);
expect(spawnCalls).toBe(1);
});

it("rejects invalid runs values", async () => {
const spawnIndexer = async () => ({
exitCode: 0,
stderr: "",
stdout: "",
});
await expect(
runBenchmarkReindex("bad-runs", [], { runs: 0, spawnIndexer }),
).rejects.toThrow(/requires runs >= 1/);
await expect(
runBenchmarkReindex("bad-runs", [], { runs: -1, spawnIndexer }),
).rejects.toThrow(/requires runs >= 1/);
await expect(
runBenchmarkReindex("bad-runs", [], { runs: 1.5, spawnIndexer }),
).rejects.toThrow(/requires runs >= 1/);
});

it("records timings when every run exits zero", async () => {
const result = await runBenchmarkReindex("ok", [], {
runs: 2,
spawnIndexer: async () => ({
exitCode: 0,
stderr: "",
stdout: "",
}),
});
expect(result.label).toBe("ok");
expect(result.runs).toBe(2);
expect(result.avg).toBeGreaterThanOrEqual(0);
});
});
47 changes: 47 additions & 0 deletions src/benchmark-reindex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
async function timeMsAsync(fn: () => Promise<void>): Promise<{ ms: number }> {
const start = performance.now();
await fn();
return { ms: performance.now() - start };
}

export interface IndexerSpawnResult {
exitCode: number | null;
stderr: string;
stdout: string;
}

export type IndexerSpawn = (args: string[]) => Promise<IndexerSpawnResult>;

export async function runBenchmarkReindex(
label: string,
args: string[],
opts: { spawnIndexer: IndexerSpawn; runs?: number },
): Promise<{
label: string;
avg: number;
min: number;
max: number;
runs: number;
}> {
Comment on lines +7 to +25
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Document the exported benchmark API surface.

The exported interface/type/function are public API but currently have no accompanying docs. Add concise JSDoc that captures intent/contract (especially failure behavior and runs semantics).

📝 Proposed doc additions
 export interface IndexerSpawnResult {
   exitCode: number | null;
   stderr: string;
   stdout: string;
 }
 
+/** Spawns an indexer process for the provided CLI arguments. */
 export type IndexerSpawn = (args: string[]) => Promise<IndexerSpawnResult>;
 
+/**
+ * Benchmarks repeated reindex runs and returns aggregate timing stats.
+ * Throws when any run exits non-zero to avoid reporting failed runs as timings.
+ */
 export async function runBenchmarkReindex(

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: All public APIs must have accompanying documentation".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/benchmark-reindex.ts` around lines 7 - 25, Add JSDoc for the exported
API: document IndexerSpawnResult (fields exitCode, stderr, stdout and what null
exitCode means), IndexerSpawn (contract: accepts args array, returns a Promise
resolving to IndexerSpawnResult and may reject on spawn failure), and
runBenchmarkReindex (describe label, args, opts.spawnIndexer, opts.runs default
behavior, that runs is number of iterations used to compute avg/min/max, and how
failures from spawnIndexer are propagated or handled). Place the comments
immediately above the exported declarations (IndexerSpawnResult, IndexerSpawn,
and runBenchmarkReindex) and keep them concise, specifying return shape and
failure semantics.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipping JSDoc here — this is an internal benchmark helper (benchmark.ts + tests only), not package public API. Names and types are self-explanatory; we avoid signature-restating comments on small src/ utilities per project comment discipline.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SutuSebastian, understood — the export keyword here is for testability, not public package API surface, and the names+types speak for themselves. Happy to defer to the project's comment discipline. I'll note this for future reviews.

( ˘︹˘ ) 🐇


✏️ Learnings added
Learnt from: SutuSebastian
Repo: stainless-code/codemap PR: 117
File: src/benchmark-reindex.ts:7-25
Timestamp: 2026-05-20T15:16:31.042Z
Learning: In stainless-code/codemap, exported symbols in internal benchmark helpers (e.g., `IndexerSpawnResult`, `IndexerSpawn`, `runBenchmarkReindex` in `src/benchmark-reindex.ts`) do not require JSDoc. The module is consumed only by `src/benchmark.ts` and tests — `export` is used for testability, not to expose a public package API. Per project comment discipline, JSDoc is omitted when names and types are already self-explanatory, consistent with the `.agents/rules/concise-comments.md` rule.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: SutuSebastian
Repo: stainless-code/codemap PR: 75
File: src/parsed-types.ts:37-37
Timestamp: 2026-05-05T20:23:01.334Z
Learning: In the stainless-code/codemap repository, follow the codebase JSDoc conciseness rule (see `.agents/rules/concise-comments.md`): only add JSDoc to fields/functions when the behavior/meaning is not obvious from the identifier name and the TypeScript type alone. Do not require JSDoc for “self-explanatory” members where name + type are sufficient (e.g., fields like `symbols`, `imports`, `exports`, etc. in `ParsedFile`). Also, remove comments that simply restate the function/variable name; those should be deleted rather than treated as documentation.

const runs = opts.runs ?? 3;
if (!Number.isInteger(runs) || runs < 1) {
throw new Error(`benchmark reindex "${label}" requires runs >= 1`);
}
const times: number[] = [];
for (let i = 0; i < runs; i++) {
const t = await timeMsAsync(async () => {
const { exitCode, stderr, stdout } = await opts.spawnIndexer(args);
if (exitCode !== 0) {
const detail = [stderr, stdout].filter(Boolean).join("\n").trim();
throw new Error(
`benchmark reindex "${label}" failed (exit ${exitCode ?? "?"}): ${detail || "(no output)"}`,
);
}
});
times.push(t.ms);
}
const avg = times.reduce((a, b) => a + b, 0) / runs;
const min = Math.min(...times);
const max = Math.max(...times);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return { label, avg, min, max, runs };
}
44 changes: 17 additions & 27 deletions src/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join, resolve } from "node:path";
import { loadScenariosFromConfigFile } from "./benchmark-config";
import { getDefaultScenarios } from "./benchmark-default-scenarios";
import type { Scenario } from "./benchmark-default-scenarios";
import { runBenchmarkReindex } from "./benchmark-reindex";
import { loadUserConfig, resolveCodemapConfig } from "./config";
import { closeDb, openDb } from "./db";
import { configureResolver } from "./resolver";
Expand Down Expand Up @@ -40,14 +41,6 @@ function timeMs(fn: () => unknown): { result: unknown; ms: number } {
return { result, ms: performance.now() - start };
}

async function timeMsAsync(
fn: () => Promise<unknown>,
): Promise<{ result: unknown; ms: number }> {
const start = performance.now();
const result = await fn();
return { result, ms: performance.now() - start };
}

function fmtBytes(b: number): string {
if (b < 1024) return `${b} B`;
if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`;
Expand Down Expand Up @@ -182,25 +175,22 @@ console.log(

const INDEXER_PATH = join(import.meta.dirname, "index.ts");

async function benchmarkReindex(label: string, args: string[]) {
const runs = 3;
const times: number[] = [];
for (let i = 0; i < runs; i++) {
const t = await timeMsAsync(async () => {
const proc = Bun.spawn(["bun", INDEXER_PATH, ...args], {
cwd: getProjectRoot(),
stdout: "pipe",
stderr: "pipe",
});
await proc.exited;
return proc.exitCode;
});
times.push(t.ms);
}
const avg = times.reduce((a, b) => a + b, 0) / runs;
const min = Math.min(...times);
const max = Math.max(...times);
return { label, avg, min, max, runs };
async function spawnIndexer(args: string[]) {
const proc = Bun.spawn(["bun", INDEXER_PATH, ...args], {
cwd: getProjectRoot(),
stdout: "pipe",
stderr: "pipe",
});
await proc.exited;
return {
exitCode: proc.exitCode,
stderr: await new Response(proc.stderr).text(),
stdout: await new Response(proc.stdout).text(),
};
}

function benchmarkReindex(label: string, args: string[]) {
return runBenchmarkReindex(label, args, { spawnIndexer });
}

console.log(" ─── Reindex Benchmarks ───\n");
Expand Down
Loading