Benchmarking & Profiling Ruby Code
Performance work starts with measurement. Pair micro-benchmarks with system-level profiling so we optimise the right code, under the right conditions, and prove the results with data.
Benchmarking pipeline
- Baseline: capture current timings (APM, logs, queue latency). Note Ruby version, CPU model, and environment.
- Micro-benchmark: isolate the hot function with Benchmark::IPS or benchmark-driver; keep inputs realistic.
- Profile: record stack samples (stackprof, rbspy) or allocations (ruby-prof, memory_profiler) to trace hotspots.
- Experiment: change one variable at a time—algorithm, data structure, caching, or concurrency primitive.
- Validate: compare deltas, run regression tests, and document assumptions/risks in the PR.
Toolkit overview
| Tool | Primary use | Command / notes |
|---|---|---|
| Benchmark::IPS | Compare Ruby implementations (IPS, stddev, iterations) | `bundle exec ruby script/bench/lesson_order.rb` |
| benchmark-driver | Run parameterised benchmarks, compare Ruby versions | `bundle exec benchmark-driver bench.yml --rbenv 3.2.2:3.3.0-preview` |
| ruby-prof | Method-level CPU/alloc analysis | `bundle exec ruby script/profile_recommendations.rb` |
| stackprof | Sampling flamegraphs (CPU or wall time) | `STACKPROF=tmp/stackprof.dump bundle exec rails s` + `stackprof --flamegraph` |
| rbspy | Attach to live processes (Sidekiq, Puma) | `rbspy record --pid |
| perf/eBPF (advanced) | Kernel-level profiling to rule out syscalls or network stack issues | Run from host: `sudo perf record -g --pid |
Example: optimizing recommendation scoring
require 'benchmark/ips'
require_relative '../app/services/recommendations/score'
data = JSON.parse(File.read('tmp/lesson_scores.json'))
Benchmark.ips do |x|
x.report('naive score') { Recommendations::Score.naive(data) }
x.report('vectorised score') { Recommendations::Score.vectorised(data) }
x.compare!
end
Bake these scripts into script/benchmark/ and link them in the PR so reviewers and QA can reproduce numbers locally.
Advanced tips
- Pin Ruby version and gemset to avoid noisy comparisons; performance varies across patch releases.
- For multi-threaded code, disable CPU frequency scaling (performance governor) on local machines to reduce jitter.
- Integrate with CI (GitHub Actions artifacts) to keep historical benchmark charts for critical paths (render pipeline, lesson scoring).
- Augment Ruby profiling with system stats (pidstat, perf, eBPF) when investigating kernel-level bottlenecks (syscalls, network).