io-event 1.16.1 → 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: dd552a5bb53e9a2e9169c55a7f789e84f761a2707c83edd5955512818ad55e25
4
- data.tar.gz: 97a35fef870d94f7c596d612fdd6664c2e65cff9b14a8cf6b9035b12584f0833
3
+ metadata.gz: 8cc6abcf010ce881e4623a5736241660d1ebf6a84883767d761616ddcb23bf4e
4
+ data.tar.gz: 4a438756e87e8feaefaafb40014f36c2b240fbbd837d4581c657eb310cb9b95a
5
5
  SHA512:
6
- metadata.gz: e187b07cc91e2ecf7fb641a61f481fbb9ba42a1a52c14532fa659ede0a6723dde5ac63e35d7a584d549ca52a94462131f5755b9da7bce1b06abd99f1ea6b3f24
7
- data.tar.gz: '090f2023d28eac6b05386727f0497bbd02db76b6a3bf0b43f9e068e5e5d36edb543a16c94806fea736c319298b491b1d13e069c7f89bf41a89dd86171b183c96'
6
+ metadata.gz: b910e009ff697f179290d916e5914621d25a5478e0f09ac806563a0c3808b30fc28ca652042608e60625830468f37088943559c590112dfadb0d3a48d3b54688
7
+ data.tar.gz: c7501e4ef87e5c9744d666e43494b60b2b3c490d9e9cb55b75a87f7de199e1421b49ba0ce5e176cbaf230d6f3b8f838a5745cff81bac6f826328e7665de7a9e9
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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
@@ -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.2"
11
11
  end
12
12
  end
data/readme.md CHANGED
@@ -18,6 +18,10 @@ 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
+
21
25
  ### v1.16.1
22
26
 
23
27
  - Ensure the pure Ruby `Select` selector returns `false`, not `nil`, when `io_wait` resumes without any ready events.
@@ -60,10 +64,6 @@ Please see the [project releases](https://socketry.github.io/io-event/releases/i
60
64
 
61
65
  - Fix `read_nonblock` when using the `URing` selector, which was not handling zero-length reads correctly. This allows reading available data without blocking.
62
66
 
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,9 @@
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
+
3
7
  ## v1.16.1
4
8
 
5
9
  - 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,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.1
4
+ version: 1.16.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file