quicsilver 0.3.0 → 0.5.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +9 -1
  3. data/.github/workflows/cibuildgem.yaml +117 -0
  4. data/.gitignore +8 -1
  5. data/CHANGELOG.md +82 -0
  6. data/Gemfile.lock +27 -2
  7. data/README.md +205 -29
  8. data/Rakefile +75 -3
  9. data/benchmarks/baseline.rb +175 -0
  10. data/benchmarks/compare.rb +398 -0
  11. data/benchmarks/helpers.rb +138 -64
  12. data/benchmarks/latency.rb +128 -0
  13. data/benchmarks/loss.rb +193 -0
  14. data/benchmarks/mobile.rb +125 -0
  15. data/docs/connection_identity.md +141 -0
  16. data/examples/README.md +44 -91
  17. data/examples/benchmark.rb +191 -0
  18. data/examples/connection_pool_demo.rb +47 -0
  19. data/examples/example_helper.rb +69 -0
  20. data/examples/falcon_middleware.rb +44 -0
  21. data/examples/feature_demo.rb +125 -0
  22. data/examples/grpc_style.rb +97 -0
  23. data/examples/latency_aware_routing.rb +382 -0
  24. data/examples/lifecycle_drain.rb +264 -0
  25. data/examples/minimal_http3_server.rb +6 -18
  26. data/examples/priorities.rb +60 -0
  27. data/examples/protocol_http_server.rb +31 -0
  28. data/examples/rack_http3_server.rb +8 -20
  29. data/examples/rails_feature_test.rb +280 -0
  30. data/examples/server_stats.rb +117 -0
  31. data/examples/simple_client_test.rb +5 -5
  32. data/examples/streaming_sse.rb +33 -0
  33. data/examples/trailers.rb +69 -0
  34. data/examples/webtransport_smoke.rb +546 -0
  35. data/examples/worker_load_balancing.rb +425 -0
  36. data/ext/quicsilver/extconf.rb +111 -12
  37. data/ext/quicsilver/include/msquic.h +2226 -0
  38. data/ext/quicsilver/include/msquic_posix.h +696 -0
  39. data/ext/quicsilver/include/quic_sal_stub.h +314 -0
  40. data/ext/quicsilver/msquic_patch.rb +68 -0
  41. data/ext/quicsilver/patches/reliable-reset.patch +723 -0
  42. data/ext/quicsilver/patches/stream-final-size.patch +47 -0
  43. data/ext/quicsilver/quicsilver.c +1646 -189
  44. data/lib/quicsilver/client/client.rb +546 -88
  45. data/lib/quicsilver/client/connection_pool.rb +202 -0
  46. data/lib/quicsilver/client/request.rb +106 -14
  47. data/lib/quicsilver/errors.rb +39 -0
  48. data/lib/quicsilver/libmsquic.2.dylib +0 -0
  49. data/lib/quicsilver/protocol/adapter.rb +196 -0
  50. data/lib/quicsilver/protocol/capsule.rb +38 -0
  51. data/lib/quicsilver/protocol/connect_stream_decoder.rb +75 -0
  52. data/lib/quicsilver/protocol/control_stream_parser.rb +117 -0
  53. data/lib/quicsilver/protocol/datagram.rb +40 -0
  54. data/lib/quicsilver/protocol/frame_parser.rb +141 -0
  55. data/lib/quicsilver/protocol/frame_reader.rb +84 -0
  56. data/lib/quicsilver/protocol/frames.rb +137 -10
  57. data/lib/quicsilver/protocol/priority.rb +65 -0
  58. data/lib/quicsilver/protocol/qpack/decoder.rb +4 -42
  59. data/lib/quicsilver/protocol/qpack/encoder.rb +49 -21
  60. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +16 -1
  61. data/lib/quicsilver/protocol/qpack/huffman.rb +4 -4
  62. data/lib/quicsilver/protocol/request_encoder.rb +6 -3
  63. data/lib/quicsilver/protocol/request_parser.rb +43 -141
  64. data/lib/quicsilver/protocol/response_encoder.rb +40 -5
  65. data/lib/quicsilver/protocol/response_parser.rb +26 -128
  66. data/lib/quicsilver/protocol/stream_input.rb +98 -0
  67. data/lib/quicsilver/protocol/stream_output.rb +59 -0
  68. data/lib/quicsilver/protocol/unidirectional_stream.rb +12 -0
  69. data/lib/quicsilver/protocol/web_transport.rb +214 -0
  70. data/lib/quicsilver/rack/context.rb +33 -0
  71. data/lib/quicsilver/response.rb +40 -0
  72. data/lib/quicsilver/server/rack_adapter.rb +77 -0
  73. data/lib/quicsilver/server/receive_queue.rb +106 -0
  74. data/lib/quicsilver/server/request_handler.rb +111 -48
  75. data/lib/quicsilver/server/request_registry.rb +18 -5
  76. data/lib/quicsilver/server/scheduler.rb +50 -0
  77. data/lib/quicsilver/server/schedulers/fiber_scheduler.rb +79 -0
  78. data/lib/quicsilver/server/schedulers/thread_scheduler.rb +76 -0
  79. data/lib/quicsilver/server/server.rb +848 -104
  80. data/lib/quicsilver/server/web_transport_flow_control.rb +270 -0
  81. data/lib/quicsilver/server/web_transport_manager.rb +281 -0
  82. data/lib/quicsilver/server/web_transport_registry.rb +40 -0
  83. data/lib/quicsilver/server/web_transport_session.rb +812 -0
  84. data/lib/quicsilver/server/web_transport_stream.rb +326 -0
  85. data/lib/quicsilver/transport/configuration.rb +113 -11
  86. data/lib/quicsilver/transport/connection.rb +376 -132
  87. data/lib/quicsilver/transport/connection_stats.rb +53 -0
  88. data/lib/quicsilver/transport/event_loop.rb +36 -0
  89. data/lib/quicsilver/transport/inbound_stream.rb +21 -1
  90. data/lib/quicsilver/transport/stream.rb +18 -3
  91. data/lib/quicsilver/transport/stream_event.rb +25 -2
  92. data/lib/quicsilver/transport/stream_id.rb +26 -0
  93. data/lib/quicsilver/version.rb +1 -1
  94. data/lib/quicsilver.rb +45 -11
  95. data/lib/rackup/handler/quicsilver.rb +57 -38
  96. data/quicsilver.gemspec +17 -2
  97. metadata +120 -10
  98. data/benchmarks/concurrent.rb +0 -110
  99. data/benchmarks/quicsilver_server.rb +0 -46
  100. data/benchmarks/rails.rb +0 -170
  101. data/benchmarks/throughput.rb +0 -113
  102. data/examples/setup_certs.sh +0 -57
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quicsilver
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Haroon Ahmed
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-03-23 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bundler
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '1.3'
68
+ - !ruby/object:Gem::Dependency
69
+ name: cibuildgem
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: minitest
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +121,48 @@ dependencies:
107
121
  - - "~>"
108
122
  - !ruby/object:Gem::Version
109
123
  version: '2.12'
124
+ - !ruby/object:Gem::Dependency
125
+ name: protocol-http
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.49'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.49'
138
+ - !ruby/object:Gem::Dependency
139
+ name: protocol-rack
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '0.22'
145
+ type: :runtime
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '0.22'
152
+ - !ruby/object:Gem::Dependency
153
+ name: console
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ type: :runtime
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
110
166
  - !ruby/object:Gem::Dependency
111
167
  name: logger
112
168
  requirement: !ruby/object:Gem::Requirement
@@ -172,6 +228,7 @@ extensions:
172
228
  extra_rdoc_files: []
173
229
  files:
174
230
  - ".github/workflows/ci.yml"
231
+ - ".github/workflows/cibuildgem.yaml"
175
232
  - ".gitignore"
176
233
  - ".gitmodules"
177
234
  - ".ruby-version"
@@ -181,25 +238,59 @@ files:
181
238
  - LICENSE
182
239
  - README.md
183
240
  - Rakefile
241
+ - benchmarks/baseline.rb
242
+ - benchmarks/compare.rb
184
243
  - benchmarks/components.rb
185
- - benchmarks/concurrent.rb
186
244
  - benchmarks/helpers.rb
187
- - benchmarks/quicsilver_server.rb
188
- - benchmarks/rails.rb
189
- - benchmarks/throughput.rb
245
+ - benchmarks/latency.rb
246
+ - benchmarks/loss.rb
247
+ - benchmarks/mobile.rb
190
248
  - bin/console
191
249
  - bin/setup
250
+ - docs/connection_identity.md
192
251
  - examples/README.md
252
+ - examples/benchmark.rb
253
+ - examples/connection_pool_demo.rb
254
+ - examples/example_helper.rb
255
+ - examples/falcon_middleware.rb
256
+ - examples/feature_demo.rb
257
+ - examples/grpc_style.rb
258
+ - examples/latency_aware_routing.rb
259
+ - examples/lifecycle_drain.rb
193
260
  - examples/minimal_http3_server.rb
261
+ - examples/priorities.rb
262
+ - examples/protocol_http_server.rb
194
263
  - examples/rack_http3_server.rb
195
- - examples/setup_certs.sh
264
+ - examples/rails_feature_test.rb
265
+ - examples/server_stats.rb
196
266
  - examples/simple_client_test.rb
267
+ - examples/streaming_sse.rb
268
+ - examples/trailers.rb
269
+ - examples/webtransport_smoke.rb
270
+ - examples/worker_load_balancing.rb
197
271
  - ext/quicsilver/extconf.rb
272
+ - ext/quicsilver/include/msquic.h
273
+ - ext/quicsilver/include/msquic_posix.h
274
+ - ext/quicsilver/include/quic_sal_stub.h
275
+ - ext/quicsilver/msquic_patch.rb
276
+ - ext/quicsilver/patches/reliable-reset.patch
277
+ - ext/quicsilver/patches/stream-final-size.patch
198
278
  - ext/quicsilver/quicsilver.c
199
279
  - lib/quicsilver.rb
200
280
  - lib/quicsilver/client/client.rb
281
+ - lib/quicsilver/client/connection_pool.rb
201
282
  - lib/quicsilver/client/request.rb
283
+ - lib/quicsilver/errors.rb
284
+ - lib/quicsilver/libmsquic.2.dylib
285
+ - lib/quicsilver/protocol/adapter.rb
286
+ - lib/quicsilver/protocol/capsule.rb
287
+ - lib/quicsilver/protocol/connect_stream_decoder.rb
288
+ - lib/quicsilver/protocol/control_stream_parser.rb
289
+ - lib/quicsilver/protocol/datagram.rb
290
+ - lib/quicsilver/protocol/frame_parser.rb
291
+ - lib/quicsilver/protocol/frame_reader.rb
202
292
  - lib/quicsilver/protocol/frames.rb
293
+ - lib/quicsilver/protocol/priority.rb
203
294
  - lib/quicsilver/protocol/qpack/decoder.rb
204
295
  - lib/quicsilver/protocol/qpack/encoder.rb
205
296
  - lib/quicsilver/protocol/qpack/header_block_decoder.rb
@@ -208,21 +299,40 @@ files:
208
299
  - lib/quicsilver/protocol/request_parser.rb
209
300
  - lib/quicsilver/protocol/response_encoder.rb
210
301
  - lib/quicsilver/protocol/response_parser.rb
302
+ - lib/quicsilver/protocol/stream_input.rb
303
+ - lib/quicsilver/protocol/stream_output.rb
304
+ - lib/quicsilver/protocol/unidirectional_stream.rb
305
+ - lib/quicsilver/protocol/web_transport.rb
306
+ - lib/quicsilver/rack/context.rb
307
+ - lib/quicsilver/response.rb
211
308
  - lib/quicsilver/server/listener_data.rb
309
+ - lib/quicsilver/server/rack_adapter.rb
310
+ - lib/quicsilver/server/receive_queue.rb
212
311
  - lib/quicsilver/server/request_handler.rb
213
312
  - lib/quicsilver/server/request_registry.rb
313
+ - lib/quicsilver/server/scheduler.rb
314
+ - lib/quicsilver/server/schedulers/fiber_scheduler.rb
315
+ - lib/quicsilver/server/schedulers/thread_scheduler.rb
214
316
  - lib/quicsilver/server/server.rb
317
+ - lib/quicsilver/server/web_transport_flow_control.rb
318
+ - lib/quicsilver/server/web_transport_manager.rb
319
+ - lib/quicsilver/server/web_transport_registry.rb
320
+ - lib/quicsilver/server/web_transport_session.rb
321
+ - lib/quicsilver/server/web_transport_stream.rb
215
322
  - lib/quicsilver/transport/configuration.rb
216
323
  - lib/quicsilver/transport/connection.rb
324
+ - lib/quicsilver/transport/connection_stats.rb
217
325
  - lib/quicsilver/transport/event_loop.rb
218
326
  - lib/quicsilver/transport/inbound_stream.rb
219
327
  - lib/quicsilver/transport/stream.rb
220
328
  - lib/quicsilver/transport/stream_event.rb
329
+ - lib/quicsilver/transport/stream_id.rb
221
330
  - lib/quicsilver/version.rb
222
331
  - lib/rackup/handler/quicsilver.rb
223
332
  - quicsilver.gemspec
224
333
  homepage: https://github.com/hahmed/quicsilver
225
- licenses: []
334
+ licenses:
335
+ - MIT
226
336
  metadata:
227
337
  allowed_push_host: https://rubygems.org
228
338
  homepage_uri: https://github.com/hahmed/quicsilver
@@ -235,14 +345,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
235
345
  requirements:
236
346
  - - ">="
237
347
  - !ruby/object:Gem::Version
238
- version: 3.2.0
348
+ version: 3.4.0
239
349
  required_rubygems_version: !ruby/object:Gem::Requirement
240
350
  requirements:
241
351
  - - ">="
242
352
  - !ruby/object:Gem::Version
243
353
  version: '0'
244
354
  requirements: []
245
- rubygems_version: 3.6.2
355
+ rubygems_version: 3.6.9
246
356
  specification_version: 4
247
357
  summary: HTTP/3 server implementation for Ruby
248
358
  test_files: []
@@ -1,110 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # Concurrency benchmark: tests HTTP/3 multiplexing and multi-connection overhead.
3
- #
4
- # Self-contained:
5
- # ruby benchmarks/concurrent.rb
6
- #
7
- # External server:
8
- # HOST=127.0.0.1 PORT=4433 ruby benchmarks/concurrent.rb
9
-
10
- require_relative "helpers"
11
- require "benchmark"
12
-
13
- MULTIPLEX_REQUESTS = ENV.fetch("MULTIPLEX_REQUESTS", "50").to_i
14
- NUM_CLIENTS = ENV.fetch("NUM_CLIENTS", "20").to_i
15
- REQUESTS_PER_CLIENT = ENV.fetch("REQUESTS_PER_CLIENT", "5").to_i
16
- HOST = ENV["HOST"]
17
- PORT = ENV["PORT"]&.to_i
18
-
19
- def test_multiplexing(host, port)
20
- Benchmarks::Helpers.print_header(
21
- "Test 1: Single-connection multiplexing",
22
- requests: MULTIPLEX_REQUESTS
23
- )
24
-
25
- request_times = []
26
- mutex = Mutex.new
27
-
28
- client = Quicsilver::Client.new(host, port, unsecure: true)
29
- client.connect
30
-
31
- elapsed = Benchmark.realtime do
32
- threads = MULTIPLEX_REQUESTS.times.map do |i|
33
- Thread.new do
34
- start = Time.now
35
- client.get("/multiplex/#{i}")
36
- req_time = Time.now - start
37
- mutex.synchronize { request_times << req_time }
38
- end
39
- end
40
- threads.each(&:join)
41
- end
42
-
43
- client.disconnect
44
-
45
- concurrency_factor = request_times.any? ? (request_times.sum / elapsed).round(2) : 0
46
- puts " Wall clock: #{(elapsed * 1000).round(2)}ms"
47
- puts " Avg request time: #{(request_times.sum / request_times.size * 1000).round(2)}ms"
48
- puts " Throughput: #{(MULTIPLEX_REQUESTS / elapsed).round(2)} req/s"
49
- puts " Concurrency factor: #{concurrency_factor}x"
50
- Benchmarks::Helpers.print_stats("Latency", request_times)
51
- end
52
-
53
- def test_concurrent_clients(host, port)
54
- Benchmarks::Helpers.print_header(
55
- "Test 2: Concurrent clients",
56
- clients: NUM_CLIENTS,
57
- requests_per_client: REQUESTS_PER_CLIENT
58
- )
59
-
60
- request_times = []
61
- mutex = Mutex.new
62
- successful = 0
63
-
64
- elapsed = Benchmark.realtime do
65
- threads = NUM_CLIENTS.times.map do |i|
66
- Thread.new do
67
- client = Quicsilver::Client.new(host, port, unsecure: true)
68
- client.connect
69
-
70
- REQUESTS_PER_CLIENT.times do |req|
71
- start = Time.now
72
- client.get("/client#{i}/request#{req}")
73
- req_time = Time.now - start
74
- mutex.synchronize { request_times << req_time }
75
- end
76
-
77
- client.disconnect
78
- mutex.synchronize { successful += 1 }
79
- rescue => e
80
- $stderr.puts " Client #{i} error: #{e.message}"
81
- end
82
- end
83
- threads.each(&:join)
84
- end
85
-
86
- total_requests = request_times.size
87
- concurrency_factor = request_times.any? ? (request_times.sum / elapsed).round(2) : 0
88
-
89
- puts " Successful clients: #{successful}/#{NUM_CLIENTS}"
90
- puts " Total requests: #{total_requests}"
91
- puts " Wall clock: #{(elapsed * 1000).round(2)}ms"
92
- puts " Avg request time: #{request_times.any? ? (request_times.sum / request_times.size * 1000).round(2) : "N/A"}ms"
93
- puts " Throughput: #{(total_requests / elapsed).round(2)} req/s"
94
- puts " Concurrency factor: #{concurrency_factor}x"
95
- Benchmarks::Helpers.print_stats("Latency", request_times)
96
- end
97
-
98
- def run_all(host, port)
99
- test_multiplexing(host, port)
100
- test_concurrent_clients(host, port)
101
- end
102
-
103
- if HOST && PORT
104
- run_all(HOST, PORT)
105
- else
106
- puts "Booting inline server..."
107
- Benchmarks::Helpers.with_server(Benchmarks::Helpers.benchmark_app) do |port|
108
- run_all("127.0.0.1", port)
109
- end
110
- end
@@ -1,46 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "quicsilver"
5
- require 'localhost/authority'
6
- require 'json'
7
-
8
- SIMPLE_APP = lambda do |env|
9
- case env['PATH_INFO']
10
- when '/'
11
- [200, {'Content-Type' => 'text/plain'}, ['OK']]
12
- when '/json'
13
- body = JSON.generate({status: 'ok', timestamp: Time.now.to_i})
14
- [200, {'Content-Type' => 'application/json'}, [body]]
15
- when '/echo'
16
- [200, {'Content-Type' => 'text/plain'}, [env['REQUEST_METHOD']]]
17
- else
18
- [404, {'Content-Type' => 'text/plain'}, ['Not Found']]
19
- end
20
- end
21
-
22
- port = ENV["PORT"] || 4433
23
- default_host = ENV["HOST"] || "0.0.0.0"
24
-
25
- authority = Localhost::Authority.fetch
26
- cert_file = authority.certificate_path
27
- key_file = authority.key_path
28
-
29
- config = ::Quicsilver::Transport::Configuration.new(cert_file, key_file)
30
-
31
- server = ::Quicsilver::Server.new(
32
- port.to_i,
33
- address: default_host,
34
- app: SIMPLE_APP,
35
- server_configuration: config
36
- )
37
-
38
- puts "Starting Quicsilver on port #{port}..."
39
-
40
- trap("INT") do
41
- puts "\nStopping server..."
42
- server.stop
43
- exit
44
- end
45
-
46
- server.start # Blocks until shutdown
data/benchmarks/rails.rb DELETED
@@ -1,170 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # Rails benchmark: concurrent POST, GET, DELETE against a Rails app.
3
- # Multiplexes requests within each connection (HTTP/3 streams),
4
- # capped by CONCURRENCY to stay within the server's stream limit.
5
- #
6
- # Start blogz first:
7
- # cd ../blogz && bundle exec rackup -s quicsilver -p 4433
8
- #
9
- # Usage:
10
- # CONNECTIONS=5 ITERATIONS=100 ruby benchmarks/rails.rb
11
- # CONCURRENCY=8 CONNECTIONS=3 ITERATIONS=200 ruby benchmarks/rails.rb
12
-
13
- require "bundler/setup"
14
- require "quicsilver"
15
- require "json"
16
- require "benchmark"
17
-
18
- require_relative "helpers"
19
-
20
- HOST = ENV.fetch("HOST", "127.0.0.1")
21
- PORT = ENV.fetch("PORT", "4433").to_i
22
- CONNECTIONS = ENV.fetch("CONNECTIONS", "5").to_i
23
- ITERATIONS = ENV.fetch("ITERATIONS", "100").to_i
24
- CONCURRENCY = ENV.fetch("CONCURRENCY", "8").to_i # max in-flight per connection
25
-
26
- total_requests = CONNECTIONS * ITERATIONS
27
-
28
- Benchmarks::Helpers.print_header(
29
- "Rails Concurrent Benchmark (multiplexed)",
30
- target: "#{HOST}:#{PORT}",
31
- connections: CONNECTIONS,
32
- "reqs/conn": ITERATIONS,
33
- concurrency: "#{CONCURRENCY} streams/conn",
34
- total: "#{total_requests * 3} (POST + GET + DELETE)"
35
- )
36
-
37
- mutex = Mutex.new
38
- results = { post: [], get: [], delete: [] }
39
- all_created_ids = []
40
-
41
- # Fire N requests with at most `concurrency` in-flight at a time on a shared connection.
42
- def multiplex(count, concurrency:)
43
- queue = Queue.new
44
- count.times { |i| queue << i }
45
-
46
- threads = concurrency.times.map do
47
- Thread.new do
48
- while (i = queue.pop(true) rescue nil)
49
- yield i
50
- end
51
- end
52
- end
53
- threads.each(&:join)
54
- end
55
-
56
- # Phase 1: Concurrent multiplexed POSTs
57
- puts "\nPhase 1: POST /posts.json (#{CONNECTIONS} conns x #{ITERATIONS}, #{CONCURRENCY} in-flight)..."
58
- post_elapsed = Benchmark.realtime do
59
- conn_threads = CONNECTIONS.times.map do |conn_id|
60
- Thread.new do
61
- client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
62
- client.connect
63
-
64
- local_times = []
65
- local_ids = []
66
-
67
- multiplex(ITERATIONS, concurrency: CONCURRENCY) do |i|
68
- start = Time.now
69
- response = client.post(
70
- "/posts.json",
71
- headers: { "content-type" => "application/json" },
72
- body: { post: { name: "Author #{conn_id}-#{i}", title: "Post #{conn_id}-#{i}" } }.to_json
73
- )
74
- elapsed = Time.now - start
75
-
76
- if response && response[:status] == 201
77
- body = JSON.parse(response[:body]) rescue {}
78
- mutex.synchronize do
79
- local_times << elapsed
80
- local_ids << body["id"] if body["id"]
81
- end
82
- end
83
- end
84
-
85
- client.disconnect
86
- mutex.synchronize do
87
- results[:post].concat(local_times)
88
- all_created_ids.concat(local_ids)
89
- end
90
- end
91
- end
92
- conn_threads.each(&:join)
93
- end
94
- puts " #{results[:post].size} created in #{post_elapsed.round(2)}s (#{(results[:post].size / post_elapsed).round(1)} req/s)"
95
-
96
- # Phase 2: Concurrent multiplexed GETs
97
- puts "\nPhase 2: GET /posts.json (#{CONNECTIONS} conns x #{ITERATIONS}, #{CONCURRENCY} in-flight)..."
98
- get_elapsed = Benchmark.realtime do
99
- conn_threads = CONNECTIONS.times.map do
100
- Thread.new do
101
- client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
102
- client.connect
103
-
104
- local_times = []
105
-
106
- multiplex(ITERATIONS, concurrency: CONCURRENCY) do |_i|
107
- start = Time.now
108
- response = client.get("/posts.json")
109
- elapsed = Time.now - start
110
-
111
- if response && response[:status] == 200
112
- mutex.synchronize { local_times << elapsed }
113
- end
114
- end
115
-
116
- client.disconnect
117
- mutex.synchronize { results[:get].concat(local_times) }
118
- end
119
- end
120
- conn_threads.each(&:join)
121
- end
122
- puts " #{results[:get].size} fetched in #{get_elapsed.round(2)}s (#{(results[:get].size / get_elapsed).round(1)} req/s)"
123
-
124
- # Phase 3: Concurrent multiplexed DELETEs
125
- delete_count = all_created_ids.size
126
- puts "\nPhase 3: DELETE /posts/:id (#{delete_count} across #{CONNECTIONS} conns, #{CONCURRENCY} in-flight)..."
127
- delete_elapsed = Benchmark.realtime do
128
- id_chunks = all_created_ids.each_slice((all_created_ids.size.to_f / CONNECTIONS).ceil).to_a
129
-
130
- conn_threads = id_chunks.map do |ids|
131
- Thread.new do
132
- next if ids.empty?
133
-
134
- client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
135
- client.connect
136
-
137
- local_times = []
138
-
139
- multiplex(ids.size, concurrency: CONCURRENCY) do |i|
140
- start = Time.now
141
- response = client.delete("/posts/#{ids[i]}.json")
142
- elapsed = Time.now - start
143
-
144
- if response && response[:status] == 204
145
- mutex.synchronize { local_times << elapsed }
146
- end
147
- end
148
-
149
- client.disconnect
150
- mutex.synchronize { results[:delete].concat(local_times) }
151
- end
152
- end
153
- conn_threads.each(&:join)
154
- end
155
- puts " #{results[:delete].size} deleted in #{delete_elapsed.round(2)}s (#{(results[:delete].size / delete_elapsed).round(1)} req/s)"
156
-
157
- # Summary
158
- total_elapsed = post_elapsed + get_elapsed + delete_elapsed
159
- total_completed = results.values.sum(&:size)
160
-
161
- puts
162
- puts "=" * 70
163
- puts "RESULTS"
164
- puts "=" * 70
165
- Benchmarks::Helpers.print_stats("POST /posts.json", results[:post])
166
- Benchmarks::Helpers.print_stats("GET /posts.json", results[:get])
167
- Benchmarks::Helpers.print_stats("DELETE /posts/:id ", results[:delete])
168
- puts "-" * 70
169
- puts " Total: #{total_completed} requests in #{total_elapsed.round(2)}s (#{(total_completed / total_elapsed).round(2)} req/s)"
170
- puts "=" * 70
@@ -1,113 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # Throughput benchmark: measures req/sec and latency percentiles.
3
- # Tests both sequential and concurrent (multiplexed) modes.
4
- #
5
- # Self-contained (boots inline server with trivial Rack app):
6
- # ruby benchmarks/throughput.rb
7
- #
8
- # External server:
9
- # HOST=127.0.0.1 PORT=4433 ruby benchmarks/throughput.rb
10
-
11
- require_relative "helpers"
12
- require "benchmark"
13
-
14
- REQUESTS = ENV.fetch("REQUESTS", "500").to_i
15
- CONNECTIONS = ENV.fetch("CONNECTIONS", "5").to_i
16
- CONCURRENCY = ENV.fetch("CONCURRENCY", "8").to_i
17
- HOST = ENV["HOST"]
18
- PORT = ENV["PORT"]&.to_i
19
-
20
- def run_benchmark(host, port)
21
- # --- Sequential: 1 request at a time per connection ---
22
- puts "\n--- Sequential (1 stream/conn, #{CONNECTIONS} conns) ---"
23
- seq_times = []
24
- mutex = Mutex.new
25
-
26
- seq_elapsed = Benchmark.realtime do
27
- per_conn = REQUESTS / CONNECTIONS
28
-
29
- threads = CONNECTIONS.times.map do
30
- Thread.new do
31
- client = Quicsilver::Client.new(host, port, connection_timeout: 5000, request_timeout: 10)
32
- client.connect
33
-
34
- local = []
35
- per_conn.times do
36
- start = Time.now
37
- response = client.get("/")
38
- local << (Time.now - start) if response && response[:status] == 200
39
- end
40
-
41
- client.disconnect
42
- mutex.synchronize { seq_times.concat(local) }
43
- end
44
- end
45
- threads.each(&:join)
46
- end
47
-
48
- Benchmarks::Helpers.print_results(
49
- total_time: seq_elapsed, total_requests: REQUESTS,
50
- times: seq_times, failed: REQUESTS - seq_times.size, latency: true
51
- )
52
-
53
- # --- Concurrent: CONCURRENCY streams per connection ---
54
- puts "\n--- Concurrent (#{CONCURRENCY} streams/conn, #{CONNECTIONS} conns) ---"
55
- con_times = []
56
- con_failed = 0
57
-
58
- con_elapsed = Benchmark.realtime do
59
- per_conn = REQUESTS / CONNECTIONS
60
-
61
- threads = CONNECTIONS.times.map do
62
- Thread.new do
63
- client = Quicsilver::Client.new(host, port, connection_timeout: 5000, request_timeout: 10)
64
- client.connect
65
-
66
- local = []
67
- queue = Queue.new
68
- per_conn.times { |i| queue << i }
69
-
70
- workers = CONCURRENCY.times.map do
71
- Thread.new do
72
- while (queue.pop(true) rescue nil)
73
- start = Time.now
74
- response = client.get("/bench")
75
- dur = Time.now - start
76
- if response && response[:status] == 200
77
- local << dur
78
- else
79
- mutex.synchronize { con_failed += 1 }
80
- end
81
- end
82
- end
83
- end
84
- workers.each(&:join)
85
- client.disconnect
86
- mutex.synchronize { con_times.concat(local) }
87
- end
88
- end
89
- threads.each(&:join)
90
- end
91
-
92
- Benchmarks::Helpers.print_results(
93
- total_time: con_elapsed, total_requests: REQUESTS,
94
- times: con_times, failed: con_failed, latency: true
95
- )
96
- end
97
-
98
- Benchmarks::Helpers.print_header(
99
- "Quicsilver Throughput (trivial Rack app, no DB)",
100
- connections: CONNECTIONS,
101
- "reqs/conn": REQUESTS / CONNECTIONS,
102
- concurrency: "#{CONCURRENCY} streams/conn",
103
- total: REQUESTS
104
- )
105
-
106
- if HOST && PORT
107
- run_benchmark(HOST, PORT)
108
- else
109
- puts "Booting inline server..."
110
- Benchmarks::Helpers.with_server(Benchmarks::Helpers.benchmark_app) do |port|
111
- run_benchmark("localhost", port)
112
- end
113
- end