timers 4.1.2 → 4.3.5

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
- SHA1:
3
- metadata.gz: 97c81b8ed305099faee9ecec3ee32f6960b70d54
4
- data.tar.gz: 90d94a796bfe60e78963b0053648396aefdb19b6
2
+ SHA256:
3
+ metadata.gz: e7c61c9db77955d85c69ca83499add3629c71628af3a825d78978b2383182b4a
4
+ data.tar.gz: 27f570753d83d4b4a98ee6ede089908d571347df8ba411edb56fe4ae965e703c
5
5
  SHA512:
6
- metadata.gz: dbea5311bcd5fb7bfe7c98191ac15146647b91ab5c994309f3bc14fe394ac97832361afa6ff7f257d844a802265125a5bafa051026fa1eed979f8310d8a21f87
7
- data.tar.gz: 2097c9e9d6e838550fa3c4735db828173852dd5e598670a376a04de132274df6464687cfe0e72992c2469d5d355932808fe1147e44ec5f26e2162046267c37ad
6
+ metadata.gz: a7bac196cc0e3e1a784f7ba961c4a09539c875f80ac366da0b0d091d9f6ad1a505fb0a86a0c9d61d44a3495342029d003749b3ac072904b37f667d8e6e90b356
7
+ data.tar.gz: 4fa05113f72aef3abe63804a097e7fd9322c5b84a2a9ad14d590906e3ca59754857378a085388c52f9eb7ef1beb934b6b75789c3c087eee0b900f3446ace7207
checksums.yaml.gz.sig ADDED
Binary file
data/lib/timers/events.rb CHANGED
@@ -1,111 +1,111 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
- require "hitimes"
3
+ # Released under the MIT License.
4
+ # Copyright, 2014-2022, by Samuel Williams.
5
+ # Copyright, 2014-2016, by Tony Arcieri.
6
+ # Copyright, 2014, by Lavir the Whiolet.
7
+ # Copyright, 2015, by Utenmiki.
8
+ # Copyright, 2015, by Donovan Keme.
9
+ # Copyright, 2021, by Wander Hillen.
5
10
 
6
- require "timers/timer"
11
+ require_relative "timer"
12
+ require_relative "priority_heap"
7
13
 
8
14
  module Timers
9
- # Maintains an ordered list of events, which can be cancelled.
10
- class Events
11
- # Represents a cancellable handle for a specific timer event.
12
- class Handle
13
- def initialize(time, callback)
14
- @time = time
15
- @callback = callback
16
- end
17
-
18
- # The absolute time that the handle should be fired at.
19
- attr_reader :time
20
-
21
- # Cancel this timer, O(1).
22
- def cancel!
23
- # The simplest way to keep track of cancelled status is to nullify the
24
- # callback. This should also be optimal for garbage collection.
25
- @callback = nil
26
- end
27
-
28
- # Has this timer been cancelled? Cancelled timer's don't fire.
29
- def cancelled?
30
- @callback.nil?
31
- end
32
-
33
- def >(other)
34
- @time > other.to_f
35
- end
36
-
37
- def to_f
38
- @time
39
- end
40
-
41
- # Fire the callback if not cancelled with the given time parameter.
42
- def fire(time)
43
- @callback.call(time) if @callback
44
- end
45
- end
46
-
47
- def initialize
48
- # A sequence of handles, maintained in sorted order, future to present.
49
- # @sequence.last is the next event to be fired.
50
- @sequence = []
51
- end
52
-
53
- # Add an event at the given time.
54
- def schedule(time, callback)
55
- handle = Handle.new(time.to_f, callback)
56
-
57
- index = bisect_left(@sequence, handle)
58
-
59
- # Maintain sorted order, O(logN) insertion time.
60
- @sequence.insert(index, handle)
61
-
62
- handle
63
- end
64
-
65
- # Returns the first non-cancelled handle.
66
- def first
67
- while (handle = @sequence.last)
68
- return handle unless handle.cancelled?
69
- @sequence.pop
70
- end
71
- end
72
-
73
- # Returns the number of pending (possibly cancelled) events.
74
- def size
75
- @sequence.size
76
- end
77
-
78
- # Fire all handles for which Handle#time is less than the given time.
79
- def fire(time)
80
- pop(time).reverse_each do |handle|
81
- handle.fire(time)
82
- end
83
- end
84
-
85
- private
86
-
87
- # Efficiently take k handles for which Handle#time is less than the given
88
- # time.
89
- def pop(time)
90
- index = bisect_left(@sequence, time)
91
-
92
- @sequence.pop(@sequence.size - index)
93
- end
94
-
95
- # Return the left-most index where to insert item e, in a list a, assuming
96
- # a is sorted in descending order.
97
- def bisect_left(a, e, l = 0, u = a.length)
98
- while l < u
99
- m = l + (u - l).div(2)
100
-
101
- if a[m] > e
102
- l = m + 1
103
- else
104
- u = m
105
- end
106
- end
107
-
108
- l
109
- end
110
- end
15
+ # Maintains a PriorityHeap of events ordered on time, which can be cancelled.
16
+ class Events
17
+ # Represents a cancellable handle for a specific timer event.
18
+ class Handle
19
+ include Comparable
20
+
21
+ def initialize(time, callback)
22
+ @time = time
23
+ @callback = callback
24
+ end
25
+
26
+ # The absolute time that the handle should be fired at.
27
+ attr_reader :time
28
+
29
+ # Cancel this timer, O(1).
30
+ def cancel!
31
+ # The simplest way to keep track of cancelled status is to nullify the
32
+ # callback. This should also be optimal for garbage collection.
33
+ @callback = nil
34
+ end
35
+
36
+ # Has this timer been cancelled? Cancelled timer's don't fire.
37
+ def cancelled?
38
+ @callback.nil?
39
+ end
40
+
41
+ def <=> other
42
+ @time <=> other.time
43
+ end
44
+
45
+ # Fire the callback if not cancelled with the given time parameter.
46
+ def fire(time)
47
+ @callback.call(time) if @callback
48
+ end
49
+ end
50
+
51
+ def initialize
52
+ # A sequence of handles, maintained in sorted order, future to present.
53
+ # @sequence.last is the next event to be fired.
54
+ @sequence = PriorityHeap.new
55
+ @queue = []
56
+ end
57
+
58
+ # Add an event at the given time.
59
+ def schedule(time, callback)
60
+ flush!
61
+
62
+ handle = Handle.new(time.to_f, callback)
63
+
64
+ @queue << handle
65
+
66
+ return handle
67
+ end
68
+
69
+ # Returns the first non-cancelled handle.
70
+ def first
71
+ merge!
72
+
73
+ while (handle = @sequence.peek)
74
+ return handle unless handle.cancelled?
75
+ @sequence.pop
76
+ end
77
+ end
78
+
79
+ # Returns the number of pending (possibly cancelled) events.
80
+ def size
81
+ @sequence.size + @queue.size
82
+ end
83
+
84
+ # Fire all handles for which Handle#time is less than the given time.
85
+ def fire(time)
86
+ merge!
87
+
88
+ while handle = @sequence.peek and handle.time <= time
89
+ @sequence.pop
90
+ handle.fire(time)
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ # Move all non-cancelled timers from the pending queue to the priority heap
97
+ def merge!
98
+ while handle = @queue.pop
99
+ next if handle.cancelled?
100
+
101
+ @sequence.push(handle)
102
+ end
103
+ end
104
+
105
+ def flush!
106
+ while @queue.last&.cancelled?
107
+ @queue.pop
108
+ end
109
+ end
110
+ end
111
111
  end
data/lib/timers/group.rb CHANGED
@@ -1,127 +1,133 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Released under the MIT License.
4
+ # Copyright, 2014-2022, by Samuel Williams.
5
+ # Copyright, 2014-2016, by Tony Arcieri.
6
+ # Copyright, 2015, by Donovan Keme.
7
+ # Copyright, 2015, by Tommy Ong Gia Phu.
8
+
3
9
  require "set"
4
10
  require "forwardable"
5
- require "hitimes"
6
11
 
7
- require "timers/timer"
8
- require "timers/events"
12
+ require_relative "interval"
13
+ require_relative "timer"
14
+ require_relative "events"
9
15
 
10
16
  module Timers
11
- # A collection of timers which may fire at different times
12
- class Group
13
- include Enumerable
14
-
15
- extend Forwardable
16
- def_delegators :@timers, :each, :empty?
17
-
18
- def initialize
19
- @events = Events.new
20
-
21
- @timers = Set.new
22
- @paused_timers = Set.new
23
-
24
- @interval = Hitimes::Interval.new
25
- @interval.start
26
- end
27
-
28
- # Scheduled events:
29
- attr_reader :events
30
-
31
- # Active timers:
32
- attr_reader :timers
33
-
34
- # Paused timers:
35
- attr_reader :paused_timers
36
-
37
- # Call the given block after the given interval. The first argument will be
38
- # the time at which the group was asked to fire timers for.
39
- def after(interval, &block)
40
- Timer.new(self, interval, false, &block)
41
- end
42
-
43
- # Call the given block immediately, and then after the given interval. The first
44
- # argument will be the time at which the group was asked to fire timers for.
45
- def now_and_after(interval, &block)
46
- yield
47
- after(interval, &block)
48
- end
49
-
50
- # Call the given block periodically at the given interval. The first
51
- # argument will be the time at which the group was asked to fire timers for.
52
- def every(interval, recur = true, &block)
53
- Timer.new(self, interval, recur, &block)
54
- end
55
-
56
- # Call the given block immediately, and then periodically at the given interval. The first
57
- # argument will be the time at which the group was asked to fire timers for.
58
- def now_and_every(interval, recur = true, &block)
59
- yield
60
- every(interval, recur, &block)
61
- end
62
-
63
- # Wait for the next timer and fire it. Can take a block, which should behave
64
- # like sleep(n), except that n may be nil (sleep forever) or a negative
65
- # number (fire immediately after return).
66
- def wait
67
- if block_given?
68
- yield wait_interval
69
-
70
- while (interval = wait_interval) && interval > 0
71
- yield interval
72
- end
73
- else
74
- while (interval = wait_interval) && interval > 0
75
- # We cannot assume that sleep will wait for the specified time, it might be +/- a bit.
76
- sleep interval
77
- end
78
- end
79
-
80
- fire
81
- end
82
-
83
- # Interval to wait until when the next timer will fire.
84
- # - nil: no timers
85
- # - -ve: timers expired already
86
- # - 0: timers ready to fire
87
- # - +ve: timers waiting to fire
88
- def wait_interval(offset = current_offset)
89
- handle = @events.first
90
- handle.time - Float(offset) if handle
91
- end
92
-
93
- # Fire all timers that are ready.
94
- def fire(offset = current_offset)
95
- @events.fire(offset)
96
- end
97
-
98
- # Pause all timers.
99
- def pause
100
- @timers.dup.each(&:pause)
101
- end
102
-
103
- # Resume all timers.
104
- def resume
105
- @paused_timers.dup.each(&:resume)
106
- end
107
-
108
- alias continue resume
109
-
110
- # Delay all timers.
111
- def delay(seconds)
112
- @timers.each do |timer|
113
- timer.delay(seconds)
114
- end
115
- end
116
-
117
- # Cancel all timers.
118
- def cancel
119
- @timers.dup.each(&:cancel)
120
- end
121
-
122
- # The group's current time.
123
- def current_offset
124
- @interval.to_f
125
- end
126
- end
17
+ # A collection of timers which may fire at different times
18
+ class Group
19
+ include Enumerable
20
+
21
+ extend Forwardable
22
+ def_delegators :@timers, :each, :empty?
23
+
24
+ def initialize
25
+ @events = Events.new
26
+
27
+ @timers = Set.new
28
+ @paused_timers = Set.new
29
+
30
+ @interval = Interval.new
31
+ @interval.start
32
+ end
33
+
34
+ # Scheduled events:
35
+ attr_reader :events
36
+
37
+ # Active timers:
38
+ attr_reader :timers
39
+
40
+ # Paused timers:
41
+ attr_reader :paused_timers
42
+
43
+ # Call the given block after the given interval. The first argument will be
44
+ # the time at which the group was asked to fire timers for.
45
+ def after(interval, &block)
46
+ Timer.new(self, interval, false, &block)
47
+ end
48
+
49
+ # Call the given block immediately, and then after the given interval. The first
50
+ # argument will be the time at which the group was asked to fire timers for.
51
+ def now_and_after(interval, &block)
52
+ yield
53
+ after(interval, &block)
54
+ end
55
+
56
+ # Call the given block periodically at the given interval. The first
57
+ # argument will be the time at which the group was asked to fire timers for.
58
+ def every(interval, recur = true, &block)
59
+ Timer.new(self, interval, recur, &block)
60
+ end
61
+
62
+ # Call the given block immediately, and then periodically at the given interval. The first
63
+ # argument will be the time at which the group was asked to fire timers for.
64
+ def now_and_every(interval, recur = true, &block)
65
+ yield
66
+ every(interval, recur, &block)
67
+ end
68
+
69
+ # Wait for the next timer and fire it. Can take a block, which should behave
70
+ # like sleep(n), except that n may be nil (sleep forever) or a negative
71
+ # number (fire immediately after return).
72
+ def wait
73
+ if block_given?
74
+ yield wait_interval
75
+
76
+ while (interval = wait_interval) && interval > 0
77
+ yield interval
78
+ end
79
+ else
80
+ while (interval = wait_interval) && interval > 0
81
+ # We cannot assume that sleep will wait for the specified time, it might be +/- a bit.
82
+ sleep interval
83
+ end
84
+ end
85
+
86
+ fire
87
+ end
88
+
89
+ # Interval to wait until when the next timer will fire.
90
+ # - nil: no timers
91
+ # - -ve: timers expired already
92
+ # - 0: timers ready to fire
93
+ # - +ve: timers waiting to fire
94
+ def wait_interval(offset = current_offset)
95
+ handle = @events.first
96
+ handle.time - Float(offset) if handle
97
+ end
98
+
99
+ # Fire all timers that are ready.
100
+ def fire(offset = current_offset)
101
+ @events.fire(offset)
102
+ end
103
+
104
+ # Pause all timers.
105
+ def pause
106
+ @timers.dup.each(&:pause)
107
+ end
108
+
109
+ # Resume all timers.
110
+ def resume
111
+ @paused_timers.dup.each(&:resume)
112
+ end
113
+
114
+ alias continue resume
115
+
116
+ # Delay all timers.
117
+ def delay(seconds)
118
+ @timers.each do |timer|
119
+ timer.delay(seconds)
120
+ end
121
+ end
122
+
123
+ # Cancel all timers.
124
+ def cancel
125
+ @timers.dup.each(&:cancel)
126
+ end
127
+
128
+ # The group's current time.
129
+ def current_offset
130
+ @interval.to_f
131
+ end
132
+ end
127
133
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2018-2022, by Samuel Williams.
5
+
6
+ module Timers
7
+ # A collection of timers which may fire at different times
8
+ class Interval
9
+ # Get the current elapsed monotonic time.
10
+ def initialize
11
+ @total = 0.0
12
+ @current = nil
13
+ end
14
+
15
+ def start
16
+ return if @current
17
+
18
+ @current = now
19
+ end
20
+
21
+ def stop
22
+ return unless @current
23
+
24
+ @total += duration
25
+
26
+ @current = nil
27
+ end
28
+
29
+ def to_f
30
+ @total + duration
31
+ end
32
+
33
+ protected def duration
34
+ now - @current
35
+ end
36
+
37
+ protected def now
38
+ ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2021, by Wander Hillen.
5
+ # Copyright, 2021-2022, by Samuel Williams.
6
+
7
+ module Timers
8
+ # A priority queue implementation using a standard binary minheap. It uses straight comparison
9
+ # of its contents to determine priority. This works because a Handle from Timers::Events implements
10
+ # the '<' operation by comparing the expiry time.
11
+ # See <https://en.wikipedia.org/wiki/Binary_heap> for explanations of the main methods.
12
+ class PriorityHeap
13
+ def initialize
14
+ # The heap is represented with an array containing a binary tree. See
15
+ # https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation for how this array
16
+ # is built up.
17
+ @contents = []
18
+ end
19
+
20
+ # Returns the earliest timer or nil if the heap is empty.
21
+ def peek
22
+ @contents[0]
23
+ end
24
+
25
+ # Returns the number of elements in the heap
26
+ def size
27
+ @contents.size
28
+ end
29
+
30
+ # Returns the earliest timer if the heap is non-empty and removes it from the heap.
31
+ # Returns nil if the heap is empty. (and doesn't change the heap in that case)
32
+ def pop
33
+ # If the heap is empty:
34
+ if @contents.empty?
35
+ return nil
36
+ end
37
+
38
+ # If we have only one item, no swapping is required:
39
+ if @contents.size == 1
40
+ return @contents.pop
41
+ end
42
+
43
+ # Take the root of the tree:
44
+ value = @contents[0]
45
+
46
+ # Remove the last item in the tree:
47
+ last = @contents.pop
48
+
49
+ # Overwrite the root of the tree with the item:
50
+ @contents[0] = last
51
+
52
+ # Bubble it down into place:
53
+ bubble_down(0)
54
+
55
+ # validate!
56
+
57
+ return value
58
+ end
59
+
60
+ # Inserts a new timer into the heap, then rearranges elements until the heap invariant is true again.
61
+ def push(element)
62
+ # Insert the item at the end of the heap:
63
+ @contents.push(element)
64
+
65
+ # Bubble it up into position:
66
+ bubble_up(@contents.size - 1)
67
+
68
+ # validate!
69
+
70
+ return self
71
+ end
72
+
73
+ # Empties out the heap, discarding all elements
74
+ def clear!
75
+ @contents = []
76
+ end
77
+
78
+ # Validate the heap invariant. Every element except the root must not be smaller than
79
+ # its parent element. Note that it MAY be equal.
80
+ def valid?
81
+ # notice we skip index 0 on purpose, because it has no parent
82
+ (1..(@contents.size - 1)).all? { |e| @contents[e] >= @contents[(e - 1) / 2] }
83
+ end
84
+
85
+ private
86
+
87
+ def swap(i, j)
88
+ @contents[i], @contents[j] = @contents[j], @contents[i]
89
+ end
90
+
91
+ def bubble_up(index)
92
+ parent_index = (index - 1) / 2 # watch out, integer division!
93
+
94
+ while index > 0 && @contents[index] < @contents[parent_index]
95
+ # if the node has a smaller value than its parent, swap these nodes
96
+ # to uphold the minheap invariant and update the index of the 'current'
97
+ # node. If the node is already at index 0, we can also stop because that
98
+ # is the root of the heap.
99
+ # swap(index, parent_index)
100
+ @contents[index], @contents[parent_index] = @contents[parent_index], @contents[index]
101
+
102
+ index = parent_index
103
+ parent_index = (index - 1) / 2 # watch out, integer division!
104
+ end
105
+ end
106
+
107
+ def bubble_down(index)
108
+ swap_value = 0
109
+ swap_index = nil
110
+
111
+ while true
112
+ left_index = (2 * index) + 1
113
+ left_value = @contents[left_index]
114
+
115
+ if left_value.nil?
116
+ # This node has no children so it can't bubble down any further.
117
+ # We're done here!
118
+ return
119
+ end
120
+
121
+ # Determine which of the child nodes has the smallest value:
122
+ right_index = left_index + 1
123
+ right_value = @contents[right_index]
124
+
125
+ if right_value.nil? or right_value > left_value
126
+ swap_value = left_value
127
+ swap_index = left_index
128
+ else
129
+ swap_value = right_value
130
+ swap_index = right_index
131
+ end
132
+
133
+ if @contents[index] < swap_value
134
+ # No need to swap, the minheap invariant is already satisfied:
135
+ return
136
+ else
137
+ # At least one of the child node has a smaller value than the current node, swap current node with that child and update current node for if it might need to bubble down even further:
138
+ # swap(index, swap_index)
139
+ @contents[index], @contents[swap_index] = @contents[swap_index], @contents[index]
140
+
141
+ index = swap_index
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end