uringmachine 0.33.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: b6b62f0845e3bc8044bb1a21481d44c231d07a0fa191a92c7d4916ca6005a076
4
- data.tar.gz: 7d2533a8c3c7c1fcc5d03c183a39a7fb789a2f05510cfbdce5bc08d9744969f3
3
+ metadata.gz: 6c4607791ec8b3aefe794167dc5a0ec500fe4ec4a58b56962d90d27fdd4c41a7
4
+ data.tar.gz: 151205d0887aef4cc0cf022baf295b9be8934f2bb895fd87bd98680a837349a0
5
5
  SHA512:
6
- metadata.gz: a3d7b2d5ae670304d8540fd6684c3e2a8548c491866cbd3a2ddf1fb65034517f55b6faa41da4f7a8965d243cd86ae965c3ad3e6cb2d5026097ac36cdf4911d8b
7
- data.tar.gz: 800d1c0fe51d8399aefd799efe3338e060c15a6a072b86b33078696f2920f84e59d138e97dd4518eb71d2a00f7ac7c5dc8860caef3d24a645486946291a09dba
6
+ metadata.gz: a4f580578fabf57dbc7c9c8d609a000b4b2ebc56ffb1273e3496137b7ba01f4ca71767e37d84d978dbf49b5012a211374cceb774b787f748c4b3351531a42c42
7
+ data.tar.gz: 0cc8a8d93345894b2f2b3570390ae829dea4bac1e975a0d777c7359e74161273df9658b0797bd2ecbe45a262efbd7588dd8f9ec673577d91e7e8030bd9ab15e4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # 1.0.1 2026-05-12
2
+
3
+ - Fix Net::HTTP performance regression when running on fiber scheduler
4
+
5
+ # 1.0.0 2026-04-27
6
+
7
+ - Remove custom -Wxxx CFLAGS
8
+
1
9
  # 0.33.0 2026-04-12
2
10
 
3
11
  - Use buffer pool for `#read_each`, `#recv_each` methods
data/README.md CHANGED
@@ -37,7 +37,7 @@ implementation that allows integration with the entire Ruby ecosystem.
37
37
  - Excellent performance characteristics for concurrent I/O-bound applications.
38
38
  - `Fiber::Scheduler` implementation to automatically integrate with the Ruby
39
39
  ecosystem in a transparent fashion.
40
- - [IO](#io-api) class with automatic buffer management for reading.
40
+ - [UM::IO](#io-higher-level-api) class with automatic buffer management for reading.
41
41
  - Optimized I/O for encrypted SSL connections.
42
42
 
43
43
  ## Design
@@ -286,7 +286,7 @@ fiber = Fiber.schedule do
286
286
  end
287
287
  ```
288
288
 
289
- ## IO API
289
+ ## IO Higher-level API
290
290
 
291
291
  `UringMachine::IO` is a class designed for efficiently read from and write to a
292
292
  socket or other file descriptor. The IO class is ideal for implementing
data/TODO.md CHANGED
@@ -140,6 +140,4 @@ uint64_t alpha_numeric[] = [
140
140
  inline int test_char(char c, uint64 *bitmap) {
141
141
  return bitmap[c / 64] & (1UL << (c % 64));
142
142
  }
143
-
144
-
145
143
  ```
@@ -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
@@ -4,7 +4,7 @@ require_relative './common'
4
4
  require 'securerandom'
5
5
 
6
6
  C = ENV['C']&.to_i || 50
7
- I = 10
7
+ I = 100
8
8
  puts "C=#{C}"
9
9
 
10
10
  class UMBenchmark
@@ -12,6 +12,7 @@ class UMBenchmark
12
12
 
13
13
  def start_redis_server
14
14
  `docker run --name #{CONTAINER_NAME} -d -p 6379:6379 redis:latest`
15
+ create_redis_conn
15
16
  end
16
17
 
17
18
  def stop_redis_server
@@ -22,7 +23,7 @@ class UMBenchmark
22
23
  Redis.new
23
24
  rescue
24
25
  if retries < 3
25
- sleep 0.5
26
+ sleep 0.2
26
27
  create_redis_conn(retries + 1)
27
28
  else
28
29
  raise
@@ -31,7 +32,7 @@ class UMBenchmark
31
32
 
32
33
  def query_redis(conn)
33
34
  conn.set('abc', 'def')
34
- p conn.get('abc')
35
+ conn.get('abc')
35
36
  end
36
37
 
37
38
  def with_container
@@ -45,25 +46,33 @@ class UMBenchmark
45
46
  stop_redis_server
46
47
  end
47
48
 
49
+ def create_redis_conn(retries = 0)
50
+ Redis.new
51
+ rescue
52
+ raise if retries >= 3
53
+
54
+ sleep 0.5
55
+ create_redis_conn(retries + 1)
56
+ end
57
+
48
58
  def benchmark
49
59
  with_container {
50
60
  Benchmark.bm { run_benchmarks(it) }
51
61
  }
52
62
  end
53
63
 
54
- # def do_threads(threads, ios)
55
- # C.times.map do
56
- # threads << Thread.new do
57
- # conn = create_redis_conn
58
- # I.times { query_redis(conn) }
59
- # ensure
60
- # conn.close
61
- # end
62
- # end
63
- # end
64
+ def do_threads(threads, ios)
65
+ C.times.map do
66
+ threads << Thread.new do
67
+ conn = create_redis_conn
68
+ I.times { query_redis(conn) }
69
+ ensure
70
+ conn.close
71
+ end
72
+ end
73
+ end
64
74
 
65
75
  def do_scheduler(scheduler, ios)
66
- return if !scheduler.is_a?(UM::FiberScheduler)
67
76
  C.times do
68
77
  Fiber.schedule do
69
78
  conn = create_redis_conn
data/benchmark/common.rb CHANGED
@@ -62,18 +62,18 @@ class UMBenchmark
62
62
  # baseline_um: [:baseline_um, "UM no concurrency"],
63
63
  # thread_pool: [:thread_pool, "ThreadPool"],
64
64
 
65
- # threads: [:threads, "Threads"],
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
- # um_fs: [:scheduler, "UM FS"],
73
+ um_fs: [:scheduler, "UM FS"],
74
74
  # um_fs_x2: [:scheduler_x, "UM FS x2"],
75
75
 
76
- # um: [:um, "UM"],
76
+ um: [:um, "UM"],
77
77
  # um_sidecar: [:um, "UM sidecar"],
78
78
  # um_sqpoll: [:um, "UM sqpoll"],
79
79
  um_x2: [:um_x, "UM x2"],
data/ext/um/extconf.rb CHANGED
@@ -74,11 +74,4 @@ $defs << '-DHAVE_IO_URING_PREP_BIND' if config[:prep_bind]
74
74
  $defs << '-DHAVE_IO_URING_PREP_LISTEN' if config[:prep_listen]
75
75
  $defs << '-DHAVE_IO_URING_SEND_VECTORIZED' if config[:send_vectoized]
76
76
 
77
- $CFLAGS << ' -Werror -Wall -Wextra'
78
-
79
- if ENV['SANITIZE']
80
- $CFLAGS << ' -fsanitize=undefined,address -lasan'
81
- $LDFLAGS << ' -fsanitize=undefined,address -lasan'
82
- end
83
-
84
77
  create_makefile 'um_ext'
data/ext/um/um.c CHANGED
@@ -62,8 +62,7 @@ inline struct io_uring_sqe *um_get_sqe(struct um *machine, struct um_op *op) {
62
62
  machine->metrics.ops_unsubmitted, machine->metrics.ops_pending, machine->metrics.total_ops
63
63
  );
64
64
 
65
- struct io_uring_sqe *sqe;
66
- sqe = io_uring_get_sqe(&machine->ring);
65
+ struct io_uring_sqe *sqe = io_uring_get_sqe(&machine->ring);
67
66
  if (likely(sqe)) goto done;
68
67
 
69
68
  um_raise_internal_error("Submission queue full. Consider raising the machine size.");
@@ -282,22 +281,22 @@ void *um_wait_for_cqe_without_gvl(void *ptr) {
282
281
  return NULL;
283
282
  }
284
283
 
285
- inline void um_profile_wait_cqe_pre(struct um *machine, double *time_monotonic0, VALUE *fiber) {
286
- // *fiber = rb_fiber_current();
287
- *time_monotonic0 = um_get_time_monotonic();
288
- // double time_cpu = um_get_time_cpu();
289
- // double elapsed = time_cpu - machine->metrics.time_last_cpu;
290
- // um_update_fiber_time_run(fiber, time_monotonic0, elapsed);
291
- // machine->metrics.time_last_cpu = time_cpu;
292
- }
284
+ // inline void um_profile_wait_cqe_pre(struct um *machine, double *time_monotonic0, VALUE *fiber) {
285
+ // VALUE fiber = rb_fiber_current();
286
+ // *time_monotonic0 = um_get_time_monotonic();
287
+ // double time_cpu = um_get_time_cpu();
288
+ // double elapsed = time_cpu - machine->metrics.time_last_cpu;
289
+ // um_update_fiber_time_run(fiber, time_monotonic0, elapsed);
290
+ // machine->metrics.time_last_cpu = time_cpu;
291
+ // }
293
292
 
294
- inline void um_profile_wait_cqe_post(struct um *machine, double time_monotonic0, VALUE fiber) {
295
- // double time_cpu = um_get_time_cpu();
296
- double elapsed = um_get_time_monotonic() - time_monotonic0;
297
- // um_update_fiber_last_time(fiber, cpu_time1);
298
- machine->metrics.time_total_wait += elapsed;
299
- // machine->metrics.time_last_cpu = time_cpu;
300
- }
293
+ // inline void um_profile_wait_cqe_post(struct um *machine, double time_monotonic0, VALUE fiber) {
294
+ // // double time_cpu = um_get_time_cpu();
295
+ // double elapsed = um_get_time_monotonic() - time_monotonic0;
296
+ // // um_update_fiber_last_time(fiber, cpu_time1);
297
+ // machine->metrics.time_total_wait += elapsed;
298
+ // // machine->metrics.time_last_cpu = time_cpu;
299
+ // }
301
300
 
302
301
  inline void *um_wait_for_sidecar_signal(void *ptr) {
303
302
  struct um *machine = ptr;
@@ -329,11 +328,11 @@ static inline void um_wait_for_and_process_ready_cqes(struct um *machine, int wa
329
328
  // fprintf(stderr, "<< sidecar wait cqes\n");
330
329
  }
331
330
  else {
332
- double time_monotonic0 = 0.0;
333
- VALUE fiber;
334
- if (machine->profile_mode) um_profile_wait_cqe_pre(machine, &time_monotonic0, &fiber);
331
+ // double time_monotonic0 = 0.0;
332
+ // VALUE fiber;
333
+ // if (machine->profile_mode) um_profile_wait_cqe_pre(machine, &time_monotonic0, &fiber);
335
334
  rb_thread_call_without_gvl(um_wait_for_cqe_without_gvl, (void *)&ctx, RUBY_UBF_IO, 0);
336
- if (machine->profile_mode) um_profile_wait_cqe_post(machine, time_monotonic0, fiber);
335
+ // if (machine->profile_mode) um_profile_wait_cqe_post(machine, time_monotonic0, fiber);
337
336
 
338
337
  if (unlikely(ctx.result < 0)) {
339
338
  // the internal calls to (maybe submit) and wait for cqes may fail with:
@@ -357,14 +356,14 @@ static inline void um_wait_for_and_process_ready_cqes(struct um *machine, int wa
357
356
  }
358
357
  }
359
358
 
360
- inline void um_profile_switch(struct um *machine, VALUE next_fiber) {
361
- // *current_fiber = rb_fiber_current();
362
- // double time_cpu = um_get_time_cpu();
363
- // double elapsed = time_cpu - machine->metrics.time_last_cpu;
364
- // um_update_fiber_time_run(cur_fiber, time_cpu, elapsed);
365
- // um_update_fiber_time_wait(next_fiber, time_cpu);
366
- // machine->metrics.time_last_cpu = time_cpu;
367
- }
359
+ // inline void um_profile_switch(struct um *machine, VALUE next_fiber) {
360
+ // VALUE current_fiber = rb_fiber_current();
361
+ // double time_cpu = um_get_time_cpu();
362
+ // double elapsed = time_cpu - machine->metrics.time_last_cpu;
363
+ // um_update_fiber_time_run(cur_fiber, time_cpu, elapsed);
364
+ // um_update_fiber_time_wait(next_fiber, time_cpu);
365
+ // machine->metrics.time_last_cpu = time_cpu;
366
+ // }
368
367
 
369
368
  inline VALUE process_runqueue_op(struct um *machine, struct um_op *op) {
370
369
  DEBUG_PRINTF("* process_runqueue_op: op=%p kind=%s ref_count=%d flags=%x\n",
@@ -378,7 +377,7 @@ inline VALUE process_runqueue_op(struct um *machine, struct um_op *op) {
378
377
  op->flags &= ~OP_F_SCHEDULED;
379
378
  um_op_release(machine, op);
380
379
 
381
- if (machine->profile_mode) um_profile_switch(machine, fiber);
380
+ // if (machine->profile_mode) um_profile_switch(machine, fiber);
382
381
  VALUE ret = rb_fiber_transfer(fiber, 1, &value);
383
382
  RB_GC_GUARD(value);
384
383
  RB_GC_GUARD(ret);
@@ -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 = '0.33.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: 0.33.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