quicsilver 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +41 -0
  3. data/.gitignore +3 -1
  4. data/CHANGELOG.md +76 -5
  5. data/Gemfile.lock +18 -4
  6. data/LICENSE +21 -0
  7. data/README.md +33 -53
  8. data/Rakefile +29 -2
  9. data/benchmarks/components.rb +191 -0
  10. data/benchmarks/concurrent.rb +110 -0
  11. data/benchmarks/helpers.rb +88 -0
  12. data/benchmarks/quicsilver_server.rb +46 -0
  13. data/benchmarks/rails.rb +170 -0
  14. data/benchmarks/throughput.rb +113 -0
  15. data/examples/minimal_http3_server.rb +0 -6
  16. data/examples/rack_http3_server.rb +0 -6
  17. data/examples/simple_client_test.rb +26 -0
  18. data/ext/quicsilver/quicsilver.c +615 -138
  19. data/lib/quicsilver/client/client.rb +250 -0
  20. data/lib/quicsilver/client/request.rb +98 -0
  21. data/lib/quicsilver/protocol/frames.rb +327 -0
  22. data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
  23. data/lib/quicsilver/protocol/qpack/encoder.rb +189 -0
  24. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +125 -0
  25. data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
  26. data/lib/quicsilver/protocol/request_encoder.rb +47 -0
  27. data/lib/quicsilver/protocol/request_parser.rb +387 -0
  28. data/lib/quicsilver/protocol/response_encoder.rb +72 -0
  29. data/lib/quicsilver/protocol/response_parser.rb +249 -0
  30. data/lib/quicsilver/server/listener_data.rb +14 -0
  31. data/lib/quicsilver/server/request_handler.rb +86 -0
  32. data/lib/quicsilver/server/request_registry.rb +50 -0
  33. data/lib/quicsilver/server/server.rb +336 -0
  34. data/lib/quicsilver/transport/configuration.rb +132 -0
  35. data/lib/quicsilver/transport/connection.rb +350 -0
  36. data/lib/quicsilver/transport/event_loop.rb +38 -0
  37. data/lib/quicsilver/transport/inbound_stream.rb +33 -0
  38. data/lib/quicsilver/transport/stream.rb +28 -0
  39. data/lib/quicsilver/transport/stream_event.rb +26 -0
  40. data/lib/quicsilver/version.rb +1 -1
  41. data/lib/quicsilver.rb +49 -9
  42. data/lib/rackup/handler/quicsilver.rb +77 -0
  43. data/quicsilver.gemspec +10 -3
  44. metadata +122 -17
  45. data/examples/minimal_http3_client.rb +0 -89
  46. data/lib/quicsilver/client.rb +0 -191
  47. data/lib/quicsilver/http3/request_encoder.rb +0 -112
  48. data/lib/quicsilver/http3/request_parser.rb +0 -158
  49. data/lib/quicsilver/http3/response_encoder.rb +0 -73
  50. data/lib/quicsilver/http3.rb +0 -68
  51. data/lib/quicsilver/listener_data.rb +0 -29
  52. data/lib/quicsilver/server.rb +0 -258
  53. data/lib/quicsilver/server_configuration.rb +0 -49
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5e879e4531621883da97721256a2b0db478ba995d8bf54c93ddf959fca9cd36d
4
- data.tar.gz: 579023d40239d8c7a77671be44fb37265d735f864a13072887598406908c0aae
3
+ metadata.gz: 36065332b2efe43090097b945882282ba770a6b6b5a54d16d7229bf8977dfb6e
4
+ data.tar.gz: 22f94375fdaa43b465b6ff9b866985ca7dd06732ea5e0cbf98a94c7c92eb97df
5
5
  SHA512:
6
- metadata.gz: 25c070017404e15d673cfcb267ef90722dd255af0c68dd3488f1065ebe8fcc84614054b7ae3b5b0e2cd1bc74ca725a7ddd4e0500b80f849580ebef3306705443
7
- data.tar.gz: 61f273afb32f2a45d07bc2f634a8f6d52973654b4e874ed01dd87a674126046f2e332741ca464b6f41a025e206064172a9614c602bb7285dab0b68b6efa7d09a
6
+ metadata.gz: edcac1b797654202cf22ef471e3de5cd23a8409f0ded8d940652cb6194d14974f0c08f7118af1f76e1895dca0598ae646764687d0fabb0555317e67a3587f690
7
+ data.tar.gz: a1521e2a828ba6bd394939a5c7b1034d1951f92329444f1adee5135db96df9703128b731c673c10c6309d104f238aa95589a9c9cfb581c6e050bc5ef6a261c8f
@@ -0,0 +1,41 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ push:
12
+ branches: [ "main" ]
13
+ pull_request:
14
+ branches: [ "main" ]
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ jobs:
20
+ test:
21
+
22
+ runs-on: ubuntu-24.04
23
+ strategy:
24
+ matrix:
25
+ ruby-version: ["3.2"]
26
+
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ with:
30
+ submodules: recursive
31
+ - name: Set up Ruby
32
+ uses: ruby/setup-ruby@v1
33
+ with:
34
+ ruby-version: ${{ matrix.ruby-version }}
35
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
36
+ - name: Build MSQUIC
37
+ run: bundle exec rake build_msquic
38
+ - name: Compile
39
+ run: bundle exec rake compile
40
+ - name: Run tests
41
+ run: bundle exec rake
data/.gitignore CHANGED
@@ -8,5 +8,7 @@
8
8
  /tmp/
9
9
  /vendor/*
10
10
  /certs/*
11
+ /certificates/*
11
12
  *.bundle
12
- lib/quicsilver/quicsilver.bundle
13
+ lib/quicsilver/quicsilver.bundle
14
+ *.gem
data/CHANGELOG.md CHANGED
@@ -5,6 +5,82 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.0] - 2026-03-23
9
+
10
+ ### Added
11
+ - QPACK Huffman coding with 8-bit decode table and encode/decode caching
12
+ - 0-RTT replay protection for unsafe HTTP methods
13
+ - Bounded backpressure support
14
+ - Buffer size limits to prevent memory exhaustion (configurable `max_body_size`, `max_header_size`, `max_header_count`, `max_frame_payload_size`)
15
+ - Content-length validation
16
+ - Multi-value header support for duplicate header fields
17
+ - Headers validation: reject connection-specific headers, require `:authority` or `host` for http/https schemes
18
+ - Incremental unidirectional stream processing with critical stream protection
19
+ - QPACK encoder and decoder stream instruction validation
20
+ - Spec-correct error signaling with error codes on `FrameError` and `MessageError`
21
+ - Suppress response body for HEAD requests per RFC 9114 §4.1
22
+ - Allow `te: trailers` header in requests per RFC 9114 §4.2
23
+ - Custom ALPN support (no longer hardcoded to `h3`)
24
+ - `Stream` and `StreamEvent` abstractions to encapsulate C extension details
25
+ - Dual-stack (IPv4/IPv6) listener support — fixes TLS handshake failures on macOS
26
+ - Client `PUT` method
27
+ - Integration test suite for curl HTTP/3
28
+
29
+ ### Fixed
30
+ - Memory leaks: free `StreamContext` on `SHUTDOWN_COMPLETE`, free `ConnectionContext` on `CONNECTION_SHUTDOWN_COMPLETE`, close `EventQ`/`ExecContext`/`WakeFd` on shutdown
31
+ - Double-free and handle leaks in C extension
32
+ - `dispatch_to_ruby` safety with `rb_protect`; client use-after-free fix
33
+ - Infinite loop on truncated varint in request/response parsers
34
+ - Frame ordering: `DATA` before `HEADERS` now raises `FrameError`
35
+ - `STOP_SENDING` / `STREAM_RESET` compliance — server properly cancels streams and resets send side
36
+ - Control stream validation: reject duplicate settings, forbidden frame types, and reserved HTTP/2 types
37
+ - QPACK static table index 57/58 casing (`includeSubDomains`)
38
+ - Stale stream handle guard in cancel and C extension
39
+ - Replaced `Thread.kill` with `Thread.raise(DrainTimeoutError)` for clean drain
40
+ - Binary encoding for `buffer_data` and empty FIN handling
41
+ - Linux/GitHub CI: use epoll instead of kqueue on non-Darwin platforms
42
+ - Circular require warning
43
+
44
+ ### Changed
45
+ - Reorganized gem structure: `protocol/`, `server/`, `transport/` directories
46
+ - Server owns the 0-RTT policy
47
+ - QPACK encoder uses O(1) static table lookup with multi-level caching
48
+ - QPACK decoder uses string-based decoding with result caching
49
+ - HTTP/3 parsers optimized with parse-level caching and lazy allocation
50
+ - Varint encoding/decoding optimized with precomputed tables
51
+ - HTTP/3 encoders handle framing only; QPACK handles field encoding (cleaner separation)
52
+ - MsQuic custom execution mode with configurable worker pool and throughput settings
53
+
54
+ ### Limitations
55
+ - Client does not reuse connections
56
+
57
+ ## [0.2.0] - 2025-12-17
58
+
59
+ ### Added
60
+ - Graceful shutdown with GOAWAY frames (RFC 9114 compliant)
61
+ - Streaming response support for lazy/chunked bodies
62
+ - Flow control settings for backpressure management
63
+ - Client HTTP verb helpers: `get`, `post`, `patch`, `delete`, `head`
64
+ - Integration test suite
65
+ - Benchmarking examples for Rails
66
+
67
+ ### Fixed
68
+ - Memory leak: send buffers now freed on SEND_COMPLETE callback
69
+ - Segfault in event loop when client_obj was invalid
70
+ - Content-Type header handling for Rack compatibility
71
+ - String concatenation performance using StringIO
72
+
73
+ ### Changed
74
+ - `server.start` now blocks until shutdown (no separate `wait_for_connections` needed)
75
+ - Refactored to global event loop architecture
76
+ - Simplified server internals with Connection and QuicStream classes
77
+ - Replaced debug puts with proper logging
78
+
79
+ ### Limitations
80
+ - No server push or trailer support
81
+ - No dynamic QPACK table (static table only)
82
+ - Client does not reuse connections
83
+
8
84
  ## [0.1.0] - 2025-10-28
9
85
 
10
86
  ### Added
@@ -15,8 +91,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
91
  - QPACK header compression (static table support)
16
92
  - Bidirectional request/response streams
17
93
  - Request body buffering for large payloads
18
-
19
- ### Limitations
20
- This is still in prototype, it has the following known limitations:
21
- - No server push, GOAWAY, or trailer support
22
- - Limited error handling
data/Gemfile.lock CHANGED
@@ -1,13 +1,25 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quicsilver (0.1.0)
4
+ quicsilver (0.2.0)
5
+ localhost (~> 1.6)
6
+ logger
7
+ rack (~> 3.0)
8
+ rackup (~> 2.0)
5
9
 
6
10
  GEM
7
11
  remote: https://rubygems.org/
8
12
  specs:
9
- minitest (5.25.5)
10
- rake (10.5.0)
13
+ benchmark-ips (2.14.0)
14
+ localhost (1.6.0)
15
+ logger (1.7.0)
16
+ minitest (5.27.0)
17
+ minitest-focus (1.4.0)
18
+ minitest (>= 4, < 6)
19
+ rack (3.2.4)
20
+ rackup (2.2.1)
21
+ rack (>= 3)
22
+ rake (13.3.1)
11
23
  rake-compiler (1.3.0)
12
24
  rake
13
25
  rake-compiler-dock (1.9.1)
@@ -17,10 +29,12 @@ PLATFORMS
17
29
  ruby
18
30
 
19
31
  DEPENDENCIES
32
+ benchmark-ips (~> 2.12)
20
33
  bundler (~> 2.0)
21
34
  minitest (~> 5.0)
35
+ minitest-focus (~> 1.3)
22
36
  quicsilver!
23
- rake (~> 10.0)
37
+ rake (~> 13.0)
24
38
  rake-compiler (~> 1.2)
25
39
  rake-compiler-dock (~> 1.3)
26
40
 
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Haroon Ahmed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  HTTP/3 server for Ruby with Rack support.
4
4
 
5
- Disclaimer: currenly in early prototype.
5
+ **Status:** Experimental (v0.2.0)
6
6
 
7
7
  ## Installation
8
8
 
@@ -15,89 +15,69 @@ rake compile
15
15
 
16
16
  ## Quick Start
17
17
 
18
- ### 1. Set up certificates
19
-
20
- ```bash
21
- bash examples/setup_certs.sh
22
- ```
23
-
24
- ### 2. Run a Rack app over HTTP/3
18
+ ### Server
25
19
 
26
20
  ```ruby
27
21
  require "quicsilver"
28
22
 
29
- # Define your Rack app
30
23
  app = ->(env) {
31
- path = env['PATH_INFO']
32
-
33
- case path
24
+ case env['PATH_INFO']
34
25
  when '/'
35
- [200, {'Content-Type' => 'text/plain'}, ["Hello HTTP/3!"]]
26
+ [200, {'content-type' => 'text/plain'}, ["Hello HTTP/3!"]]
36
27
  when '/api/users'
37
- [200, {'Content-Type' => 'application/json'}, ['{"users": ["alice", "bob"]}']]
28
+ [200, {'content-type' => 'application/json'}, ['{"users": ["alice", "bob"]}']]
38
29
  else
39
- [404, {'Content-Type' => 'text/plain'}, ["Not Found"]]
30
+ [404, {'content-type' => 'text/plain'}, ["Not Found"]]
40
31
  end
41
32
  }
42
33
 
43
- # Start HTTP/3 server with Rack app
44
34
  server = Quicsilver::Server.new(4433, app: app)
45
- server.start
46
- server.wait_for_connections
35
+ server.start # Blocks until shutdown
47
36
  ```
48
37
 
49
- ### 3. Test with the client
50
-
51
- ```bash
52
- ruby examples/minimal_http3_client.rb
53
- ```
54
-
55
- ## Usage
56
-
57
- ### Rack HTTP/3 Server
38
+ ### Client
58
39
 
59
40
  ```ruby
60
41
  require "quicsilver"
61
42
 
62
- app = ->(env) {
63
- [200, {'Content-Type' => 'text/html'}, ["<h1>Hello from HTTP/3!</h1>"]]
64
- }
43
+ client = Quicsilver::Client.new("127.0.0.1", 4433, unsecure: true)
44
+ client.connect
65
45
 
66
- server = Quicsilver::Server.new(4433, app: app)
67
- server.start
68
- server.wait_for_connections
46
+ response = client.get("/api/users")
47
+ puts response[:body]
48
+
49
+ response = client.post("/api/users", body: '{"name": "charlie"}')
50
+
51
+ client.disconnect
69
52
  ```
70
53
 
71
- ### HTTP/3 Client
54
+ ## Usage with Rails
72
55
 
73
- ```ruby
74
- require "quicsilver"
56
+ ```bash
57
+ rackup -s quicsilver -p 4433
58
+ ```
75
59
 
76
- client = Quicsilver::Client.new("127.0.0.1", 4433, unsecure: true)
77
- client.connect
60
+ ## Configuration
78
61
 
79
- # Send HTTP/3 request
80
- request = Quicsilver::HTTP3::RequestEncoder.new(
81
- method: 'GET',
82
- path: '/api/users',
83
- authority: 'example.com'
62
+ ```ruby
63
+ config = Quicsilver::ServerConfiguration.new("/path/to/cert.pem", "/path/to/key.pem",
64
+ idle_timeout_ms: 10_000, # Connection idle timeout (ms)
65
+ max_concurrent_requests: 100 # Max concurrent requests per connection
84
66
  )
85
- client.send_data(request.encode)
86
67
 
87
- client.disconnect
68
+ server = Quicsilver::Server.new(4433,
69
+ app: app,
70
+ address: "0.0.0.0",
71
+ server_configuration: config
72
+ )
88
73
  ```
89
74
 
90
75
  ## Development
91
76
 
92
77
  ```bash
93
- # Run tests
94
- rake test
95
-
96
- # Build extension
97
- rake compile
98
-
99
- # Clean build artifacts
100
- rake clean
78
+ rake compile # Build C extension
79
+ rake test # Run tests
80
+ rake clean # Clean build artifacts
101
81
  ```
102
82
 
103
83
  ## License
data/Rakefile CHANGED
@@ -15,8 +15,12 @@ task :setup do
15
15
  end
16
16
 
17
17
  task :build_msquic => :setup do
18
- # Build MSQUIC using CMake with proper macOS framework linking
19
- sh 'cd vendor/msquic && cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXE_LINKER_FLAGS="-framework CoreServices" -DCMAKE_SHARED_LINKER_FLAGS="-framework CoreServices"'
18
+ cmake_args = ['-B build', '-DCMAKE_BUILD_TYPE=Release']
19
+ if RUBY_PLATFORM =~ /darwin/
20
+ cmake_args << '-DCMAKE_EXE_LINKER_FLAGS="-framework CoreServices"'
21
+ cmake_args << '-DCMAKE_SHARED_LINKER_FLAGS="-framework CoreServices"'
22
+ end
23
+ sh "cd vendor/msquic && cmake #{cmake_args.join(' ')}"
20
24
  sh 'cd vendor/msquic && cmake --build build --config Release'
21
25
  end
22
26
 
@@ -28,4 +32,27 @@ Rake::TestTask.new(:test) do |t|
28
32
  t.test_files = FileList["test/**/*_test.rb"]
29
33
  end
30
34
 
35
+ namespace :benchmark do
36
+ desc "Run throughput benchmark"
37
+ task :throughput do
38
+ ruby "benchmarks/throughput.rb"
39
+ end
40
+
41
+ desc "Run concurrency benchmark"
42
+ task :concurrent do
43
+ ruby "benchmarks/concurrent.rb"
44
+ end
45
+
46
+ desc "Run component micro-benchmarks"
47
+ task :components do
48
+ ruby "benchmarks/components.rb"
49
+ end
50
+
51
+ desc "Run all benchmarks"
52
+ task :all => [:components, :throughput, :concurrent]
53
+ end
54
+
55
+ desc "Run all benchmarks"
56
+ task :benchmark => "benchmark:all"
57
+
31
58
  task :default => :test
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env ruby
2
+ # Component micro-benchmarks using benchmark-ips.
3
+ # No server needed — pure in-process measurements.
4
+ #
5
+ # Usage: ruby benchmarks/components.rb
6
+
7
+ require "bundler/setup"
8
+ require "quicsilver"
9
+ require "benchmark/ips"
10
+
11
+ # --- Varint ---
12
+
13
+ puts "=" * 60
14
+ puts "Varint encode/decode"
15
+ puts "=" * 60
16
+
17
+ small = 6
18
+ medium = 1_000
19
+ large = 1_000_000
20
+
21
+ encoded_small = Quicsilver::Protocol.encode_varint(small)
22
+ encoded_medium = Quicsilver::Protocol.encode_varint(medium)
23
+ encoded_large = Quicsilver::Protocol.encode_varint(large)
24
+
25
+ Benchmark.ips do |x|
26
+ x.config(warmup: 1, time: 3)
27
+
28
+ x.report("encode small (#{small})") { Quicsilver::Protocol.encode_varint(small) }
29
+ x.report("encode medium (#{medium})") { Quicsilver::Protocol.encode_varint(medium) }
30
+ x.report("encode large (#{large})") { Quicsilver::Protocol.encode_varint(large) }
31
+ x.report("decode small") { Quicsilver::Protocol.decode_varint(encoded_small.bytes, 0) }
32
+ x.report("decode medium") { Quicsilver::Protocol.decode_varint(encoded_medium.bytes, 0) }
33
+ x.report("decode large") { Quicsilver::Protocol.decode_varint(encoded_large.bytes, 0) }
34
+
35
+ x.compare!
36
+ end
37
+
38
+ # --- Huffman ---
39
+
40
+ puts
41
+ puts "=" * 60
42
+ puts "Huffman encode/decode"
43
+ puts "=" * 60
44
+
45
+ huffman_inputs = [
46
+ "www.example.com",
47
+ "application/json",
48
+ "text/html; charset=utf-8",
49
+ "GET",
50
+ "/api/v1/users?page=1&limit=50"
51
+ ]
52
+
53
+ encoded_huffman = huffman_inputs.map { |s| Quicsilver::Protocol::Qpack::Huffman.encode(s) }
54
+
55
+ Benchmark.ips do |x|
56
+ x.config(warmup: 1, time: 3)
57
+
58
+ huffman_inputs.each do |input|
59
+ x.report("encode #{input[0..20]}") { Quicsilver::Protocol::Qpack::Huffman.encode(input) }
60
+ end
61
+ encoded_huffman.each_with_index do |enc, i|
62
+ x.report("decode #{huffman_inputs[i][0..20]}") { Quicsilver::Protocol::Qpack::Huffman.decode(enc) }
63
+ end
64
+
65
+ x.compare!
66
+ end
67
+
68
+ # --- QPACK Encoder ---
69
+
70
+ puts
71
+ puts "=" * 60
72
+ puts "QPACK Encoder"
73
+ puts "=" * 60
74
+
75
+ headers = [
76
+ [":method", "GET"],
77
+ [":path", "/api/v1/users"],
78
+ [":scheme", "https"],
79
+ [":authority", "example.com"],
80
+ ["accept", "application/json"],
81
+ ["user-agent", "quicsilver-bench/1.0"],
82
+ ["accept-encoding", "gzip, deflate"]
83
+ ]
84
+
85
+ encoder_huffman = Quicsilver::Protocol::Qpack::Encoder.new(huffman: true)
86
+ encoder_raw = Quicsilver::Protocol::Qpack::Encoder.new(huffman: false)
87
+
88
+ Benchmark.ips do |x|
89
+ x.config(warmup: 1, time: 3)
90
+
91
+ x.report("encode (huffman on)") { encoder_huffman.encode(headers) }
92
+ x.report("encode (huffman off)") { encoder_raw.encode(headers) }
93
+
94
+ x.compare!
95
+ end
96
+
97
+ # --- QPACK Decoder ---
98
+
99
+ puts
100
+ puts "=" * 60
101
+ puts "QPACK Decoder (string decoding)"
102
+ puts "=" * 60
103
+
104
+ # Build payloads for decode_qpack_string
105
+ huffman_payload = Quicsilver::Protocol::Qpack::Huffman.encode("application/json")
106
+ huffman_bytes = [0x80 | huffman_payload.bytesize] + huffman_payload.bytes # Huffman flag set
107
+
108
+ raw_string = "application/json"
109
+ raw_bytes = [raw_string.bytesize] + raw_string.bytes # No Huffman flag
110
+
111
+ # Include the decoder module in a throwaway object
112
+ decoder = Object.new
113
+ decoder.extend(Quicsilver::Protocol::Qpack::Decoder)
114
+
115
+ Benchmark.ips do |x|
116
+ x.config(warmup: 1, time: 3)
117
+
118
+ x.report("decode huffman string") { decoder.decode_qpack_string(huffman_bytes, 0) }
119
+ x.report("decode raw string") { decoder.decode_qpack_string(raw_bytes, 0) }
120
+
121
+ x.compare!
122
+ end
123
+
124
+ # --- Request Parser ---
125
+
126
+ puts
127
+ puts "=" * 60
128
+ puts "Request Parser"
129
+ puts "=" * 60
130
+
131
+ # Build a realistic GET request frame
132
+ request_encoder = Quicsilver::Protocol::Qpack::Encoder.new(huffman: true)
133
+ request_headers_payload = request_encoder.encode([
134
+ [":method", "GET"],
135
+ [":path", "/api/v1/users?page=1"],
136
+ [":scheme", "https"],
137
+ [":authority", "example.com"],
138
+ ["accept", "application/json"],
139
+ ["user-agent", "quicsilver-bench/1.0"]
140
+ ])
141
+
142
+ # HEADERS frame: type=0x01, varint length, payload
143
+ request_frame = Quicsilver::Protocol.encode_varint(0x01) +
144
+ Quicsilver::Protocol.encode_varint(request_headers_payload.bytesize) +
145
+ request_headers_payload
146
+
147
+ Benchmark.ips do |x|
148
+ x.config(warmup: 1, time: 3)
149
+
150
+ x.report("parse GET request") do
151
+ parser = Quicsilver::Protocol::RequestParser.new(request_frame)
152
+ parser.parse
153
+ end
154
+
155
+ x.compare!
156
+ end
157
+
158
+ # --- Response Parser ---
159
+
160
+ puts
161
+ puts "=" * 60
162
+ puts "Response Parser"
163
+ puts "=" * 60
164
+
165
+ # Build a realistic 200 response with body
166
+ response_encoder = Quicsilver::Protocol::Qpack::Encoder.new(huffman: true)
167
+ response_headers_payload = response_encoder.encode([
168
+ [":status", "200"],
169
+ ["content-type", "application/json"],
170
+ ["server", "quicsilver"]
171
+ ])
172
+
173
+ body = '{"users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]}'
174
+
175
+ response_frame = Quicsilver::Protocol.encode_varint(0x01) +
176
+ Quicsilver::Protocol.encode_varint(response_headers_payload.bytesize) +
177
+ response_headers_payload +
178
+ Quicsilver::Protocol.encode_varint(0x00) +
179
+ Quicsilver::Protocol.encode_varint(body.bytesize) +
180
+ body
181
+
182
+ Benchmark.ips do |x|
183
+ x.config(warmup: 1, time: 3)
184
+
185
+ x.report("parse 200 response + body") do
186
+ parser = Quicsilver::Protocol::ResponseParser.new(response_frame)
187
+ parser.parse
188
+ end
189
+
190
+ x.compare!
191
+ end