uringmachine 1.0.0 → 1.0.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8704dd734601efec57a8bb095c43dc44f752cd01b0cbe04c550fbc321fb2fbfa
4
- data.tar.gz: 86d5a99e31296951b2fb565bd12a60b64f6ef43ba331c16bb1307d5a27b27971
3
+ metadata.gz: 6c4607791ec8b3aefe794167dc5a0ec500fe4ec4a58b56962d90d27fdd4c41a7
4
+ data.tar.gz: 151205d0887aef4cc0cf022baf295b9be8934f2bb895fd87bd98680a837349a0
5
5
  SHA512:
6
- metadata.gz: e83aece1507a0be85ccbcc7bd229dbe2976fab489a59bb07814e147f51de5a9e526a4a4fbc08ddc5b3b01d96bfd132ad79de40acb58bcfca31eaab2b1516df01
7
- data.tar.gz: 0d5572ccc1822cf50d9c0d3e1ad0fccb39f80640b753a9962a737a58ca6ea0024cb3342fe3c91d097f4f785186ded75f3cde587463409a2148ddb81bfef8de8f
6
+ metadata.gz: a4f580578fabf57dbc7c9c8d609a000b4b2ebc56ffb1273e3496137b7ba01f4ca71767e37d84d978dbf49b5012a211374cceb774b787f748c4b3351531a42c42
7
+ data.tar.gz: 0cc8a8d93345894b2f2b3570390ae829dea4bac1e975a0d777c7359e74161273df9658b0797bd2ecbe45a262efbd7588dd8f9ec673577d91e7e8030bd9ab15e4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ # 1.0.1 2026-05-12
2
+
3
+ - Fix Net::HTTP performance regression when running on fiber scheduler
4
+
1
5
  # 1.0.0 2026-04-27
2
6
 
3
7
  - Remove custom -Wxxx CFLAGS
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './common'
4
+ require_relative './bm_net_http_support'
5
+
6
+ require 'net/http'
7
+
8
+ CONCURRENCY = ENV['C']&.to_i || 50
9
+ ITERATIONS = ENV['I']&.to_i || 200
10
+
11
+ # Adapted from benchmark code in https://github.com/yaroslav/carbon_fiber
12
+
13
+ RESPONSE_BODY = %({"ok":true,"value":12345})
14
+ REQUEST_PATH = "/api"
15
+
16
+ SERVER = LoopbackServer.new(
17
+ response_body: RESPONSE_BODY,
18
+ content_type: "application/json"
19
+ )
20
+ PORT = SERVER.port
21
+ sleep(0.1)
22
+
23
+ class UMBenchmark
24
+ def run_http_client
25
+ Net::HTTP.start("127.0.0.1", PORT, nil, nil) do |http|
26
+ ITERATIONS.times do
27
+ request = Net::HTTP::Get.new(REQUEST_PATH)
28
+ response = http.request(request)
29
+ raise "unexpected status #{response.code}" unless response.code == "200"
30
+ raise "unexpected body size" unless response.body&.bytesize == RESPONSE_BODY.bytesize
31
+ end
32
+ end
33
+ rescue => e
34
+ p e
35
+ p e.backtrace
36
+ exit!
37
+ end
38
+
39
+ def do_threads(threads, ios)
40
+ CONCURRENCY.times do
41
+ threads << Thread.new { run_http_client }
42
+ end
43
+ end
44
+
45
+ def do_scheduler(scheduler, ios)
46
+ CONCURRENCY.times do
47
+ Fiber.schedule { run_http_client }
48
+ end
49
+ end
50
+
51
+ def do_scheduler_x(div, scheduler, ios)
52
+ (CONCURRENCY/div).times { run_http_client }
53
+ end
54
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from benchmark code in https://github.com/yaroslav/carbon_fiber
4
+
5
+ require "socket"
6
+
7
+ class LoopbackServer
8
+ attr_reader :port
9
+
10
+ def initialize(response_body:, content_type:)
11
+ @response_body = response_body.b
12
+ @content_type = content_type
13
+ @response = build_response(@response_body, @content_type)
14
+ @server = TCPServer.new("127.0.0.1", 0)
15
+ @port = @server.local_address.ip_port
16
+ @closed = false
17
+ @lock = Thread::Mutex.new
18
+ @client_threads = []
19
+ @client_sockets = []
20
+ @accept_thread = Thread.new { accept_loop }
21
+ @accept_thread.report_on_exception = false
22
+ end
23
+
24
+ def close
25
+ return if @closed
26
+
27
+ if Fiber.scheduler
28
+ closer = Thread.new { close_without_scheduler }
29
+ closer.report_on_exception = false
30
+ closer.join(2.0)
31
+ else
32
+ close_without_scheduler
33
+ end
34
+ rescue
35
+ end
36
+
37
+ private
38
+
39
+ def close_without_scheduler
40
+ return if @closed
41
+
42
+ @closed = true
43
+ @server.close unless @server.closed?
44
+ @lock.synchronize do
45
+ @client_sockets.each do |socket|
46
+ socket.close unless socket.closed?
47
+ rescue
48
+ end
49
+ end
50
+ @accept_thread.join(0.5)
51
+ @lock.synchronize { @client_threads.dup }.each { |thread| thread.join(0.5) }
52
+ end
53
+
54
+ def accept_loop
55
+ loop do
56
+ conn = @server.accept
57
+ conn.sync = true
58
+
59
+ worker = Thread.new(conn) do |socket|
60
+ Thread.current.report_on_exception = false
61
+ handle_client(socket)
62
+ end
63
+
64
+ @lock.synchronize do
65
+ @client_sockets << conn
66
+ @client_threads << worker
67
+ end
68
+ end
69
+ rescue IOError, Errno::EBADF
70
+ nil
71
+ end
72
+
73
+ def handle_client(socket)
74
+ loop do
75
+ request = read_request(socket)
76
+ break unless request
77
+
78
+ socket.write(@response)
79
+ end
80
+ rescue IOError, Errno::ECONNRESET, Errno::EPIPE
81
+ nil
82
+ ensure
83
+ begin
84
+ @lock.synchronize { @client_sockets.delete(socket) }
85
+ socket.close unless socket.closed?
86
+ rescue # rubocop:disable Lint/SuppressedException
87
+ end
88
+ end
89
+
90
+ def read_request(socket)
91
+ buffer = +""
92
+ loop do
93
+ buffer << socket.readpartial(4096)
94
+ break if buffer.include?("\r\n\r\n")
95
+ end
96
+ buffer
97
+ rescue IOError, Errno::ECONNRESET, Errno::EPIPE
98
+ nil
99
+ end
100
+
101
+ def build_response(body, content_type)
102
+ "HTTP/1.1 200 OK\r\n" \
103
+ "Content-Length: #{body.bytesize}\r\n" \
104
+ "Content-Type: #{content_type}\r\n" \
105
+ "Connection: keep-alive\r\n" \
106
+ "\r\n" \
107
+ "#{body}"
108
+ end
109
+ end
data/benchmark/common.rb CHANGED
@@ -64,14 +64,14 @@ class UMBenchmark
64
64
 
65
65
  threads: [:threads, "Threads"],
66
66
 
67
- # async_uring: [:scheduler, "Async uring"],
67
+ async_uring: [:scheduler, "Async uring"],
68
68
  # async_uring_x2: [:scheduler_x, "Async uring x2"],
69
69
 
70
70
  # async_epoll: [:scheduler, "Async epoll"],
71
71
  # async_epoll_x2: [:scheduler_x, "Async epoll x2"],
72
72
 
73
73
  um_fs: [:scheduler, "UM FS"],
74
- um_fs_x2: [:scheduler_x, "UM FS x2"],
74
+ # um_fs_x2: [:scheduler_x, "UM FS x2"],
75
75
 
76
76
  um: [:um, "UM"],
77
77
  # um_sidecar: [:um, "UM sidecar"],
@@ -210,13 +210,21 @@ class UringMachine
210
210
  # Waits for the given io to become ready.
211
211
  #
212
212
  # @param io [IO] IO object
213
- # @param events [Number] readiness bitmask
213
+ # @param events [Integer] readiness bitmask
214
214
  # @param timeout [Number, nil] optional timeout
215
- # @return [void]
215
+ # @return [Integer] ready events bitmask
216
216
  def io_wait(io, events, timeout = nil)
217
- # p io_wait: [io, events, timeout]
217
+ # Useful note from the Carbon Fiber Fiber::Scheduler implementation:
218
+ # Net::HTTP#begin_transport calls `wait_readable(0)` before every
219
+ # keep-alive request to probe for a closed connection. On a healthy
220
+ # connection this is always "not readable", so returning false
221
+ # directly saves one MSG_PEEK recvfrom per request. On a genuinely
222
+ # closed connection Net::HTTP will detect EOF on the next real read
223
+ # and reconnect — one extra request's worth of latency, at most.
224
+ return 0 if timeout == 0 && events == ::IO::READABLE && io.is_a?(BasicSocket)
225
+
218
226
  timeout ||= io.timeout
219
- if timeout
227
+ if timeout && timeout > 0
220
228
  @machine.timeout(timeout, Timeout::Error) {
221
229
  @machine.poll(io.fileno, events)
222
230
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class UringMachine
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uringmachine
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -61,6 +61,8 @@ files:
61
61
  - benchmark/bm_io_ssl.rb
62
62
  - benchmark/bm_mutex_cpu.rb
63
63
  - benchmark/bm_mutex_io.rb
64
+ - benchmark/bm_net_http.rb
65
+ - benchmark/bm_net_http_support.rb
64
66
  - benchmark/bm_pg_client.rb
65
67
  - benchmark/bm_queue.rb
66
68
  - benchmark/bm_redis_client.rb