uringmachine 0.22.1 → 0.23.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbf0486feb49686a85ad162dd569ce125b4f168904e6f364e7c4358be234ca78
4
- data.tar.gz: 27d0e3533c7d41f87b95376258992e37ebfa0d42c32ac0b46e7595939b3f1afa
3
+ metadata.gz: b185b9cafdee3930061ed7101a12ccb500a8f131a9715a6a1268b22507ec2d85
4
+ data.tar.gz: c2d0fe4aced8f2340b2cdd29cf9540b7075198be56a0e695f58b9fbab5fae65c
5
5
  SHA512:
6
- metadata.gz: c4bf87080fe32e145e334944cc1871d109f5b11c5c3cb413c9de3f44cf09b9554ac3fabeb54650c18f67ed96d6381a0290ae8e461ff37b9fef3d27351675ff68
7
- data.tar.gz: a53addafe3007de3ad77f3e42e4ca3b3f90ae372af24b00beff1b5b486fa4c1f5d6c84de22788192b05328c14c7eeec4e602a77623a00c5c2f8de5bdbd8dca7d
6
+ metadata.gz: 75b0fe0a71242d728cbe1901457e41a11255c1922eca6501e3fa4286a7bd89a01ad7e5625baa24721ad63efc16da24a4b4d8ffeb85114be8fbe910066a0b033e
7
+ data.tar.gz: 64afeb65cc42c5b5c3af30dc361f2917a6341fd38e645114c48972bb6aa6617f496be6de040dfec6eefa102ae1d933cd726549890a9d57369f465be3b6df613f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+
2
+ # 0.23.0 2025-12-16
3
+
4
+ - Add `UM#accept_into_queue`, fix `#accept_each` to throw on error
5
+ - Use Set instead of Hash for holding pending fibers
6
+ - Add `UM#writev`, `UM#sendv` methods
7
+ - Allocate um_op and um_op_result in batches of 256
8
+ - Remove `SIGCLD` const
9
+
1
10
  # 0.22.1 2025-12-11
2
11
 
3
12
  - Comment out SIGCLD constant
@@ -12,6 +21,7 @@
12
21
  - More tests and benchmarks
13
22
  - Add `UM#await_fibers` for awaiting fibers
14
23
  - Add `UM.socketpair` for creating a socket pair
24
+ - Fix segfault caused by waiting fibers not being marked
15
25
  - Fiber scheduler:
16
26
  - Use fiber's mailbox for processing blocking operations
17
27
  - Add `#io_close`, `#yield` hooks, remove `#process_fork` hook
@@ -26,8 +36,8 @@
26
36
  - Add debug logging for key io_uring interactions
27
37
  - Add UM#mark and DEBUG_MARK for debugging specific UM instances
28
38
  - Short-circuit zero-length writes
29
- - Add optional file_offset argument to #read, #write. Add optional len and file_off
30
- set arguments to #write_async
39
+ - Add optional file_offset argument to #read, #write. Add optional len and
40
+ file_offset arguments to #write_async
31
41
  - Add support for specifying SQPOLL mode and SQ idle timeout in `UM#initialize`
32
42
  - Add support for specifying number of SQ entries in `UM#initialize`
33
43
  - Implement global worker pool for blocking operations in fiber scheduler
data/TODO.md CHANGED
@@ -1,148 +1,28 @@
1
1
  ## immediate
2
2
 
3
- ## Measuring CPU time for fibers
4
-
5
- - use CPU time (CLOCK_THREAD_CPUTIME_ID)
6
- - measure:
7
- - time each fiber is waiting
8
- - time each fiber is running
9
- - time machine is waiting (for CQEs)
10
- - time machine is running fibers from the runqueue
11
- - can be turned on/off at any time
12
- - no performance impact when off
13
-
14
- How can this be implemented:
15
-
16
- - `um_get_time_cpu()` function for reading CPU time (CLOCK_THREAD_CPUTIME_ID) as
17
- double.
18
- - add to `struct um`:
19
-
20
- ```c
21
- struct um {
22
- ...
23
- int profiling_mode;
24
- double total_time_run;
25
- double total_time_wait;
26
- double last_cpu_time;
27
- }
28
- ```
3
+ ## buffer rings - automatic management
29
4
 
30
- - `UM#profile=` to turn it on/off.
31
- - On `machine.profile = true`, reset `total_time_xxx` and `last_cpu_time`
5
+ ```ruby
6
+ # completely hands off
7
+ machine.read_each(fd) { |str| ... }
32
8
 
33
- ```c
34
- machine->total_time_run = 0;
35
- machine->total_time_wait = 0;
36
- machine->last_cpu_time = um_get_time_cpu();
37
- ```
9
+ # what if we want to get IO::Buffer?
10
+ machine.read_each(fd, io_buffer: true) { |iobuff, len| ... }
11
+ ```
38
12
 
39
- - when profiling is active:
40
- - before processing CQEs:
41
-
42
- ```c
43
- // before
44
- double cpu_time0;
45
- VALUE fiber;
46
- int profiling_mode = machine->profiling_mode;
47
- if (profiling_mode) {
48
- fiber = rb_fiber_current();
49
- cpu_time0 = um_get_time_cpu();
50
- double elapsed = cpu_time0 - machine->last_cpu_time;
51
- um_update_fiber_time_run(fiber, cpu_time0, elapsed);
52
- machine->total_time_run += elapsed;
53
- }
54
- process_cqes(...)
55
- // after
56
- if (profiling_mode) {
57
- double cpu_time1 = um_get_time_cpu();
58
- double elapsed = cpu_time1 - cpu_time0;
59
- um_update_fiber_last_time(fiber, cpu_time1);
60
- machine->total_time_wait += elapsed;
61
- machine->last_cpu_time = cpu_time1;
62
- }
63
- ```
64
-
65
- - when doing switching, in `um_process_runqueue_op`:
66
-
67
- ```c
68
- // before
69
- double cpu_time;
70
- VALUE cur_fiber;
71
- VALUE next_fiber = get_next_fiber(...);
72
- int profiling_mode = machine->profiling_mode;
73
- if (profiling_mode) {
74
- cur_fiber = rb_fiber_current();
75
- cpu_time = um_get_time_cpu();
76
- double elapsed = cpu_time - machine->last_cpu_time;
77
- um_update_fiber_time_run(cur_fiber, cpu_time, elapsed);
78
- machine->total_time_run += elapsed;
79
- um_update_fiber_time_wait(next_fiber, cpu_time);
80
- machine->last_cpu_time = cpu_time;
81
- }
82
- do_fiber_transfer(...)
83
- ```
84
-
85
- - updating fiber time instance vars:
86
-
87
- ```c
88
- inline void um_update_fiber_time_run(VALUE fiber, double stamp, double elapsed) {
89
- // VALUE fiber_stamp = rb_ivar_get(fiber, ID_time_last_cpu);
90
- VALUE fiber_total_run = rb_ivar_get(fiber, ID_time_total_run);
91
- double total = NIL_P(fiber_total_run) ?
92
- elapsed : NUM2DBL(fiber_total_run) + elapsed;
93
- rb_ivar_set(fiber, ID_time_total_run, DBL2NUM(total));
94
- rb_ivar_set(fiber, ID_time_last_cpu, DBL2NUM(stamp));
95
- }
96
-
97
- inline void um_update_fiber_time_wait(VALUE fiber, double stamp) {
98
- VALUE fiber_last_stamp = rb_ivar_get(fiber, ID_time_last_cpu);
99
- if (likely(!NIL_P(fiber_last_stamp))) {
100
- double last_stamp = NUM2DBL(fiber_last_stamp);
101
- double elapsed = stamp - last_stamp;
102
- VALUE fiber_total_wait = rb_ivar_get(fiber, ID_time_total_wait);
103
- double total = NIL_P(fiber_total_wait) ?
104
- elapsed : NUM2DBL(fiber_total_wait) + elapsed;
105
- rb_ivar_set(fiber, ID_time_total_wait, DBL2NUM(total));
106
- }
107
- else
108
- rb_ivar_set(fiber, ID_time_total_wait, DBL2NUM(0.0));
109
- rb_ivar_set(fiber, ID_time_last_cpu, DBL2NUM(stamp));
110
- }
111
- ```
112
-
113
- ## Metrics API
114
-
115
- - machine metrics: `UM#metrics` - returns a hash containing metrics:
13
+ ## write/send multiple buffers at once
116
14
 
117
- ```ruby
118
- {
119
- size:, # SQ size (entries)
120
- total_ops:, # total ops submitted
121
- total_fiber_switches:, # total fiber switches
122
- total_cqe_waits:, # total number of CQE waits
123
- ops_pending:, # number of pending ops
124
- ops_unsubmitted:, # number of unsubmitted
125
- ops_runqueue:, # number of ops in runqueue
126
- ops_free:, # number of ops in freelist
127
- ops_transient:, # number of ops in transient list
128
- hwm_pending:, # high water mark - pending ops
129
- hwm_unsubmitted:, # high water mark - unsubmitted ops
130
- hwm_runqueue:, # high water mark - runqueue depth
131
- hwm_free:, # high water mark - ops in free list
132
- hwm_transient:, # high water mark - ops in transient list
133
- # when profiling is active
134
- time_total_run:, # total CPU time running
135
- time_total_wait:, # total CPU time waiting for CQEs
136
- }
137
- ```
15
+ This is done as vectored IO:
138
16
 
139
- - For this we need to add tracking for:
140
- - runqueue list size
141
- - transient list size
142
- - free list size
143
- - Those will be done in um_op.c (in linked list management code)
17
+ ```ruby
18
+ machine.writev(fd, buf1, buf2, buf3)
144
19
 
145
- - All metrics info in kept in
20
+ # with optional file offset:
21
+ machine.writev(fd, buf1, buf2, buf3, 0)
22
+
23
+ # for the moment it won't take flags
24
+ machine.sendv(fd, buf1, buf2, buf3)
25
+ ```
146
26
 
147
27
  ## useful concurrency tools
148
28
 
@@ -152,13 +32,19 @@ How can this be implemented:
152
32
  debouncer = UM.debounce { }
153
33
  ```
154
34
 
35
+
36
+
37
+ ## polyvalent select
38
+
39
+ - select on multiple queues (ala Go)
40
+ - select on mixture of queues and fds
41
+
155
42
  ## ops
156
43
 
157
44
  - [ ] multishot timeout
158
45
  - [v] machine.periodically(interval) { ... }
159
46
  - [ ] machine.prep_timeout_multishot(interval)
160
47
 
161
- - writev
162
48
  - splice / - tee
163
49
  - sendto
164
50
  - recvfrom
data/benchmark/README.md CHANGED
@@ -21,7 +21,7 @@ implementations:
21
21
 
22
22
  - `UM`: fiber-based concurrency using the UringMachine low-level API.
23
23
 
24
- <img src="./chart.png">
24
+ <img src="./chart_all.png">
25
25
 
26
26
  ## Observations
27
27
 
@@ -38,6 +38,20 @@ class UMBenchmark
38
38
  end
39
39
  end
40
40
 
41
+ def do_baseline
42
+ GROUPS.times do
43
+ r, w = IO.pipe
44
+ r.sync = true
45
+ w.sync = true
46
+ ITERATIONS.times {
47
+ w.write(DATA)
48
+ r.read(SIZE)
49
+ }
50
+ r.close
51
+ w.close
52
+ end
53
+ end
54
+
41
55
  def do_scheduler(scheduler, ios)
42
56
  GROUPS.times do
43
57
  r, w = IO.pipe
data/benchmark/common.rb CHANGED
@@ -54,6 +54,7 @@ class UMBenchmark
54
54
  end
55
55
 
56
56
  @@benchmarks = {
57
+ baseline: [:baseline, "No Concurrency"],
57
58
  threads: [:threads, "Threads"],
58
59
  thread_pool: [:thread_pool, "ThreadPool"],
59
60
  async_uring: [:scheduler, "Async uring"],
@@ -69,6 +70,10 @@ class UMBenchmark
69
70
  end
70
71
  end
71
72
 
73
+ def run_baseline
74
+ do_baseline
75
+ end
76
+
72
77
  def run_threads
73
78
  threads = []
74
79
  ios = []
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ source 'https://rubygems.org'
7
+ gem 'uringmachine', path: '..'
8
+ gem 'benchmark-ips'
9
+ end
10
+
11
+ require 'benchmark/ips'
12
+ require 'uringmachine'
13
+
14
+ @machine = UM.new
15
+
16
+ make_socket_pair = -> do
17
+ port = 10000 + rand(30000)
18
+ server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
19
+ @machine.setsockopt(server_fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
20
+ @machine.bind(server_fd, '127.0.0.1', port)
21
+ @machine.listen(server_fd, UM::SOMAXCONN)
22
+
23
+ client_conn_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
24
+ @machine.connect(client_conn_fd, '127.0.0.1', port)
25
+
26
+ server_conn_fd = @machine.accept(server_fd)
27
+
28
+ @machine.close(server_fd)
29
+ [client_conn_fd, server_conn_fd]
30
+ end
31
+
32
+ @client_fd, @server_fd = make_socket_pair.()
33
+
34
+ @read_buf = +''
35
+ @read_fiber = @machine.spin do
36
+ while true
37
+ @machine.read(@client_fd, @read_buf, 65536, 0)
38
+ end
39
+ end
40
+
41
+ STR_COUNT = ARGV[0]&.to_i || 3
42
+ STR_SIZE = ARGV[1]&.to_i || 100
43
+
44
+ @parts = ['*' * STR_SIZE] * STR_COUNT
45
+
46
+ @server_io = IO.new(@server_fd)
47
+ @server_io.sync = true
48
+ def io_write
49
+ @server_io.write(*@parts)
50
+ @machine.snooze
51
+ end
52
+
53
+ def um_write
54
+ str = @parts.join
55
+ len = str.bytesize
56
+
57
+ while len > 0
58
+ ret = @machine.write(@server_fd, str, len)
59
+ len -= ret
60
+ str = str[ret..-1] if len > 0
61
+ end
62
+ end
63
+
64
+ def um_send
65
+ str = @parts.join
66
+ @machine.send(@server_fd, str, str.bytesize, UM::MSG_WAITALL)
67
+ end
68
+
69
+ @bgid = @machine.setup_buffer_ring(0, 8)
70
+ def um_send_bundle
71
+ @machine.send_bundle(@server_fd, @bgid, @parts)
72
+ end
73
+
74
+ p(STR_COUNT:, STR_SIZE:)
75
+
76
+ Benchmark.ips do |x|
77
+ x.report('IO#write') { io_write }
78
+ x.report('UM#write') { um_write }
79
+ x.report('UM#send') { um_send }
80
+ x.report('UM#send_bundle') { um_send_bundle }
81
+
82
+ x.compare!(order: :baseline)
83
+ end
data/benchmark/send.rb CHANGED
@@ -5,49 +5,21 @@ require 'bundler/inline'
5
5
  gemfile do
6
6
  source 'https://rubygems.org'
7
7
  gem 'uringmachine', path: '..'
8
+ gem 'benchmark'
8
9
  gem 'benchmark-ips'
9
10
  end
10
11
 
11
12
  require 'benchmark/ips'
12
13
  require 'uringmachine'
14
+ require 'securerandom'
13
15
 
14
16
  @machine = UM.new
15
17
 
16
- make_socket_pair = -> do
17
- port = 10000 + rand(30000)
18
- server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
19
- @machine.setsockopt(server_fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
20
- @machine.bind(server_fd, '127.0.0.1', port)
21
- @machine.listen(server_fd, UM::SOMAXCONN)
18
+ @parts = ['1' * 256, '2' * (1 << 14), '3' * 256]
19
+ @total_len = @parts.map(&:bytesize).reduce(:+)
22
20
 
23
- client_conn_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
24
- @machine.connect(client_conn_fd, '127.0.0.1', port)
25
-
26
- server_conn_fd = @machine.accept(server_fd)
27
-
28
- @machine.close(server_fd)
29
- [client_conn_fd, server_conn_fd]
30
- end
31
-
32
- @client_fd, @server_fd = make_socket_pair.()
33
-
34
- @read_buf = +''
35
- @read_fiber = @machine.spin do
36
- while true
37
- @machine.read(@client_fd, @read_buf, 65536, 0)
38
- end
39
- end
40
-
41
- STR_COUNT = ARGV[0]&.to_i || 3
42
- STR_SIZE = ARGV[1]&.to_i || 100
43
-
44
- @parts = ['*' * STR_SIZE] * STR_COUNT
45
-
46
- @server_io = IO.new(@server_fd)
47
- @server_io.sync = true
48
21
  def io_write
49
22
  @server_io.write(*@parts)
50
- @machine.snooze
51
23
  end
52
24
 
53
25
  def um_write
@@ -55,28 +27,51 @@ def um_write
55
27
  len = str.bytesize
56
28
 
57
29
  while len > 0
58
- ret = @machine.write(@server_fd, str, len)
30
+ ret = @machine.write(@s2, str, len)
59
31
  len -= ret
60
32
  str = str[ret..-1] if len > 0
61
33
  end
62
34
  end
63
35
 
36
+ def um_writev
37
+ @machine.writev(@s2, *@parts)
38
+ end
39
+
64
40
  def um_send
65
41
  str = @parts.join
66
- @machine.send(@server_fd, str, str.bytesize, UM::MSG_WAITALL)
42
+ @machine.send(@s2, str, str.bytesize, UM::MSG_WAITALL)
43
+ end
44
+
45
+ def um_sendv
46
+ @machine.sendv(@s2, *@parts)
67
47
  end
68
48
 
69
49
  @bgid = @machine.setup_buffer_ring(0, 8)
70
50
  def um_send_bundle
71
- @machine.send_bundle(@server_fd, @bgid, @parts)
51
+ @machine.send_bundle(@s2, @bgid, @parts)
72
52
  end
73
53
 
74
- p(STR_COUNT:, STR_SIZE:)
54
+ PORT = SecureRandom.rand(10001..60001)
55
+
56
+ pid = fork { `nc -l localhost #{PORT} > /dev/null` }
57
+ at_exit {
58
+ Process.kill(:SIGKILL, pid)
59
+ Process.wait(pid)
60
+ }
61
+
62
+ sleep 0.5
63
+ @s2 = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
64
+ @machine.connect(@s2, '127.0.0.1', PORT)
65
+ @server_io = IO.for_fd(@s2)
66
+ @server_io.sync = true
67
+
75
68
 
76
69
  Benchmark.ips do |x|
77
70
  x.report('IO#write') { io_write }
78
71
  x.report('UM#write') { um_write }
72
+ x.report('UM#writev') { um_writev }
79
73
  x.report('UM#send') { um_send }
74
+ x.report('UM#sendv') { um_sendv }
80
75
  x.report('UM#send_bundle') { um_send_bundle }
81
76
 
82
77
  x.compare!(order: :baseline)
data/ext/um/extconf.rb CHANGED
@@ -24,7 +24,8 @@ def get_config
24
24
  {
25
25
  kernel_version: combined_version,
26
26
  prep_bind: combined_version >= 611,
27
- prep_listen: combined_version >= 611
27
+ prep_listen: combined_version >= 611,
28
+ send_vectoized: combined_version >= 617
28
29
  }
29
30
  end
30
31
 
@@ -45,6 +46,10 @@ if !find_header 'liburing.h', File.join(liburing_path, 'src/include')
45
46
  raise "Couldn't find liburing.h"
46
47
  end
47
48
 
49
+ if !find_header 'liburing/io_uring.h', File.join(liburing_path, 'src/include')
50
+ raise "Couldn't find liburing/io_uring.h"
51
+ end
52
+
48
53
  if !find_library('uring', nil, File.join(liburing_path, 'src'))
49
54
  raise "Couldn't find liburing.a"
50
55
  end
@@ -54,6 +59,7 @@ have_func("&rb_process_status_new")
54
59
  $defs << "-DUM_KERNEL_VERSION=#{config[:kernel_version]}"
55
60
  $defs << '-DHAVE_IO_URING_PREP_BIND' if config[:prep_bind]
56
61
  $defs << '-DHAVE_IO_URING_PREP_LISTEN' if config[:prep_listen]
62
+ $defs << '-DHAVE_IO_URING_SEND_VECTORIZED' if config[:send_vectoized]
57
63
 
58
64
  $CFLAGS << ' -Wno-pointer-arith'
59
65