Optimization without measurement is guessing. This page describes how to profile rypipe pipelines and interpret the results.
crates/rypipe-core/examples/bench_throughput.rs is a self-contained benchmark. It uses a tiny inline TSV-like adapter so the result measures the engine, not an external parser.
Run it:
cargo run --release -p rypipe-core --example bench_throughputOr use the Python wrapper, which runs the benchmark and optionally writes JSON results:
python benchmarks/bench_throughput.py --output .benchmarks/rypipe.jsonbenchmarks/bench_throughput.py accepts only --output; it rebuilds the benchmark from source so the binary always matches HEAD. The crates/rypipe-core/examples/ directory also contains focused micro-benchmarks: bench_scanner_single, bench_push_tier, and bench_blockmasks.
The output reports rows, time, rows per second, MB/s, and RSS. Use these to compare configurations.
Always profile a release build. Debug builds are 10-50x slower and the profile will be dominated by unrelated overhead.
cargo build --release -p rypipe-coreFor symbols without full debug overhead, the workspace defines a profiling Cargo profile (release with debug symbols):
cargo build --profile profiling -p rypipe-corerypipe-core also ships compile-time instrumentation features:
profile: enables timing counters in the engine (for example the push-tier breakdown inTableBuilder). It costs roughly 15% on filtered paths, so use it only for measurement builds.chunk_profile()/reset_chunk_profile()(parallel) anddiscovery_profile()/reset_discovery_profile()(parallel streaming) are always available.alloc-stats: enables allocation tracking (alloc_stats::snapshot(),reset(),print_stats()) used by the bench harness.bench: enables the tier-ladder cost decomposition used bysrc/bench.
On Linux:
perf record -g cargo run --release -p rypipe-core --example bench_throughput
perf report -g 'graph,0.5,caller'Look for time spent in:
find_split_points: splitter is expensive.parse_chunk: parser is the bottleneck.StrColumn::pushor builder append: string allocation/copy dominates.- Arrow export or compute kernels: export is expensive.
- Python GIL-related functions: Python boundary is the bottleneck.
cargo flamegraph produces an SVG flamegraph:
cargo install flamegraph
cargo flamegraph --release -p rypipe-core --example bench_throughputOpen flamegraph.svg in a browser. Wide bars are hot functions. Look for unexpected wide bars such as JSON serialization, Python dict construction, or allocations.
Use /usr/bin/time -v on Linux:
/usr/bin/time -v cargo run --release -p rypipe-core --example bench_throughputLook at Maximum resident set size (kbytes). Compare this across engine modes and chunk counts.
In Python, you can sample RSS during a run with psutil:
import psutil, time, os
proc = psutil.Process(os.getpid())
peak = 0
while running:
peak = max(peak, proc.memory_info().rss)
time.sleep(0.01)
print(f"peak RSS: {peak / 1024 / 1024:.1f} MiB")If the pipeline includes Python stages, wrap the Rust parse in py.allow_threads (PyO3) so the GIL is released. Profile the Python side separately with cProfile:
python -m cProfile -o profile.stats script.py
python -c "import pstats; pstats.Stats('profile.stats').sort_stats('cumtime').print_stats(20)"If most time is in _rypipe native code, optimize Rust. If most time is in Python callables, move work into fused stages or Rust.
When benchmarking, change one variable at a time:
chunks: 1, 2, 4, 8, 16, 32.memory: 64 MiB, 256 MiB, 512 MiB, 1 GiB.use_mmapandprefault: all four combinations.auto_dict: on vs off.field_types: typed parse vs string inference.
Plot throughput vs RSS to find the Pareto frontier.
- Use
bench_throughputas a baseline. - Profile release builds with
perforcargo flamegraph. - Measure RSS separately; throughput is not the only metric.
- Separate Python time from Rust time before optimizing.