Skip to content

Commit 271b404

Browse files
Substantial efficiency improvements
1 parent c96dc29 commit 271b404

33 files changed

+1199
-147
lines changed

ChangeLog.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,30 @@
1-
## Changes between 2.3.4 and 2.3.5 (in development)
1+
## Changes between 2.4.0 and 2.5.0 (in development)
22

33
No changes yet.
44

5+
## Changes between 2.3.4 and 2.4.0 (Dec 30, 2025)
6+
7+
### Bug Fixes
8+
9+
* Fixed `BadResponseError` constructor which referenced undefined variable `data` instead of `actual`
10+
11+
### Performance Improvements
12+
13+
Optimized encoding and decoding hot paths:
14+
15+
* Built-in `Q>`/`q>` packing/unpacking directives are 6-7x faster than the original implementation (that originally targeted Ruby 1.8.x)
16+
* Switched to `unpack1` instead of `unpack().first` throughout
17+
* Use `byteslice` instead of `slice` for binary string operations
18+
* Use `getbyte` for single byte access (4x faster than alternatives)
19+
* Adopted `frozen_string_literal` pragma
20+
21+
The improvements on Ruby 3.4 are very meaningful:
22+
23+
* `AMQ::Pack.pack_uint64_big_endian`: about 6.6x faster
24+
* `AMQ::Pack.unpack_uint64_big_endian`: about 7.2x faster
25+
* `Basic.Deliver.decode`: about 1.7x faster
26+
* `Basic.Ack/Nack/Reject.encode`: about 2.5x faster
27+
528

629
## Changes between 2.3.3 and 2.3.4 (May 12, 2025)
730

@@ -16,13 +39,13 @@ GitHub issue: [#80](https://github.com/ruby-amqp/amq-protocol/pull/80)
1639

1740
### Improved Compatibility with Ruby 3.4
1841

19-
Contribiuted by @BenTalagan.
42+
Contributed by @BenTalagan.
2043

2144
GitHub issue: [#79](https://github.com/ruby-amqp/amq-protocol/pull/79)
2245

2346
### Support Binary-Encoded Frozen Strings for Payloads
2447

25-
Contribiuted by @djrodgerspryor.
48+
Contributed by @djrodgerspryor.
2649

2750
GitHub issue: [#78](https://github.com/ruby-amqp/amq-protocol/pull/78)
2851

Gemfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ group :development do
1010
gem "rake"
1111
end
1212

13+
group :benchmark do
14+
gem "benchmark-ips", "~> 2.12"
15+
gem "benchmark-memory", "~> 0.2"
16+
end
17+
1318
group :test do
1419
gem "rspec", ">= 3.8.0"
1520
gem "rspec-its"

benchmarks/frame_encoding.rb

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env ruby
2+
# encoding: utf-8
3+
# frozen_string_literal: true
4+
5+
$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
6+
7+
require "amq/protocol/client"
8+
require "benchmark/ips"
9+
10+
puts
11+
puts "-" * 80
12+
puts "Frame Encoding Benchmarks on #{RUBY_DESCRIPTION}"
13+
puts "-" * 80
14+
15+
# Test data
16+
SMALL_PAYLOAD = "x" * 100
17+
MEDIUM_PAYLOAD = "x" * 1024
18+
LARGE_PAYLOAD = "x" * 16384
19+
20+
CHANNEL = 1
21+
22+
Benchmark.ips do |x|
23+
x.config(time: 5, warmup: 2)
24+
25+
x.report("Frame.encode(:method, small)") do
26+
AMQ::Protocol::Frame.encode(:method, SMALL_PAYLOAD, CHANNEL)
27+
end
28+
29+
x.report("Frame.encode(:method, medium)") do
30+
AMQ::Protocol::Frame.encode(:method, MEDIUM_PAYLOAD, CHANNEL)
31+
end
32+
33+
x.report("Frame.encode(:body, large)") do
34+
AMQ::Protocol::Frame.encode(:body, LARGE_PAYLOAD, CHANNEL)
35+
end
36+
37+
x.report("MethodFrame.new + encode") do
38+
frame = AMQ::Protocol::MethodFrame.new(SMALL_PAYLOAD, CHANNEL)
39+
frame.encode
40+
end
41+
42+
x.report("BodyFrame.new + encode") do
43+
frame = AMQ::Protocol::BodyFrame.new(MEDIUM_PAYLOAD, CHANNEL)
44+
frame.encode
45+
end
46+
47+
x.report("HeartbeatFrame.encode") do
48+
AMQ::Protocol::HeartbeatFrame.encode
49+
end
50+
51+
x.compare!
52+
end
53+
54+
puts
55+
puts "-" * 80
56+
puts "Frame Header Decoding"
57+
puts "-" * 80
58+
59+
# Encoded frame headers
60+
METHOD_HEADER = [1, 0, 1, 0, 0, 0, 100].pack("CnN") # type=1, channel=1, size=100
61+
BODY_HEADER = [3, 0, 5, 0, 0, 16, 0].pack("CnN") # type=3, channel=5, size=4096
62+
63+
Benchmark.ips do |x|
64+
x.config(time: 5, warmup: 2)
65+
66+
x.report("Frame.decode_header (method)") do
67+
AMQ::Protocol::Frame.decode_header(METHOD_HEADER)
68+
end
69+
70+
x.report("Frame.decode_header (body)") do
71+
AMQ::Protocol::Frame.decode_header(BODY_HEADER)
72+
end
73+
74+
x.compare!
75+
end

benchmarks/method_encoding.rb

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env ruby
2+
# encoding: utf-8
3+
# frozen_string_literal: true
4+
5+
$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
6+
7+
require "amq/protocol/client"
8+
require "benchmark/ips"
9+
10+
puts
11+
puts "-" * 80
12+
puts "AMQP Method Encoding/Decoding Benchmarks on #{RUBY_DESCRIPTION}"
13+
puts "-" * 80
14+
15+
FRAME_SIZE = 131072 # 128KB, typical default
16+
17+
# Common message properties
18+
BASIC_PROPERTIES = {
19+
content_type: "application/json",
20+
delivery_mode: 2,
21+
priority: 0,
22+
headers: { "x-custom" => "value" }
23+
}.freeze
24+
25+
MINIMAL_PROPERTIES = {
26+
delivery_mode: 2
27+
}.freeze
28+
29+
# Payloads
30+
SMALL_BODY = '{"id":1}'.freeze
31+
MEDIUM_BODY = ('x' * 1024).freeze
32+
LARGE_BODY = ('x' * 65536).freeze
33+
34+
puts "=== Basic.Publish (Full Message Encoding) ==="
35+
puts "This is the critical hot path for publishing messages"
36+
puts
37+
38+
Benchmark.ips do |x|
39+
x.config(time: 5, warmup: 2)
40+
41+
x.report("Publish small (8B) + minimal props") do
42+
AMQ::Protocol::Basic::Publish.encode(
43+
1, # channel
44+
SMALL_BODY, # payload
45+
MINIMAL_PROPERTIES,
46+
"", # exchange
47+
"test.queue", # routing_key
48+
false, # mandatory
49+
false, # immediate
50+
FRAME_SIZE
51+
)
52+
end
53+
54+
x.report("Publish small (8B) + full props") do
55+
AMQ::Protocol::Basic::Publish.encode(
56+
1,
57+
SMALL_BODY,
58+
BASIC_PROPERTIES,
59+
"",
60+
"test.queue",
61+
false,
62+
false,
63+
FRAME_SIZE
64+
)
65+
end
66+
67+
x.report("Publish medium (1KB) + full props") do
68+
AMQ::Protocol::Basic::Publish.encode(
69+
1,
70+
MEDIUM_BODY,
71+
BASIC_PROPERTIES,
72+
"",
73+
"test.queue",
74+
false,
75+
false,
76+
FRAME_SIZE
77+
)
78+
end
79+
80+
x.report("Publish large (64KB) + full props") do
81+
AMQ::Protocol::Basic::Publish.encode(
82+
1,
83+
LARGE_BODY,
84+
BASIC_PROPERTIES,
85+
"",
86+
"test.queue",
87+
false,
88+
false,
89+
FRAME_SIZE
90+
)
91+
end
92+
93+
x.compare!
94+
end
95+
96+
# Create sample encoded methods for decoding benchmarks
97+
puts
98+
puts "=== Method Decoding ==="
99+
100+
# Simulate encoded Basic.Deliver frame payload (after class/method ID)
101+
# Basic.Deliver: consumer_tag(shortstr), delivery_tag(longlong), redelivered(bit), exchange(shortstr), routing_key(shortstr)
102+
def make_deliver_payload(consumer_tag, delivery_tag, exchange, routing_key)
103+
buffer = String.new(encoding: 'BINARY')
104+
buffer << consumer_tag.bytesize.chr
105+
buffer << consumer_tag
106+
buffer << AMQ::Pack.pack_uint64_big_endian(delivery_tag)
107+
buffer << "\x00" # redelivered = false
108+
buffer << exchange.bytesize.chr
109+
buffer << exchange
110+
buffer << routing_key.bytesize.chr
111+
buffer << routing_key
112+
buffer
113+
end
114+
115+
DELIVER_PAYLOAD_SHORT = make_deliver_payload("ctag", 1, "", "q")
116+
DELIVER_PAYLOAD_TYPICAL = make_deliver_payload("bunny-consumer-12345", 999999, "amq.topic", "events.user.created")
117+
118+
Benchmark.ips do |x|
119+
x.config(time: 5, warmup: 2)
120+
121+
x.report("Basic.Deliver.decode (short)") do
122+
AMQ::Protocol::Basic::Deliver.decode(DELIVER_PAYLOAD_SHORT)
123+
end
124+
125+
x.report("Basic.Deliver.decode (typical)") do
126+
AMQ::Protocol::Basic::Deliver.decode(DELIVER_PAYLOAD_TYPICAL)
127+
end
128+
129+
x.compare!
130+
end
131+
132+
puts
133+
puts "=== Properties Encoding/Decoding ==="
134+
135+
Benchmark.ips do |x|
136+
x.config(time: 5, warmup: 2)
137+
138+
x.report("encode_properties (minimal)") do
139+
AMQ::Protocol::Basic.encode_properties(100, MINIMAL_PROPERTIES)
140+
end
141+
142+
x.report("encode_properties (full)") do
143+
AMQ::Protocol::Basic.encode_properties(1024, BASIC_PROPERTIES)
144+
end
145+
146+
x.compare!
147+
end
148+
149+
# Create encoded properties for decode benchmark
150+
ENCODED_MINIMAL_PROPS = AMQ::Protocol::Basic.encode_properties(100, MINIMAL_PROPERTIES)
151+
ENCODED_FULL_PROPS = AMQ::Protocol::Basic.encode_properties(1024, BASIC_PROPERTIES)
152+
153+
# Skip the first 12 bytes (class_id, weight, body_size)
154+
PROPS_DATA_MINIMAL = ENCODED_MINIMAL_PROPS[12..-1]
155+
PROPS_DATA_FULL = ENCODED_FULL_PROPS[12..-1]
156+
157+
Benchmark.ips do |x|
158+
x.config(time: 5, warmup: 2)
159+
160+
x.report("decode_properties (minimal)") do
161+
AMQ::Protocol::Basic.decode_properties(PROPS_DATA_MINIMAL)
162+
end
163+
164+
x.report("decode_properties (full)") do
165+
AMQ::Protocol::Basic.decode_properties(PROPS_DATA_FULL)
166+
end
167+
168+
x.compare!
169+
end
170+
171+
puts
172+
puts "=== Other Common Methods ==="
173+
174+
Benchmark.ips do |x|
175+
x.config(time: 5, warmup: 2)
176+
177+
x.report("Basic.Ack.encode") do
178+
AMQ::Protocol::Basic::Ack.encode(1, 12345, false)
179+
end
180+
181+
x.report("Basic.Nack.encode") do
182+
AMQ::Protocol::Basic::Nack.encode(1, 12345, false, true)
183+
end
184+
185+
x.report("Basic.Reject.encode") do
186+
AMQ::Protocol::Basic::Reject.encode(1, 12345, true)
187+
end
188+
189+
x.report("Queue.Declare.encode") do
190+
AMQ::Protocol::Queue::Declare.encode(1, "test.queue", false, true, false, false, false, {})
191+
end
192+
193+
x.report("Exchange.Declare.encode") do
194+
AMQ::Protocol::Exchange::Declare.encode(1, "test.exchange", "topic", false, true, false, false, false, {})
195+
end
196+
197+
x.compare!
198+
end

0 commit comments

Comments
 (0)