Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

10x performance improvements #178

Merged
merged 38 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
86cf728
benchmark aimed at measuring http-2 overhead
HoneyryderChuck Mar 12, 2025
1a4f879
improving the encoding context to use more efficient enumerable funct…
HoneyryderChuck Mar 7, 2025
7444155
using ruby 3.4 String#append_as_bytes wherever possible
HoneyryderChuck Mar 7, 2025
0defb72
connection: cache frame properties used multiple times in local varia…
HoneyryderChuck Mar 7, 2025
d538ae7
remove redundant parentheses
HoneyryderChuck Mar 7, 2025
669b76f
using local var in framer
HoneyryderChuck Mar 8, 2025
e2e1f04
moved empty collection to main namespace
HoneyryderChuck Mar 10, 2025
ae55786
refactor frame generation routines to reduce number of string/array a…
HoneyryderChuck Mar 10, 2025
b44b092
remove needless array allocations during connection handshake
HoneyryderChuck Mar 10, 2025
b76f85e
remove needless check from #connection_settings (they're already done…
HoneyryderChuck Mar 10, 2025
00c8dd3
initialize @h2c_upgrade (for object shape opt)
HoneyryderChuck Mar 10, 2025
c36ba67
rewrite of variable usage, avoid multiple Hash#[] calls on flow control
HoneyryderChuck Mar 10, 2025
4a7e2fe
rewrite logic of #encode and #encode_headers, in order to emit frames…
HoneyryderChuck Mar 10, 2025
9e592cb
fixed inconsistency of @last_activated_stream and @last_stream_id
HoneyryderChuck Mar 10, 2025
4897d4f
improve handling of partial data frame generation
HoneyryderChuck Mar 10, 2025
8f62ccc
forego recycling of @streams_recently_closed if first stream of the c…
HoneyryderChuck Mar 10, 2025
a571910
another instance of replacing a multi-value compare with Array#includ…
HoneyryderChuck Mar 10, 2025
1bddbb1
header compression: eliminate two intermediate arrays (at least) by y…
HoneyryderChuck Mar 10, 2025
6da27ad
inlined #add_to_table logic
HoneyryderChuck Mar 10, 2025
19025a2
memoizing current table size in encoding context
HoneyryderChuck Mar 10, 2025
00b2c69
using the offset parameter in the #header function
HoneyryderChuck Mar 10, 2025
a1804c5
turned huffman class into a module
HoneyryderChuck Mar 11, 2025
afa2514
Huffman.encode: reduce the number of intermediate strings
HoneyryderChuck Mar 11, 2025
db73b15
supporting buffering to existing string when compressing strings
HoneyryderChuck Mar 11, 2025
e148589
when buffer is empty, push-then-pop can be avoided if the remote wind…
HoneyryderChuck Mar 12, 2025
590ed7c
Decompressor#header: respond with correct size hash instead of init-t…
HoneyryderChuck Mar 12, 2025
8a93838
EncodingContext#process: avoid intermediate array generation
HoneyryderChuck Mar 12, 2025
0b9c671
improving sig for header_command to use exact record types for each t…
HoneyryderChuck Mar 12, 2025
41e1056
avoid array accessors by spreading the two elements of the huffman st…
HoneyryderChuck Mar 12, 2025
8a27021
avoid slow string 1 byte drip, instead iterate over bytes, and slice …
HoneyryderChuck Mar 12, 2025
dce6852
use Hash#fetch_values to reduce hash lookup usage
HoneyryderChuck Mar 12, 2025
9f36edf
Use #filter_map instead of custom #each_with_object injecting to an a…
HoneyryderChuck Mar 12, 2025
0f57063
initializing ivars from mixins (previously memoized) in the class ini…
HoneyryderChuck Mar 12, 2025
071d7f2
use Comparable#betwen? for integer range
HoneyryderChuck Mar 14, 2025
6a8e330
test with ruby 3.4
HoneyryderChuck Mar 15, 2025
e77067e
prefer Integer#<< over Integer#**
HoneyryderChuck Mar 25, 2025
49cca5a
fixup! rewrite of variable usage, avoid multiple Hash#[] calls on flo…
HoneyryderChuck Mar 25, 2025
0e0c892
EncodincContext#dereference: bailout on index value without hash lookup
HoneyryderChuck Mar 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby: [2.7 ,'3.0', 3.1, 3.2, 3.3, jruby, truffleruby]
ruby: [2.7 ,'3.0', 3.1, 3.2, 3.3, 3.4, jruby, truffleruby]

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
inherit_from: .rubocop_todo.yml

require:
plugins:
- rubocop-performance

AllCops:
Expand Down
4 changes: 2 additions & 2 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ Metrics/BlockNesting:
# Offense count: 5
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 371
Enabled: false

Metrics/ModuleLength:
Max: 120
Enabled: false

# Offense count: 12
Metrics/CyclomaticComplexity:
Expand Down
7 changes: 7 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,10 @@ group :types do
end
end
end

group :benchmark do
platform :mri do
gem "memory_profiler"
gem "singed"
end
end
190 changes: 190 additions & 0 deletions benchmarks/client_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# frozen_string_literal: true

require "uri"
require "http/2"

DEBUG = ENV.key?("DEBUG")
BENCHMARK = ENV.fetch("BENCH", "profile")
ITERATIONS = 5000

METHOD = "GET"
BODY = "bang"
URL = URI.parse(ARGV[0] || "http://localhost:8080/")
CLIENT = HTTP2::Client.new
SERVER = HTTP2::Server.new

CLIENT_BUFFER = "".b
SERVER_BUFFER = "".b

def log
return unless DEBUG

puts yield
end

log { "build client..." }
CLIENT.on(:frame) do |bytes|
log { "(client) sending bytes: #{bytes.size}" }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use standard arrow semantics (-> or <-) for client/server responses if you want.

CLIENT_BUFFER << bytes
end
CLIENT.on(:frame_sent) do |frame|
log { "(client) Sent frame: #{frame.inspect}" }
end
CLIENT.on(:frame_received) do |frame|
log { "(client) Received frame: #{frame.inspect}" }
end

CLIENT.on(:altsvc) do |f|
log { "(client) received ALTSVC #{f}" }
end

log { "build server..." }
SERVER.on(:frame) do |bytes|
log { "(server) sending bytes: #{bytes.bytesize}" }
SERVER_BUFFER << bytes
end
SERVER.on(:frame_sent) do |frame|
log { "(server) Sent frame: #{frame.inspect}" }
end
SERVER.on(:frame_received) do |frame|
log { "(server) Received frame: #{frame.inspect}" }
end

SERVER.on(:goaway) do
log { "(server) goaway received" }
end

SERVER.on(:stream) do |stream|
req = {}
buffer = "".b

stream.on(:active) { log { "(server stream:#{stream.id}) client opened new stream" } }
stream.on(:close) { log { "(server stream:#{stream.id}) stream closed" } }

stream.on(:headers) do |h|
log { "(server stream:#{stream.id}) request headers: #{Hash[*h.flatten]}" }
end

stream.on(:data) do |d|
log { "(server stream:#{stream.id}) payload chunk: <<#{d}>>" }
buffer << d
end

stream.on(:half_close) do
log { "(server stream:#{stream.id}) client closed its end of the stream" }

response = nil
if req[":method"] == "POST"
log { "(server stream:#{stream.id}) Received POST request, payload: #{buffer}" }
response = "(server stream:#{stream.id}) Hello HTTP 2.0! POST payload: #{buffer}"
else
log { "Received GET request" }
response = "(server stream:#{stream.id}) Hello HTTP 2.0! GET request"
end

stream.headers(
{
":status" => "200",
"content-length" => response.bytesize.to_s,
"content-type" => "text/plain",
"x-stream-id" => "stream-#{stream.id}"
}, end_stream: false
)

# split response into multiple DATA frames
stream.data(response[0, 5], end_stream: false)
stream.data(response[5, -1] || "")
end
end

def send_request
stream = CLIENT.new_stream

stream.on(:close) do
log { "(client stream:#{stream.id}) stream closed" }
end

stream.on(:half_close) do
log { "(client stream:#{stream.id}) closing client-end of the stream" }
end

stream.on(:headers) do |h|
log { "(client stream:#{stream.id}) response headers: #{h}" }
end

stream.on(:data) do |d|
log { "(client stream:#{stream.id}) response data chunk: <<#{d}>>" }
end

stream.on(:altsvc) do |f|
log { "(client stream:#{stream.id}) received ALTSVC #{f}" }
end

head = {
":scheme" => URL.scheme,
":method" => METHOD,
":authority" => [URL.host, URL.port].join(":"),
":path" => URL.path,
"accept" => "*/*"
}

log { "Sending HTTP 2.0 request" }

if head[":method"] == "GET"
stream.headers(head, end_stream: true)
else
stream.headers(head, end_stream: false)
stream.data(BODY)
end

until CLIENT_BUFFER.empty? && SERVER_BUFFER.empty?
unless CLIENT_BUFFER.empty?
SERVER << CLIENT_BUFFER
CLIENT_BUFFER.clear
end

unless SERVER_BUFFER.empty?
CLIENT << SERVER_BUFFER
SERVER_BUFFER.clear
end
end
end

def benchmark(bench_type, &block)
return yield if DEBUG

case bench_type
when "profile"
require "singed"
Singed.output_directory = "tmp/"

flamegraph(&block)
when "memory"
require "memory_profiler"
MemoryProfiler.report(allow_files: ["lib/http/2"], &block).pretty_print

when "benchmark"
require "benchmark"
puts Benchmark.measure(&block)
end
end

GC.start
GC.disable

puts "warmup..."
ITERATIONS.times do
# start client stream
send_request
end

puts "bench!"
# Benchmark.bmbm do |x|
benchmark(BENCHMARK) do
ITERATIONS.times do
# start client stream
send_request
end

CLIENT.goaway
end
5 changes: 5 additions & 0 deletions lib/http/2.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# frozen_string_literal: true

require "http/2/version"

module HTTP2
EMPTY = [].freeze
end

require "http/2/extensions"
require "http/2/base64"
require "http/2/error"
Expand Down
1 change: 1 addition & 0 deletions lib/http/2/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def initialize(settings = {})

@local_role = :client
@remote_role = :server
@h2c_upgrade = nil

super
end
Expand Down
Loading