CodeGraph ships a reproducible comparison harness in codegraph-bench. It is
separate from correctness: a faster result never relaxes schema, golden, or graph
parity.
For each pinned corpus, --run compares the release Rust CLI with a built copy
of the pinned upstream TypeScript CLI on byte-identical source trees. It
records:
- cold and warm full-index wall time;
- one-file incremental synchronization wall time;
- cold and warm query latency (median, MAD, p50, and p99);
- peak RSS samples when the host exposes them;
- resulting database size; and
- a Rust-only in-process parse metric, clearly separated because upstream has no equivalent entry point.
Before timing a corpus, the pipeline indexes both arms and requires normalized
SQLite .schema equality. A schema mismatch aborts the run rather than comparing
non-equivalent work.
The source registry in
crates/codegraph-bench/src/corpus.rs
is authoritative. Each row pins a repository URL, full commit SHA, the release tag
the commit was taken from (when there is one), benchmark subdirectory, expected
non-blank LOC, and source-file count. --fetch-corpora creates immutable
checkouts under ignored bench/corpora/ and refuses an existing checkout at the
wrong commit.
| Corpus | Repository | Tag | Commit | Benchmark directory | Used for |
|---|---|---|---|---|---|
fd-small |
sharkdp/fd |
— | 25461e5ce13dc12ff2a75993285a87e99b33db2d |
src |
timing (Rust) |
tokio-medium |
tokio-rs/tokio |
— | ecb5125a6787b9d8eb818b1b00973bcd55ae77c0 |
tokio/src |
timing (Rust) |
typescript-large |
microsoft/TypeScript |
— | 7964e22f2b85f16e520f0e902c7fd7b6f0c15416 |
src |
timing (TypeScript) |
express-medium |
expressjs/express |
— | dae209ae6559c29cfca2a1f4414c51d89ea643d5 |
. |
timing (JavaScript) |
leveldb-small |
google/leveldb |
1.23 |
99b3c03b3284f5886f9ef9a4ef703d57373e61be |
. |
precision review (C++) |
gson-medium |
google/gson |
gson-parent-2.14.0 |
3ff35d6269894901ab8006258395aafc4b9765cd |
gson |
precision review (Java) |
newtonsoft-large |
JamesNK/Newtonsoft.Json |
13.0.4 |
4e13299d4b0ec96bd4df9954ef646bd2d1b5bf2a |
Src |
precision review (C#) |
alamofire-small |
Alamofire/Alamofire |
5.12.2 |
bda9ed57d72988a3a2ada33d824583541f86eac6 |
Source |
precision review (Swift) |
redis-medium |
redis/redis |
8.10.2 |
498ecd0d6d007db11ddb3aea9428552598a78622 |
src |
timing (C with function-pointer calls) |
kong-medium |
Kong/kong |
3.9.3 |
a643428bc4d5397152164a63bcc0f8bc65fce69d |
kong |
timing (Lua) |
A unit test in codegraph-bench checks that every registry row appears in this
table with its commit, tag, and benchmark directory.
Inspect the registry and local state:
cargo run --locked -p codegraph-bench --bin bench -- --list-corporaFetch the exact commits (networked, explicit operation):
cargo run --locked -p codegraph-bench --bin bench -- --fetch-corporaBoth take --corpora-root <dir> to use another directory of checkouts, such as
one shared between worktrees.
-
Build the shipped Rust binary with the same locked release profile used by release workflows:
cargo build --locked --release -p codegraph-rs
-
Materialize the pinned upstream checkout at
reference/colbyand buildreference/colby/dist/bin/codegraph.jsaccording to that checkout'sRUN.md. Record its exact commit in the result artifact. -
Fetch the corpus pins and verify
--list-corporareports the expected commit, file count, and LOC for every selected corpus. -
Use an otherwise idle machine. Record CPU, memory, OS, kernel, Node, Rust, Rust implementation commit, and upstream implementation commit.
The full pipeline is Linux-oriented because peak-RSS collection and cold-cache handling use Linux facilities when available. If the kernel does not allow page cache drops, the harness records best-effort userspace eviction; do not label that series equivalent to a privileged cold-cache run without the emitted caveat.
Use at least two runs because run one is discarded. A normal evidence run should use enough samples for stable p99 interpretation; twelve is the CLI default.
cargo run --locked --release -p codegraph-bench --bin bench -- \
--run \
--runs 12 \
--corpora all \
--out target/benchmark-results.json \
--report-md docs/benchmark-results.mdTo isolate one corpus during harness development:
cargo run --locked --release -p codegraph-bench --bin bench -- \
--run --runs 12 --corpora fd-small \
--out target/benchmark-fd.jsonTo re-render Markdown without rerunning measurements:
cargo run --locked -p codegraph-bench --bin bench -- \
--render-md target/benchmark-results.json docs/benchmark-results.md--ab times a cold init of two CodeGraph binaries on the same pinned corpora,
and checks that each binary builds the same graph on every run. It compares two
builds of this project, such as release builds of a base commit and of a change;
the upstream CLI is not involved.
cargo build --locked --release -p codegraph-rs -p codegraph-bench
target/release/bench --ab \
--baseline /path/to/baseline/codegraph \
--candidate target/release/codegraph \
--corpora fd-small,tokio-medium,typescript-large,express-medium \
--corpora-root bench/corpora \
--threads 32,4 \
--runs 6 \
--out target/ab.json \
--report-md target/ab.md--corpora takes registry names, comma-separated or repeated, or all.
--corpora-root is a directory of registry checkouts and defaults to
bench/corpora. Before anything runs, a checkout that is missing, at another
commit than its pin, or modified (non-empty git status --porcelain) is
refused. --runs counts every run of a cell, the warm-up included, and must be
at least 2. Without --out the JSON report goes to stdout; progress goes to
stderr.
For each corpus, thread count, and run, the harness:
- copies the corpus's benchmark directory into a fresh temporary project,
leaving out
.git,.codegraph, and.codegraph-wsland skipping symlinks; - runs
<binary> init <project>withRAYON_NUM_THREADS=<threads>,CODEGRAPH_NO_DAEMON=1, andCODEGRAPH_NO_WATCH=1, and withCODEGRAPH_DIRremoved from the environment; - records the wall time from spawn to exit, polled every millisecond, and the
peak RSS: on Linux the child's own
VmHWM, read from/proc/<pid>/statusat each poll, sincewait4'sru_maxrssalso counts the address space the child was forked from (the harness's, which grows with every database it canonicalizes);ru_maxrsson other Unix hosts; not collected on Windows; - checks
.codegraph/codegraph.dbwithPRAGMA quick_check, canonicalizes it through the equivalence oracle, and fingerprints it; and - deletes the project.
The CLI has no thread-count flag. Indexing parses and resolves on rayon's global
thread pool, which takes its size from RAYON_NUM_THREADS, so each thread count
is a separate cell with that variable set.
The binaries alternate: even runs time the baseline first and odd runs the
candidate first. The first run of every (corpus, binary, thread count) cell is a
warm-up. It is checked like every other run but left out of the statistics.
"Cold" means that no index exists when init starts; the page cache is warm,
because the project was just copied.
The fingerprint hashes each canonical surface with SHA-256: for nodes,
edges, unresolved_refs, and files, the compact JSON array of the
surface's canonical rows; for schema, the normalized schema text. The graph
hash is the SHA-256 of the five lines <surface> <sha256>, in that order.
The harness stops with a non-zero exit status, and writes no report, when:
- a run exits non-zero, is killed by a signal, or runs longer than 30 minutes;
- a run exits 0 but leaves no database, or leaves one that fails
PRAGMA quick_checkor canonicalization; or - two runs of the same binary on one corpus produce different graph hashes, at the same or at different thread counts.
A difference between the baseline and candidate graphs is not a failure. The
report records it per corpus, so a change that alters the graph can still be
timed against its row growth; --graph-diff shows the rows.
The JSON report has kind codegraph-ab and schema_version 1. Its fields:
argv: thebenchcommand line.environment: CPU model, logical CPUs, memory, OS, kernel,rustc --version, the harness commit and whether its worktree was dirty, and how RSS was collected.settings: the invocation, environment overrides, thread variable, thread counts, runs, discarded warm-ups, run order, run timeout, and budget limits.baseline,candidate: the path, SHA-256, size, and--versionline of each binary.corpora[]: the name, pinned commit and tag, source directory, copied files, and skipped symlinks of each corpus.corpora[].graph:identical,differing_surfaces, and each binary's fingerprint:hash, per-surface hashes, and canonical row counts.corpora[].cells[]: one entry per thread count, holding every run of each binary (wall_ms,peak_rss_kb,warmup,canonical_hash), the median and MAD of the measured runs, canonical rows per second at the median, the candidate/baselineratioof wall time, peak RSS, rows, and rows per second, and thebudgetverdict.
--report-md renders the same data as a Markdown table with one row per corpus
and thread count, below a table of the binaries, their hashes, and the
environment.
Publishing an A/B result follows the evidence requirements: the report carries the binary hashes, the environment, every run, and the graph fingerprints.
Measured against a release build of the base commit, a change stays within the project's performance budget when:
- the cold
initmedian is at most 1.10× the baseline's. A slowerinitis still within budget when the candidate graph has more canonical rows and its canonical rows per second stay at or above 0.90× the baseline's; - a single-file
synctakes at most 1.15× the baseline's time; - the
explorep50 is at most 1.10× the baseline's; and - the peak RSS median is at most 1.15× the baseline's.
--ab measures the init and peak RSS terms and reports them per cell as
budget.init_wall_ok and budget.peak_rss_ok. It never fails a run for being
over budget; sync and explore are measured separately. A change over budget
is fixed, or kept only with its A/B report attached for the owner to accept.
--graph-diff <LEFT> <RIGHT> compares two canonical graphs. Each side is a
CodeGraph SQLite database or a golden directory (nodes.json, edges.json,
refs.json, files.json, schema.sql), so a fresh index can be compared with
a committed golden or with another index:
target/release/bench --graph-diff reference/golden/typescript \
/tmp/cg-fixture-typescript/.codegraph/codegraph.dbA database is copied, together with its -wal when one exists, into a temporary
directory and read only there, so the input never gains SQLite sidecars. The
copy must pass PRAGMA quick_check.
Rows compare as multisets of canonical JSON. A row only on the left is removed, a row only on the right is added, and a node or file whose attributes changed appears once on each side. The report gives the removed and added counts of each surface and whether the schema changed, then a table of the changed groups, then every changed row under its group, all in a fixed order. A group is (surface, language, kind, resolvedBy):
- a node groups by its language and kind;
- an edge by its source node's language, its kind, and
metadata.resolvedBy; a synthesized edge without one showssynthesizedBy:<metadata.synthesizedBy>; - an unresolved reference by its language and
reference_kind; and - a file by its language.
The exit status is 0 when the graphs are identical and 1 when they differ. It is
2 when an input cannot be read, loaded, or canonicalized, and nothing is printed
to stdout in that case. --graph-diff runs on its own; other options are
ignored.
A publishable result must include:
- raw JSON and generated Markdown from the same run;
- clean Git status or an explicit diff description;
- both implementation commit SHAs;
- exact corpus pins and observed counts;
- environment fields emitted by the harness;
- run count, discarded-first policy, and cache mode;
- schema equality for every corpus;
- every raw sample, median, MAD, p50, and p99;
- failures, timeouts, unsupported RSS, or cache-eviction caveats.
Do not copy a latency number into README, AGENTS, or release notes without linking to such an artifact. Results are snapshots, not timeless product guarantees.