io-event 1.16.1 → 1.16.3

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: dd552a5bb53e9a2e9169c55a7f789e84f761a2707c83edd5955512818ad55e25
4
- data.tar.gz: 97a35fef870d94f7c596d612fdd6664c2e65cff9b14a8cf6b9035b12584f0833
3
+ metadata.gz: 7823ee584acc17c23f0f464a60271449a4298733efa62623653420b9181a5201
4
+ data.tar.gz: c71e15d6f35113a68bf31442621d2a4b9f8e8ca8d9b0d27dae234c30005f4ae0
5
5
  SHA512:
6
- metadata.gz: e187b07cc91e2ecf7fb641a61f481fbb9ba42a1a52c14532fa659ede0a6723dde5ac63e35d7a584d549ca52a94462131f5755b9da7bce1b06abd99f1ea6b3f24
7
- data.tar.gz: '090f2023d28eac6b05386727f0497bbd02db76b6a3bf0b43f9e068e5e5d36edb543a16c94806fea736c319298b491b1d13e069c7f89bf41a89dd86171b183c96'
6
+ metadata.gz: 60764fbe8a01ce3a77d8ad796473934f41a92edf30772265d4f0abbaf89bb9b1b79e1776cd9071111e0ff2590fc9402892cbaea1e22c779f9f3fdfe5ca18b9e9
7
+ data.tar.gz: 452f83f2b2a7550b6aa01f1d5094b62171e2a6179d8760651c3275397be65fb653adf12c8db1c3470d5cdcdbacf88e48dd208f01b53e2f0ed50eac91d104c06e
checksums.yaml.gz.sig CHANGED
Binary file
data/ext/extconf.rb CHANGED
@@ -55,7 +55,7 @@ have_func("rb_io_descriptor")
55
55
  have_func("&rb_process_status_wait")
56
56
  have_func("rb_fiber_current")
57
57
  have_func("&rb_fiber_raise")
58
- have_func("epoll_pwait2") if enable_config("epoll_pwait2", true)
58
+ have_func("epoll_pwait2(0, 0, 0, 0, 0)", "sys/epoll.h") if enable_config("epoll_pwait2", true)
59
59
 
60
60
  have_header("ruby/io/buffer.h")
61
61
 
@@ -107,6 +107,8 @@ module IO::Event
107
107
 
108
108
  @selector.close
109
109
  @selector = nil
110
+
111
+ @log&.flush
110
112
  end
111
113
 
112
114
  # Transfer from the calling fiber to the selector.
@@ -20,6 +20,8 @@ module IO::Event
20
20
  @input.read_nonblock(1)
21
21
  end
22
22
  end
23
+ rescue IOError
24
+ # This is expected on shutdown.
23
25
  end
24
26
 
25
27
  @fiber.transfer
@@ -36,7 +38,6 @@ module IO::Event
36
38
  def close
37
39
  @input.close
38
40
  @output.close
39
- # @fiber.raise(::Interrupt)
40
41
  end
41
42
  end
42
43
 
@@ -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
@@ -52,15 +52,19 @@ module IO::Event
52
52
  @waiting = nil
53
53
  end
54
54
 
55
+ # An optional reference to a fiber which can be cleared before it is resumed.
55
56
  Optional = Struct.new(:fiber) do
57
+ # Transfer control to the fiber if it is still available.
56
58
  def transfer(*arguments)
57
59
  fiber&.transfer(*arguments)
58
60
  end
59
61
 
62
+ # @returns [Boolean | Nil] Whether the referenced fiber is still alive.
60
63
  def alive?
61
64
  fiber&.alive?
62
65
  end
63
66
 
67
+ # Clear the referenced fiber.
64
68
  def nullify
65
69
  self.fiber = nil
66
70
  end
@@ -111,7 +115,9 @@ module IO::Event
111
115
  !@ready.empty?
112
116
  end
113
117
 
118
+ # A linked list node used to track fibers waiting for IO events.
114
119
  Waiter = Struct.new(:fiber, :events, :tail) do
120
+ # @returns [Boolean | Nil] Whether the waiting fiber is still alive.
115
121
  def alive?
116
122
  self.fiber&.alive?
117
123
  end
@@ -138,10 +144,12 @@ module IO::Event
138
144
  tail&.dispatch(events, &reactivate)
139
145
  end
140
146
 
147
+ # Clear the waiting fiber so it will not be resumed.
141
148
  def invalidate
142
149
  self.fiber = nil
143
150
  end
144
151
 
152
+ # Iterate over each active waiting fiber and its requested events.
145
153
  def each(&block)
146
154
  if fiber = self.fiber
147
155
  yield fiber, self.events
@@ -3,12 +3,17 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2021-2026, by Samuel Williams.
5
5
 
6
+ require_relative "native"
6
7
  require_relative "selector/select"
7
8
  require_relative "debug/selector"
8
9
 
9
10
  module IO::Event
10
11
  # @namespace
11
12
  module Selector
13
+ selectors = [:URing, :EPoll, :KQueue, :Select]
14
+ BEST = const_get(selectors.find{|name| const_defined?(name)})
15
+ private_constant :BEST
16
+
12
17
  # The default selector implementation, which is chosen based on the environment and available implementations.
13
18
  #
14
19
  # @parameter env [Hash] The environment to read configuration from.
@@ -16,16 +21,8 @@ module IO::Event
16
21
  def self.default(env = ENV)
17
22
  if name = env["IO_EVENT_SELECTOR"]&.to_sym
18
23
  return const_get(name)
19
- end
20
-
21
- if self.const_defined?(:URing)
22
- URing
23
- elsif self.const_defined?(:EPoll)
24
- EPoll
25
- elsif self.const_defined?(:KQueue)
26
- KQueue
27
24
  else
28
- Select
25
+ BEST
29
26
  end
30
27
  end
31
28
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024-2025, by Samuel Williams.
4
+ # Copyright, 2024-2026, by Samuel Williams.
5
5
 
6
6
  require_relative "priority_heap"
7
7
 
@@ -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.1"
10
+ VERSION = "1.16.3"
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.3
22
+
23
+ - Handle `IOError` raised while shutting down the pure Ruby interrupt pipe, so `IO::Event::Interrupt#close` does not leak expected shutdown errors from the interrupt fiber.
24
+
25
+ ### v1.16.2
26
+
27
+ - Improve timer heap performance by batching scheduled timer insertion, compacting cancelled timers during flush, and avoiding unnecessary heap rebuilds for small incremental inserts.
28
+
21
29
  ### v1.16.1
22
30
 
23
31
  - Ensure the pure Ruby `Select` selector returns `false`, not `nil`, when `io_wait` resumes without any ready events.
@@ -56,14 +64,6 @@ Please see the [project releases](https://socketry.github.io/io-event/releases/i
56
64
 
57
65
  - Fix Windows build.
58
66
 
59
- ### v1.11.1
60
-
61
- - Fix `read_nonblock` when using the `URing` selector, which was not handling zero-length reads correctly. This allows reading available data without blocking.
62
-
63
- ### v1.11.0
64
-
65
- - [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.)
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.3
4
+
5
+ - Handle `IOError` raised while shutting down the pure Ruby interrupt pipe, so `IO::Event::Interrupt#close` does not leak expected shutdown errors from the interrupt fiber.
6
+
7
+ ## v1.16.2
8
+
9
+ - Improve timer heap performance by batching scheduled timer insertion, compacting cancelled timers during flush, and avoiding unnecessary heap rebuilds for small incremental inserts.
10
+
3
11
  ## v1.16.1
4
12
 
5
13
  - Ensure the pure Ruby `Select` selector returns `false`, not `nil`, when `io_wait` resumes without any ready events.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io-event
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.16.1
4
+ version: 1.16.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  - Math Ieu
9
9
  - Wander Hillen
10
10
  - Jean Boussier
11
+ - Tavian Barnes
11
12
  - Benoit Daloze
12
13
  - Bruno Sutic
13
14
  - Shizuo Fujita
14
- - Tavian Barnes
15
15
  - Alex Matchneer
16
16
  - Anthony Ross
17
17
  - Delton Ding
metadata.gz.sig CHANGED
Binary file