test: automated performance benchmarking, and the main-vs-#51 measurement - #52
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds automated performance benchmarking across all three layers, then uses it to measure PR #51 against
main. Stacks on #51 (feat/perf-integration).The layers
Criterion microbenchmarks (
crates/cc-core/benches/). The subgraph fixture was rebuilt: the old one made N flatFilenodes with emptychildren, which leaves the child->parent map empty, sofind_render_ancestorreturned on its first probe and the aggregation path never ran. It benchmarked the one input for which the parent map is irrelevant.common::nested_graphnow buildsDirectory > File > CodeBlock > CodeBlocktrees (5 directory levels, 3-way fanout) with edges between leaf blocks, rendered with containers COLLAPSED so both endpoints walk real ancestor chains. The flat case is kept assubgraph_flat_no_hierarchy, renamed to say what it is. New groups: parent-map-sensitive subgraph extraction at 2k/10k/50k nodes,subgraph_fully_expanded,neighborhood_bfs,parse_result_serialize(time + printed payload size), andresolve_hub_ambiguityat 500/1000 definer files with a unique-name control.End-to-end harness (
crates/cc-core/examples/perf_harness.rs). Runs scan -> parse -> resolve, then the query battery the UI issues — subgraph extraction at four render-set sizes, 200 neighborhood queries, 50 edge-detail drill-ins, parse-payload build + serialize — and prints one JSON object of timings, graph stats and payload bytes. It compiles UNCHANGED onmain: it uses only the cc-core API identical on both sides and never namesParseResultas a type. Workload selection is a seeded SplitMix64 stream over sorted id lists, so nothing depends onHashMapiteration order.Synthetic repo generator (
benchmarks/gen_repo.py). Seeded, deterministic, byte-identical between checkouts. Presets 200/2000/10000 modules.--hub-fractionsets the share of modules defining the same names (get,run,new,__init__), which is what drives symbol resolution into its ambiguous tiers.Frontend routing bench (
packages/app/benchmarks/edgeRouting.bench.ts). Deliberately outside thetests/*.test.tsglob. Three scenarios:full_scan_routing(reimplemented in the bench, so identical on every branch — the control),indexed_routing(needsobstacleIndex.ts, reported as skipped where absent), andshipped_redraw(whatever this branch actually does, budget gate included).Runner and CI.
benchmarks/run_all.shdrives every layer with uniform parameters..github/workflows/bench.yml(workflow_dispatch, or abenchPR label) produces the same artifacts on demand, with no thresholds — shared runners are too noisy for a gate, and a gate built on that noise would be ignored within a week.How to run
Full procedure, including the two-worktree branch comparison, in
docs/features/benchmarking.md.Headline:
main@ 30ab9b5 vsfeat/perf-integration@ 1e8903eOne machine (Ryzen 7 7840U), sequential runs, identical inputs.
mainneighborhood_bfs/50000(criterion)resolve_hub_ambiguity/ambiguous_resolve/1000(criterion)subgraph_nested_collapsed/directories_only/10000(criterion)cargo benchinherits[profile.release], which #51 changes (lto = "thin",codegen-units = 1;mainhas cargo defaults). That asymmetry is real shipping behaviour, so the table keeps it — butRESULTS.mdalso carries a profile-matched column (#51 rebuilt withmain's flags). In it, every benchmark #51 did not touch lands within ±2% while the targeted ones keep their full win, so the 5-8% seen elsewhere is thin LTO rather than a code change.The frontend
full_scan_routingcontrol agrees across branches to within 1.1% at all three sizes, which is the evidence that the two runs are comparable.Two findings that go the other way
The "~-22% payload" figure did not reproduce. Measured -7.8% / -8.4% / -9.8% on the three synthetic sizes and -9.1% on this repo. Dumping the payload and measuring per field (via the harness's new
--dump-payload) shows why:signatureis 9.1% of the payload and the FIFTH largest contributor, behindchildren(14.7%),id(12.9%), the node-map keys (11.9%) andspan(11.5%) — so the doc comment calling it "the single largest contributor to the payload" is wrong for this repo. The node id is paid for three times (map key,idfield, and again in the parent'schildrenarray), together 39.5% of the payload. That is where the next payload win is.Skipping the node-map clone is a build-side win, not a serialize-side one. Building the
ParseResultis -39.0% at the large size, but SERIALIZING the slim borrowed form is +51.0% despite emitting fewer bytes. Net build+serialize is -8.8% (large), -0.6% (medium), -15.4% (this repo) — between neutral and modest, not the step change the build number alone suggests. The memory saving stands regardless.Everything else in #51 confirmed, most of it by more than claimed. Details and the full tables are in
benchmarks/RESULTS.md.