io-event 1.16.0 → 1.16.2

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: 4928b16497387fabcf4eb19d7dfefe5840674e385b086db2f0df0a15367e4d09
4
- data.tar.gz: 635ceb881767fc1d6fd876f39da295b9402971f3aaf0e5778ef022108abaf890
3
+ metadata.gz: 8cc6abcf010ce881e4623a5736241660d1ebf6a84883767d761616ddcb23bf4e
4
+ data.tar.gz: 4a438756e87e8feaefaafb40014f36c2b240fbbd837d4581c657eb310cb9b95a
5
5
  SHA512:
6
- metadata.gz: 943e688606c2df3398c465103e1212143d2af98cf0b36f761f54ca4f9f627fe833992106c5b8c851fe61b7dba5b73dcf81978ec6db7f126af6532b25bd19815f
7
- data.tar.gz: e3870c0635b7f3ea0fd01390f5401adb2cd99fc9e7afc326b05efbf870715c609a5cde47f45b0e326223c7caad752ae51258cbe6f92eac7a7e9b799e58de3f2a
6
+ metadata.gz: b910e009ff697f179290d916e5914621d25a5478e0f09ac806563a0c3808b30fc28ca652042608e60625830468f37088943559c590112dfadb0d3a48d3b54688
7
+ data.tar.gz: c7501e4ef87e5c9744d666e43494b60b2b3c490d9e9cb55b75a87f7de199e1421b49ba0ce5e176cbaf230d6f3b8f838a5745cff81bac6f826328e7665de7a9e9
checksums.yaml.gz.sig CHANGED
Binary file
@@ -32,7 +32,6 @@ struct IO_Event_Selector_URing
32
32
  {
33
33
  struct IO_Event_Selector backend;
34
34
  struct io_uring ring;
35
- size_t pending;
36
35
 
37
36
  // Flag indicating whether the selector is currently blocked in a system call.
38
37
  // Set to 1 when blocked in io_uring_wait_cqe_timeout() without GVL, 0 otherwise.
@@ -239,7 +238,6 @@ VALUE IO_Event_Selector_URing_allocate(VALUE self) {
239
238
  IO_Event_Selector_initialize(&selector->backend, self, Qnil);
240
239
  selector->ring.ring_fd = -1;
241
240
 
242
- selector->pending = 0;
243
241
  selector->blocked = 0;
244
242
  selector->interrupt.descriptor = -1;
245
243
  selector->wakeup_registered = 0;
@@ -421,15 +419,14 @@ void IO_Event_Selector_URing_dump_completion_queue(struct IO_Event_Selector_URin
421
419
  // Flush the submission queue, optionally yielding if unsuccessful.
422
420
  static
423
421
  int io_uring_submit_all(struct IO_Event_Selector_URing *selector, bool yield) {
424
- while (selector->pending > 0) {
422
+ struct io_uring *ring = &selector->ring;
423
+
424
+ while (io_uring_sq_ready(ring) > 0) {
425
425
  int result = io_uring_submit(&selector->ring);
426
426
 
427
- if (result >= 0) {
428
- // io_uring_submit() returns the number of submitted SQEs
429
- selector->pending -= result;
430
- } else if (result == -EBUSY || result == -EAGAIN) {
427
+ if (result == -EBUSY || result == -EAGAIN) {
431
428
  if (yield) IO_Event_Selector_yield(&selector->backend);
432
- } else {
429
+ } else if (result < 0) {
433
430
  rb_syserr_fail(-result, "io_uring_submit_all:io_uring_submit");
434
431
  return result;
435
432
  }
@@ -442,7 +439,10 @@ int io_uring_submit_all(struct IO_Event_Selector_URing *selector, bool yield) {
442
439
  // Flush the submission queue if pending operations are present.
443
440
  static
444
441
  int io_uring_submit_flush(struct IO_Event_Selector_URing *selector) {
445
- if (DEBUG) fprintf(stderr, "io_uring_submit_now(pending=%ld)\n", selector->pending);
442
+ if (DEBUG) {
443
+ unsigned pending = io_uring_sq_ready(&selector->ring);
444
+ fprintf(stderr, "io_uring_submit_flush(pending=%u)\n", pending);
445
+ }
446
446
 
447
447
  return io_uring_submit_all(selector, false);
448
448
  }
@@ -450,15 +450,21 @@ int io_uring_submit_flush(struct IO_Event_Selector_URing *selector) {
450
450
  // Immediately flush the submission queue, yielding to the event loop if it was not successful.
451
451
  static
452
452
  int io_uring_submit_now(struct IO_Event_Selector_URing *selector) {
453
- if (DEBUG) fprintf(stderr, "io_uring_submit_now(pending=%ld)\n", selector->pending);
454
-
453
+ if (DEBUG) {
454
+ unsigned pending = io_uring_sq_ready(&selector->ring);
455
+ fprintf(stderr, "io_uring_submit_now(pending=%u)\n", pending);
456
+ }
457
+
455
458
  return io_uring_submit_all(selector, true);
456
459
  }
457
460
 
458
461
  // Submit a pending operation. This does not submit the operation immediately, but instead defers it to the next call to `io_uring_submit_flush` or `io_uring_submit_now`. This is useful for operations that are not urgent, but should be used with care as it can lead to a deadlock if the submission queue is not flushed.
459
462
  static
460
463
  void io_uring_submit_pending(struct IO_Event_Selector_URing *selector) {
461
- if (DEBUG) fprintf(stderr, "io_uring_submit_pending(ring=%p, pending=%ld)\n", &selector->ring, selector->pending);
464
+ if (DEBUG) {
465
+ unsigned pending = io_uring_sq_ready(&selector->ring);
466
+ fprintf(stderr, "io_uring_submit_pending(ring=%p, pending=%u)\n", &selector->ring, pending);
467
+ }
462
468
  }
463
469
 
464
470
  struct io_uring_sqe * io_get_sqe(struct IO_Event_Selector_URing *selector) {
@@ -471,7 +477,6 @@ struct io_uring_sqe * io_get_sqe(struct IO_Event_Selector_URing *selector) {
471
477
  sqe = io_uring_get_sqe(&selector->ring);
472
478
  }
473
479
 
474
- selector->pending += 1;
475
480
  return sqe;
476
481
  }
477
482
 
@@ -10,6 +10,8 @@ class IO
10
10
  # of its contents to determine priority.
11
11
  # See <https://en.wikipedia.org/wiki/Binary_heap> for explanations of the main methods.
12
12
  class PriorityHeap
13
+ HEAPIFY_INSERT_RATIO = 2
14
+
13
15
  # Initializes the heap.
14
16
  def initialize
15
17
  # The heap is represented with an array containing a binary tree. See
@@ -79,18 +81,34 @@ class IO
79
81
  return self
80
82
  end
81
83
 
82
- # Add multiple elements to the heap efficiently in O(n) time.
83
- # This is more efficient than calling push multiple times (O(n log n)).
84
+ # Add multiple elements to the heap efficiently.
84
85
  #
85
86
  # @parameter elements [Array] The elements to add to the heap.
86
87
  # @returns [self] Returns self for method chaining.
87
88
  def concat(elements)
88
89
  return self if elements.empty?
89
90
 
90
- # Add all elements to the array without maintaining heap property - O(n)
91
- @contents.concat(elements)
91
+ # Rebuilding the whole heap is `O(n + m)`, where `n` is the existing heap size and `m` is the appended batch size. Incremental `push` is `O(m log(n))`, but is often closer to `O(m)` when appended elements are later than the existing entries and do not bubble far. Prefer `heapify` only when building from empty or when the batch dominates the existing heap.
92
+ if @contents.empty? || elements.size > @contents.size * HEAPIFY_INSERT_RATIO
93
+ @contents.concat(elements)
94
+ heapify!
95
+ else
96
+ elements.each{|element| push(element)}
97
+ end
98
+
99
+ return self
100
+ end
101
+
102
+ # Mutate the heap contents directly, then rebuild the heap property.
103
+ #
104
+ # This supports batched operations that can be completed with a single `O(n)` heapify instead of multiple `O(log n)` heap operations.
105
+ #
106
+ # @yields {|contents| ...} The heap contents array.
107
+ # @returns [self] Returns self for method chaining.
108
+ def heapify
109
+ yield @contents
92
110
 
93
- # Rebuild the heap property for the entire array - O(n)
111
+ # The block may arbitrarily append, delete or reorder contents, so repair the invariant with one `O(n)` bottom-up heapify pass.
94
112
  heapify!
95
113
 
96
114
  return self
@@ -159,7 +159,7 @@ module IO::Event
159
159
  def io_wait(fiber, io, events)
160
160
  waiter = @waiting[io] = Waiter.new(fiber, events, @waiting[io])
161
161
 
162
- @loop.transfer
162
+ @loop.transfer || false
163
163
  ensure
164
164
  waiter&.invalidate
165
165
  end
@@ -9,6 +9,8 @@ class IO
9
9
  module Event
10
10
  # An efficient sorted set of timers.
11
11
  class Timers
12
+ COMPACT_MINIMUM_COUNT = 128
13
+
12
14
  # A handle to a scheduled timer.
13
15
  class Handle
14
16
  # Initialize the handle with the given time and block.
@@ -16,6 +18,7 @@ class IO
16
18
  # @parameter time [Float] The time at which the block should be called.
17
19
  # @parameter block [Proc] The block to call.
18
20
  def initialize(time, block)
21
+ @timers = nil
19
22
  @time = time
20
23
  @block = block
21
24
  end
@@ -26,6 +29,16 @@ class IO
26
29
  # @attribute [Proc | Nil] The block to call when the timer fires.
27
30
  attr :block
28
31
 
32
+ # Mark the timer as inserted into the heap.
33
+ def schedule!(timers)
34
+ @timers = timers
35
+ end
36
+
37
+ # Mark the timer as removed from the heap.
38
+ def removed!
39
+ @timers = nil
40
+ end
41
+
29
42
  # Compare the handle with another handle.
30
43
  #
31
44
  # @parameter other [Handle] The other handle to compare with.
@@ -49,7 +62,14 @@ class IO
49
62
 
50
63
  # Cancel the timer.
51
64
  def cancel!
65
+ return if @block.nil?
66
+
52
67
  @block = nil
68
+
69
+ if timers = @timers
70
+ @timers = nil
71
+ timers.cancelled!(self)
72
+ end
53
73
  end
54
74
 
55
75
  # @returns [Boolean] Whether the timer has been cancelled.
@@ -62,6 +82,7 @@ class IO
62
82
  def initialize
63
83
  @heap = PriorityHeap.new
64
84
  @scheduled = []
85
+ @cancelled = 0
65
86
  end
66
87
 
67
88
  # @returns [Integer] The number of timers in the heap.
@@ -101,6 +122,8 @@ class IO
101
122
  while handle = @heap.peek
102
123
  if handle.cancelled?
103
124
  @heap.pop
125
+ handle.removed!
126
+ @cancelled -= 1 if @cancelled > 0
104
127
  else
105
128
  return handle.time - now
106
129
  end
@@ -123,9 +146,12 @@ class IO
123
146
  while handle = @heap.peek
124
147
  if handle.cancelled?
125
148
  @heap.pop
149
+ handle.removed!
150
+ @cancelled -= 1 if @cancelled > 0
126
151
  elsif handle.time <= now
127
152
  # Remove the earliest timer from the heap:
128
153
  @heap.pop
154
+ handle.removed!
129
155
 
130
156
  # Call the block:
131
157
  handle.call(now)
@@ -137,11 +163,53 @@ class IO
137
163
 
138
164
  # Flush all scheduled timers into the heap.
139
165
  #
140
- # This is a small optimization which assumes that most timers (timeouts) will be cancelled.
166
+ # Scheduling appends to `@scheduled` and cancellation is `O(1)`. We pay the cost of filtering and heap repair here, where we can batch work and choose between incremental insertion and one `heapify` pass.
141
167
  protected def flush!
142
- while handle = @scheduled.pop
143
- @heap.push(handle) unless handle.cancelled?
168
+ # Once cancelled handles are both numerous and a large fraction of the heap, rebuild the heap. This is `O(n + m)`, but it removes retained cancelled handles and appends live scheduled handles in the same `heapify` pass instead of paying for separate filtering and insertion.
169
+ if @cancelled >= COMPACT_MINIMUM_COUNT && @cancelled * 2 > @heap.size
170
+ @heap.heapify do |contents|
171
+ contents.delete_if do |handle|
172
+ if handle.cancelled?
173
+ handle.removed!
174
+ true
175
+ end
176
+ end
177
+
178
+ @scheduled.each do |handle|
179
+ unless handle.cancelled?
180
+ handle.schedule!(self)
181
+ contents << handle
182
+ end
183
+ end
184
+ end
185
+
186
+ @cancelled = 0
187
+ else
188
+ # If we are not compacting the heap, filter scheduled handles in place before insertion. This keeps cancelled scheduled handles out of the heap without adding cancellation-time heap deletion.
189
+ @scheduled.delete_if do |handle|
190
+ if handle.cancelled?
191
+ true
192
+ else
193
+ handle.schedule!(self)
194
+ false
195
+ end
196
+ end
197
+
198
+ # Small heaps can become entirely cancelled before reaching the compaction threshold. Clear those immediately so `size` does not retain cancelled handles indefinitely.
199
+ if @cancelled == @heap.size && @scheduled.empty?
200
+ @heap.clear!
201
+ @cancelled = 0
202
+ else
203
+ @heap.concat(@scheduled)
204
+ end
144
205
  end
206
+
207
+ @scheduled.clear
208
+ end
209
+
210
+ # Track cancelled timers that are still retained in the heap.
211
+ def cancelled!(handle)
212
+ @cancelled += 1
145
213
  end
146
214
  end
147
215
  end
@@ -7,6 +7,6 @@
7
7
  class IO
8
8
  # @namespace
9
9
  module Event
10
- VERSION = "1.16.0"
10
+ VERSION = "1.16.2"
11
11
  end
12
12
  end
data/readme.md CHANGED
@@ -18,6 +18,14 @@ Please see the [project documentation](https://socketry.github.io/io-event/) for
18
18
 
19
19
  Please see the [project releases](https://socketry.github.io/io-event/releases/index) for all releases.
20
20
 
21
+ ### v1.16.2
22
+
23
+ - Improve timer heap performance by batching scheduled timer insertion, compacting cancelled timers during flush, and avoiding unnecessary heap rebuilds for small incremental inserts.
24
+
25
+ ### v1.16.1
26
+
27
+ - Ensure the pure Ruby `Select` selector returns `false`, not `nil`, when `io_wait` resumes without any ready events.
28
+
21
29
  ### v1.16.0
22
30
 
23
31
  - Use `eventfd` for `URing` cross-thread wakeup, and enable `IORING_SETUP_SINGLE_ISSUER`, `IORING_SETUP_DEFER_TASKRUN`, and `IORING_SETUP_TASKRUN_FLAG`. The waking thread now signals via `eventfd` rather than submitting a `NOP` SQE, which unlocks the single-issuer optimisation, defers task work to the application thread, and lets `select()` skip the `io_uring_get_events()` syscall when no task work is pending.
@@ -56,14 +64,6 @@ Please see the [project releases](https://socketry.github.io/io-event/releases/i
56
64
 
57
65
  - Fix `read_nonblock` when using the `URing` selector, which was not handling zero-length reads correctly. This allows reading available data without blocking.
58
66
 
59
- ### v1.11.0
60
-
61
- - [Introduce `IO::Event::WorkerPool` for off-loading blocking operations.](https://socketry.github.io/io-event/releases/index#introduce-io::event::workerpool-for-off-loading-blocking-operations.)
62
-
63
- ### v1.10.2
64
-
65
- - Improved consistency of handling closed IO when invoking `#select`.
66
-
67
67
  ## Contributing
68
68
 
69
69
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Releases
2
2
 
3
+ ## v1.16.2
4
+
5
+ - Improve timer heap performance by batching scheduled timer insertion, compacting cancelled timers during flush, and avoiding unnecessary heap rebuilds for small incremental inserts.
6
+
7
+ ## v1.16.1
8
+
9
+ - Ensure the pure Ruby `Select` selector returns `false`, not `nil`, when `io_wait` resumes without any ready events.
10
+
3
11
  ## v1.16.0
4
12
 
5
13
  - Use `eventfd` for `URing` cross-thread wakeup, and enable `IORING_SETUP_SINGLE_ISSUER`, `IORING_SETUP_DEFER_TASKRUN`, and `IORING_SETUP_TASKRUN_FLAG`. The waking thread now signals via `eventfd` rather than submitting a `NOP` SQE, which unlocks the single-issuer optimisation, defers task work to the application thread, and lets `select()` skip the `io_uring_get_events()` syscall when no task work is pending.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io-event
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.16.0
4
+ version: 1.16.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -123,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
125
  requirements: []
126
- rubygems_version: 4.0.6
126
+ rubygems_version: 4.0.10
127
127
  specification_version: 4
128
128
  summary: An event loop.
129
129
  test_files: []
metadata.gz.sig CHANGED
Binary file