timers 4.1.2 → 4.3.5

Sign up to get free protection for your applications and to get access to all the features.
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