Latest run · Sep 26, 2026

Sprout Benchmarks

Performance is a feature. Architecture is measurable.

What three architecture phases changed in Sprout, measured on llvm-project (185,509 files) against the v0.2.0 release. Speed claims count only when the result held in every one of 5 interleaved rounds.

Repository
llvm-project · 185,509 files
Machine
Apple M4 · 10 cores · 16 GB
System
macOS 26.6.2 · Go 1.26.5
Method
warm filesystem cache · 5 interleaved rounds
Compared
bc13e1e v0.2.0→636d292 main after #30
Speedllvm-project

−39%

--json wall time

2.13 s→1.30 s

each round −46% to −38%

Memoryllvm-project

−80%

--json peak RSS

547 MB→109 MB

each round −84% to −77%

Allocationsllvm-project

−80%

--json bytes allocated

1,128 MB→231 MB

Memoryllvm-project

−27%

Tree live memory

88.3 MB→64.5 MB

Memoryllvm-project

−71%

Live heap objects per node

4.03→1.17

Memoryllvm-project

−27%

--entry peak RSS

157 MB→115 MB

each round −27% to −24%

How this was measured · What didn't improve

Five kinds of result, kept apart

A faster command, a smaller heap, fewer allocations, a better algorithm and a more correct answer are different claims. Each result on this page carries exactly one of these labels.

  • SpeedWall time, counted only when faster in every interleaved round
  • MemoryPeak RSS and live heap
  • AllocationsBytes and objects allocated per run
  • ComplexityAsymptotic change in a data structure, not an end-to-end speedup
  • CorrectnessWhat Sprout finds, not how fast

Architecture timeline

Three phases, each merged as its own pull request and measured against the same baseline.

  1. Phase 1 · #27 · f1525e6

    Streaming JSON

    BeforeRecursive JSON materialization: every node marshalled into a temporary buffer, then the whole document re-indented

    AfterOne streaming writer that encodes each value once into a reused buffer; compact by default, --pretty opt-in

    • Speed--json wall time · llvm-project2.13 s → 1.30 s−39%
    • Allocations--json bytes allocated · llvm-project1,128 MB → 231 MB−80%
    • Memory--json peak RSS · llvm-project547 MB → 109 MB−80%
  2. Phase 2 · #29 · 58d2169

    Compact tree

    BeforeOne heap allocation per node, plus a stored absolute path and a separate name string

    AfterOne allocation per directory, names sharing their relative path's bytes, and filesystem paths rebuilt on demand

    • MemoryTree live memory · llvm-project88.3 MB → 64.5 MB−27%
    • MemoryLive heap objects per node · llvm-project4.03 → 1.17−71%
  3. Phase 3 · #30 · 636d292

    Dependency graph

    BeforePer-file import maps, forward edges only, package-level Go resolution against the root module

    AfterInteger file IDs with forward and reverse adjacency, test relationships, file-level Go edges and every go.mod in the tree

    • CorrectnessDependency edges found · kubernetes~27.7k → 47,906

      More edges because nested Go modules now resolve and edges point at the file actually used. The baseline count varied between runs (27,692–27,702); the current count is the same every run.

    • ComplexityDependents lookup · kubernetesO(N) scan · 108,827 ns → O(1) lookup · 0.7 ns

      Complexity improvement, not a measured user-facing speedup. v0.2.0 had no command that asked for dependents, and no current command's runtime is dominated by these lookups.

All results

Every measurement from this run, grouped by kind. Where a per-round range is shown (wall time, peak RSS), the change is the median of the paired per-round changes across all 5 rounds; absolute wall times are medians of steady rounds 2–4. Live-heap and allocation figures are near-deterministic, so their change follows directly from before and after.

Speed Wall time, counted only when faster in every interleaved round

MeasurementRepositoryCommandBaselineCurrentChange
--json wall timellvm-projectsprout --json2.13 s1.30 s−39%each round −46% to −38%
--json --pretty wall timeBaseline has no --pretty flag; its --json was the indented form and is byte-identical to the current --json --pretty.llvm-projectsprout --json --pretty2.11 s1.20 s−40%each round −45% to −37%
--json wall timekubernetessprout --json0.56 s0.33 s−39%each round −50% to −31%
--json --pretty wall timekubernetessprout --json --pretty0.50 s0.30 s−40%each round −43% to −39%
--entry wall timellvm-projectsprout --entry1.11 s1.04 s−6%each round −15% to −1%

Memory Peak RSS and live heap

MeasurementRepositoryCommandBaselineCurrentChange
--json peak RSSllvm-projectsprout --json547 MB109 MB−80%each round −84% to −77%
--json --pretty peak RSSllvm-projectsprout --json --pretty622 MB110 MB−83%each round −84% to −74%
--json peak RSSkubernetessprout --json136 MB29.0 MB−79%each round −82% to −77%
Tree peak RSSllvm-projectsprout139 MB113 MB−18%each round −20% to −15%
--entry peak RSSllvm-projectsprout --entry157 MB115 MB−27%each round −27% to −24%
--ai peak RSSllvm-projectsprout --ai235 MB194 MB−18%each round −21% to −16%
Tree live memoryllvm-projectin-process, 201,148 nodes88.3 MB64.5 MB−27%
Live bytes per nodellvm-projectin-process460 B336 B−27%
Live heap objects per nodellvm-projectin-process4.031.17−71%
Tree live memorykubernetesin-process, 37,183 nodes19.9 MB13.2 MB−34%

Allocations Bytes and objects allocated per run

MeasurementRepositoryCommandBaselineCurrentChange
--json bytes allocatedBaseline allocation varies about 2% between runs (encoding/json buffer growth).llvm-projectsprout --json1,128 MB231 MB−80%
--json allocationsllvm-projectsprout --json2.52 M1.52 M−40%
Allocations to build the treellvm-projectin-process1.21 M0.81 M−33%
Graph build allocationskubernetesin-process, no tests16.55 M16.19 M−2%

Complexity Asymptotic change in a data structure, not an end-to-end speedup

MeasurementRepositoryCommandBaselineCurrentChange
Dependents lookupComplexity improvement, not a measured user-facing speedup. v0.2.0 had no command that asked for dependents, and no current command's runtime is dominated by these lookups.kubernetesin-memory microbenchmarkO(N) scan · 108,827 nsO(1) lookup · 0.7 nsO(N) → O(1)

Correctness What Sprout finds, not how fast

MeasurementRepositoryCommandBaselineCurrentChange
Dependency edges foundMore edges because nested Go modules now resolve and edges point at the file actually used. The baseline count varied between runs (27,692–27,702); the current count is the same every run.kubernetesgraph, no tests~27.7k47,906Changed

What didn't improve

Some changes were faster in some rounds and slower in others. When the rounds disagree, there is no claim: each range below crosses zero, which is exactly why it isn't counted.

  • Plain tree wall time · llvm-project

    0.82 s → 0.76 s No consistent performance change

    −13%0+4%

  • --ai wall time · llvm-project

    2.20 s → 2.25 s No consistent performance change

    −14%0+12%

  • Graph build time · kubernetes

    294 ms → 280 ms No consistent performance change

    −23%0+8%

  • --entry wall time · kubernetes

    0.64 s → 0.57 s No consistent performance change

    Output differs after Phase 3 (more precise edges), so this compares different work.

    −23%0+6%

  • Small repositories (sprout, ky, click, anyhow) finish in under 10 ms, below /usr/bin/time's resolution. In-process timings showed no consistent change.
  • With tests included, the Kubernetes graph has 13,003 files (4,348 tests) and 79,558 edges, and takes about 488 ms to build: the cost future impact and context queries will pay.

The bottleneck moved

JSON serialization is no longer the interesting part of the profile. Filesystem traversal and Git work are now what cost the most. From CPU and allocation profiles of the current revision:

sprout --json, llvm-project (CPU)

Filesystem traversal

  • lstat50%
  • ReadDir35%
  • JSON writing3.8%

% of CPU

Reading directories is also 76% of CPU for the plain tree.

sprout --ai, llvm-project (single indicative runs)

Git subprocesses in --ai, run one after another

  • git status0.60 s
  • git log (90 days, 9.8 MB)0.41 s
  • git ls-files0.36 s

seconds, out of a 2.25 s run

sprout --entry, kubernetes

Source reading and parsing

  • Reading files (CPU)43%
  • Go parser (allocated bytes)58%
  • Graph linking (CPU)1.4%

%

Shares come from separate CPU and allocation profiles, so they don't add up.

Benchmark log

Every controlled run, newest first. Runs are added to src/data/benchmarks/history.json; the page is rebuilt from that file, and the build fails if a percentage disagrees with its before/after values or per-round range, or if an "improved" claim didn't hold in every round.

DateRunComparedRepositoriesEnvironment
2026-09-26latestPhases 1–3: streaming JSON, compact tree, dependency graphbc13e1e → 636d292llvm-project, kubernetes, sprout, ky, click, anyhowApple M4, macOS 26.6.2, Go 1.26.5

Methodology and limitations

Speedup claims were only counted when the direction held across all 5 interleaved rounds.

How the Sep 26, 2026 run was measured
  • One controlled pass: same machine, same session, same inputs. Every revision was built with the same toolchain, and the same benchmark harness was added to each.
  • CLI runs used /usr/bin/time -l with output sent to /dev/null; output size and SHA-256 were recorded in a separate, untimed pass.
  • One warm-up per case, then 5 rounds. Within each round, every revision ran back to back, and their order rotated each round.
  • Speedup claims were only counted when the direction held across all 5 interleaved rounds. Timing is compared per round (current ÷ baseline), so machine-wide slowdowns cancel.
  • For timings, the percentage is the median per-round change (current ÷ baseline within each of the 5 rounds), shown with its range across rounds. The absolute before/after times are medians of the steady rounds 2–4; round 0 ran about 2× slower for every revision alike (a machine-wide warm-up effect), which pairing within rounds cancels.
  • Allocation and memory counts come from Go benchmarks (-benchmem) and in-process heap measurements. They vary little between runs; the spread is noted where it exceeds 1%.

Inputs

RepositoryRevisionFilesRole
llvm-project9f8d81253185,509large heterogeneous repository
kubernetesdfd7b93a31,412Go-heavy, multi-module
sprout636d29244small single-package Go
ky0d59458104small TypeScript control
click06b2a67178small Python control
anyhowc63b27954small Rust control
Limitations
  • Warm page cache only. Every CLI run is a fresh process, but files were already in the OS cache; there are no cold-disk measurements.
  • One machine and one OS. System-call costs differ on Linux, so results there may differ.
  • Small repositories finish below the external timer's resolution, so only in-process timings apply to them.
  • The baseline had no --pretty flag: its --json was the indented form, and it is byte-identical to the current --json --pretty. The default --json is now compact (30.6 MB vs 68.6 MB on llvm-project) with identical decoded values.
  • Kubernetes --entry and --ai produce different output after Phase 3, so their timings compare different work.
  • Absolute times are not comparable to figures quoted in individual pull requests, which wrote output to disk and ran in other sessions.