Profiling Guide¶
This guide covers local CPU profiling of mrrc with cargo flamegraph. It is
the "where is the time going?" companion to
Benchmarking, which covers "how fast is it?" measurement
with criterion, pytest-benchmark, and CodSpeed.
Where profiling fits in the perf workflow¶
The benchmark layers tell you that something is slow or regressed:
- criterion (
cargo bench) gives local walltime numbers per scenario. - CodSpeed runs the single-threaded criterion benches in CI under
simulation mode and flags PR-vs-main drift. Simulation mode serializes
threads, so rayon speedup is only measurable locally — profile and bench
parallel_benchmarkson your own machine.
A flamegraph tells you why: which functions dominate the hot path, and where allocations or copies hide. Profile locally when a bench number moves and the cause isn't obvious, or before starting optimization work to find the real bottleneck.
Prerequisites¶
cargo-flamegraph¶
Install flamegraph for CPU profiling:
macOS requirements:
- Full Xcode (not just Command Line Tools) —
cargo flamegraphsamples viaxctrace, which ships only with the full Xcode install from the App Store or developer.apple.com.
Linux requirements:
perf(usually packaged aslinux-toolsorperf)perlfor the flamegraph script processing
Debug symbols¶
Cargo.toml sets debug = true in [profile.bench], so bench builds keep
the debuginfo flamegraph needs for readable stacks. No extra flags required.
Fixtures¶
The benches read tests/data/fixtures/ (1k / 5k / 10k record files, checked
in). See Benchmarking for regenerating the larger
gitignored fixtures.
CPU profiling with flamegraph¶
All bench targets are criterion harnesses (harness = false). When invoked
through cargo flamegraph, pass --bench after -- so the criterion
binary runs in benchmark mode, and --profile-time <secs> so it loops the
benchmark for a fixed duration instead of spending samples on criterion's
statistical analysis. A trailing filter narrows to one scenario.
Single-threaded read path¶
marc_benchmarks covers the core ISO 2709 read, field-access, and
JSON/XML serialization paths:
cargo flamegraph --bench marc_benchmarks -o flamegraphs/read_1k.svg \
-- --bench --profile-time 10 read_1k_records
Other useful scenario filters: read_10k_records, read_1k_records_from_path,
read_1k_with_field_access, serialize_1k_to_json, serialize_1k_to_xml,
roundtrip_1k_records.
Concurrent read path¶
parallel_benchmarks covers rayon-based concurrent reading against
sequential baselines:
cargo flamegraph --bench parallel_benchmarks -o flamegraphs/parallel_4x.svg \
-- --bench --profile-time 10 parallel_4x_1k_records
Format conversion paths¶
format_benchmarks covers CSV, MARC-in-JSON, MODS, Dublin Core, and
BIBFRAME serialization plus the parser-pool path:
cargo flamegraph --bench format_benchmarks -o flamegraphs/formats.svg \
-- --bench --profile-time 10
Error-handling overhead¶
error_handling_benchmarks compares strict and lenient parsing of clean
versus malformed input:
cargo flamegraph --bench error_handling_benchmarks -o flamegraphs/errors.svg \
-- --bench --profile-time 10
Sampling frequency¶
The default sampling rate is 99 Hz. For short hot paths, raise it with
flamegraph's --freq flag (before the --):
cargo flamegraph --freq 1000 --bench marc_benchmarks \
-o flamegraphs/read_1k_1khz.svg -- --bench --profile-time 10 read_1k_records
Interpreting flamegraphs¶
- Width = time spent in function (wider = more time)
- Height = call stack depth (taller = deeper nesting)
- Color = random (for visual distinction)
- X-axis = alphabetical merge order, not time
- Y-axis = call stack
Common patterns to look for¶
- Flat top = pure computation or tight loop
- Sawtooth pattern = recursive calls or repeated function sequences
- Thin slices = low-overhead functions called many times
- Wide base = time spent in main execution path
Optimization checklist¶
Hard-won rules from past mrrc profiling work:
- Measure before optimizing. Architectural changes that aren't on the critical path produce zero improvement; a flamegraph confirms the change targets where time actually goes.
- Allocations on the hot path are the usual culprit. The biggest past
wins came from eliminating per-record
StringandVecallocations in frequently-called parse functions, not from clever restructuring. - Find where allocations happen, not just how many. The call stack matters: one allocation in a per-record loop outweighs many in setup code.
- Re-bench after every change. Small incremental optimizations with
direct criterion measurement beat large speculative refactors. Compare
cargo benchoutput before and after each commit.
Troubleshooting¶
macOS: "tool 'xctrace' requires Xcode"¶
Command Line Tools alone are not enough — install the full Xcode from the App Store or developer.apple.com, then point the tools at it:
Linux: "perf not found"¶
sudo apt-get install linux-tools-common linux-tools-generic # Debian/Ubuntu
sudo dnf install perf # Fedora/RHEL
Flamegraph output empty or sparse¶
- Ensure the run is long enough to collect samples — raise
--profile-time - Increase sampling frequency with
--freq - Confirm the scenario filter matches a bench name (no match = instant exit)