Skip to content

Commit 98bcf37

Browse files
Merge pull request #88 from eglitobias/benchmark-tooling
Add benchmark comparison tooling
2 parents 2c07948 + 66378e1 commit 98bcf37

6 files changed

Lines changed: 205 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ bin/*
1616
*.bundle
1717
Makefile
1818
profiling/dumps/*
19+
benchmarks/results/*

ChangeLog.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
## Changes between 2.7.0 and 2.8.0 (unreleased)
22

3-
No changes yet.
3+
### Performance Improvements in Frame Decoding and Encoding
4+
5+
Replacing `x == nil` with `x.nil?` in the frame layer hot path yields a
6+
consistent **+15–18% throughput improvement** in `Frame.decode_header`
7+
(called on every received frame) and **+12–14%** in `HeartbeatFrame.encode`,
8+
across Ruby 3.3, 3.4, and 4.0.
9+
10+
See benchmarks/BENCHMARKS.md for instructions on how to reproduce these numbers on your machine.
411

512

613
## Changes between 2.6.0 and 2.7.0 (Mar 31, 2026)

benchmarks/BENCHMARKS.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Benchmarking
2+
3+
## Running the suite
4+
5+
```bash
6+
ruby benchmarks/run_all.rb
7+
```
8+
9+
Results are saved to `benchmarks/results/` with a timestamp.
10+
11+
## Comparing two commits
12+
13+
`benchmark_compare.sh` runs the full suite on both commits and prints a colour-coded delta summary. The after-sha defaults to `HEAD`.
14+
15+
```bash
16+
bash benchmarks/benchmark_compare.sh <before-sha> [after-sha] [ruby-binary ...]
17+
```
18+
19+
With the current ruby only:
20+
```bash
21+
bash benchmarks/benchmark_compare.sh 6d857de
22+
```
23+
24+
With multiple rubies (asdf example):
25+
```bash
26+
bash benchmarks/benchmark_compare.sh 6d857de HEAD \
27+
$(asdf where ruby 3.3.11)/bin/ruby \
28+
$(asdf where ruby 3.4.9)/bin/ruby \
29+
$(asdf where ruby 4.0.2)/bin/ruby
30+
```
31+
32+
With rbenv:
33+
```bash
34+
bash benchmarks/benchmark_compare.sh 6d857de HEAD \
35+
$(rbenv prefix 3.3.11)/bin/ruby \
36+
$(rbenv prefix 3.4.9)/bin/ruby
37+
```
38+
39+
You can also diff any two saved result files directly:
40+
```bash
41+
ruby benchmarks/compare_results.rb benchmarks/results/before_3_4_9.txt benchmarks/results/after_3_4_9.txt
42+
```
43+
44+
## Getting reliable results
45+
46+
Benchmark results are sensitive to system load. For trustworthy numbers:
47+
48+
- Run on an otherwise idle machine (close browsers, pause background processes)
49+
- Results within ±5% of each other between runs are noise — `compare_results.rb` filters these out automatically
50+
- If a benchmark shows a large error margin (e.g. `±10%` in the raw output), discard that result and re-run
51+
52+
## Individual benchmarks
53+
54+
| Script | What it covers |
55+
|---|---|
56+
| `benchmarks/frame_encoding.rb` | Frame encode/decode — the core hot path |
57+
| `benchmarks/table_encoding.rb` | AMQP table encode/decode |
58+
| `benchmarks/method_encoding.rb` | Method/properties encode/decode, Basic.Publish |
59+
| `benchmarks/pack_unpack.rb` | Low-level pack/unpack primitives |
60+
61+
Run any of them directly with `ruby benchmarks/<name>.rb`.

benchmarks/benchmark_compare.sh

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env bash
2+
# Benchmarks two commits and prints a before/after delta summary.
3+
#
4+
# Usage:
5+
# bash benchmarks/benchmark_compare.sh <before-sha> [after-sha] [ruby-binary ...]
6+
#
7+
# Defaults: after-sha = HEAD, ruby-binary = ruby
8+
#
9+
# Examples:
10+
# bash benchmarks/benchmark_compare.sh 6d857de
11+
# bash benchmarks/benchmark_compare.sh 6d857de HEAD
12+
# bash benchmarks/benchmark_compare.sh 6d857de HEAD ruby3.3 ruby3.4
13+
#
14+
# With asdf:
15+
# bash benchmarks/benchmark_compare.sh 6d857de HEAD \
16+
# $(asdf where ruby 3.3.11)/bin/ruby \
17+
# $(asdf where ruby 3.4.9)/bin/ruby
18+
#
19+
# With rbenv:
20+
# bash benchmarks/benchmark_compare.sh 6d857de HEAD \
21+
# $(rbenv prefix 3.3.11)/bin/ruby \
22+
# $(rbenv prefix 3.4.9)/bin/ruby
23+
set -euo pipefail
24+
25+
if [[ $# -lt 1 ]]; then
26+
echo "Usage: $0 <before-sha> [after-sha] [ruby-binary ...]"
27+
exit 1
28+
fi
29+
30+
DIR="$(cd "$(dirname "$0")" && pwd)"
31+
REPO="$(cd "$DIR/.." && pwd)"
32+
33+
# Resolve to full SHAs immediately, before any git checkout changes HEAD
34+
BEFORE_LABEL=${1}
35+
AFTER_LABEL=${2:-HEAD}
36+
BEFORE=$(git -C "$REPO" rev-parse "$BEFORE_LABEL")
37+
AFTER=$(git -C "$REPO" rev-parse "$AFTER_LABEL")
38+
shift 2 2>/dev/null || shift $#
39+
RUBIES=("${@:-ruby}")
40+
RESULTS="$DIR/results"
41+
mkdir -p "$RESULTS"
42+
43+
CURRENT_BRANCH=$(git -C "$REPO" rev-parse --abbrev-ref HEAD)
44+
45+
restore() {
46+
git -C "$REPO" checkout --quiet "$CURRENT_BRANCH"
47+
}
48+
trap restore EXIT
49+
50+
run_suite() {
51+
local label=$1 sha=$2 ruby_bin=$3
52+
local ruby_label
53+
ruby_label=$("$ruby_bin" -e 'print "#{RUBY_VERSION}"')
54+
local tag="${label}_${ruby_label//./_}"
55+
local out="$RESULTS/${tag}.txt"
56+
57+
echo
58+
echo "=== [$ruby_label] $label ($sha) ==="
59+
git -C "$REPO" checkout --quiet "$sha"
60+
"$ruby_bin" -e 'require "benchmark/ips"' 2>/dev/null || \
61+
"$ruby_bin" -S gem install benchmark-ips --quiet --no-document
62+
# Prepend ruby's own directory to PATH so that bare `ruby` calls in older
63+
# versions of run_all.rb (before the RbConfig.ruby fix) also use the right binary
64+
PATH="$(dirname "$ruby_bin"):$PATH" "$ruby_bin" "$DIR/run_all.rb" 2>&1 | tee "$out"
65+
echo ">>> Saved: $out"
66+
}
67+
68+
for ruby_bin in "${RUBIES[@]}"; do
69+
run_suite "before" "$BEFORE" "$ruby_bin"
70+
run_suite "after" "$AFTER" "$ruby_bin"
71+
done
72+
73+
# Restore before printing the summary so compare_results.rb is available
74+
restore
75+
76+
echo
77+
echo "=================================================================="
78+
echo "Summary: before ($BEFORE_LABEL) vs after ($AFTER_LABEL)"
79+
echo "=================================================================="
80+
81+
for ruby_bin in "${RUBIES[@]}"; do
82+
ruby_label=$("$ruby_bin" -e 'print "#{RUBY_VERSION}"')
83+
before_file="$RESULTS/before_${ruby_label//./_}.txt"
84+
after_file="$RESULTS/after_${ruby_label//./_}.txt"
85+
[[ -f "$before_file" && -f "$after_file" ]] || continue
86+
87+
echo
88+
echo " Ruby $ruby_label ($BEFORE_LABEL vs $AFTER_LABEL)"
89+
"$ruby_bin" "$DIR/compare_results.rb" "$before_file" "$after_file"
90+
done

benchmarks/compare_results.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env ruby
2+
# encoding: utf-8
3+
# frozen_string_literal: true
4+
#
5+
# Prints a before/after delta summary from two benchmark result files.
6+
# Changes within ±5% are considered noise and hidden.
7+
#
8+
# Usage: ruby benchmarks/compare_results.rb <before.txt> <after.txt>
9+
10+
NOISE_THRESHOLD = 5.0
11+
12+
def parse_ips(file)
13+
results = {}
14+
in_comparison = false
15+
16+
File.readlines(file).each do |line|
17+
in_comparison = true if line.strip == "Comparison:"
18+
in_comparison = false if in_comparison && line.strip.empty?
19+
next unless in_comparison
20+
21+
results[$1.strip] = $2.to_f if line =~ /^(.+?):\s+([\d.]+) i\/s/
22+
end
23+
24+
results
25+
end
26+
27+
before = parse_ips(ARGV[0])
28+
after = parse_ips(ARGV[1])
29+
30+
deltas = (before.keys & after.keys).filter_map do |name|
31+
b, a = before[name], after[name]
32+
pct = ((a - b) / b * 100).round(1)
33+
pct.abs >= NOISE_THRESHOLD ? [name, pct] : nil
34+
end.sort_by { |_, pct| -pct }
35+
36+
if deltas.empty?
37+
puts " No changes beyond ±#{NOISE_THRESHOLD}% noise threshold"
38+
else
39+
deltas.each do |name, pct|
40+
color = pct >= 0 ? "\e[32m" : "\e[31m"
41+
sign = pct >= 0 ? "+" : ""
42+
puts " #{("%-45s" % name)} #{color}#{sign}#{pct}%\e[0m"
43+
end
44+
end

benchmarks/run_all.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
puts "\n>>> Running #{benchmark}..."
4747
puts
4848

49-
output = `ruby #{benchmark_path} 2>&1`
49+
output = `#{RbConfig.ruby} #{benchmark_path} 2>&1`
5050
puts output
5151

5252
f.puts ">>> #{benchmark}"

0 commit comments

Comments
 (0)