1//============================================================================
2//
3// SSSS tt lll lll
4// SS SS tt ll ll
5// SS tttttt eeee ll ll aaaa
6// SSSS tt ee ee ll ll aa
7// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
8// SS SS tt ee ll ll aa aa
9// SSSS ttt eeeee llll llll aaaaa
10//
11// Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony
12// and the Stella Team
13//
14// See the file "License.txt" for information on usage and redistribution of
15// this file, and for a DISCLAIMER OF ALL WARRANTIES.
16//============================================================================
17
18#ifndef TIMER_MANAGER_HXX
19#define TIMER_MANAGER_HXX
20
21#include <algorithm>
22#include <functional>
23#include <chrono>
24#include <unordered_map>
25#include <set>
26#include <cstdint>
27#include <thread>
28#include <mutex>
29#include <condition_variable>
30
31#include "bspf.hxx"
32
33/**
34 This class provides a portable periodic/one-shot timer infrastructure
35 using worker threads and generic C++11 code.
36
37 @author Doug Gale (doug65536)
38 From "Code Review"
39 https://codereview.stackexchange.com/questions/127552/portable-periodic-one-shot-timer-thread-follow-up
40
41 Modifications and cleanup for Stella by Stephen Anthony
42*/
43class TimerManager
44{
45 public:
46 // Each Timer is assigned a unique ID of type TimerId
47 using TimerId = uInt64;
48
49 // Function object we actually use
50 using TFunction = std::function<void()>;
51
52 // Values that are a large-range millisecond count
53 using millisec = uInt64;
54
55 // Constructor does not start worker until there is a Timer.
56 explicit TimerManager();
57
58 // Destructor is thread safe, even if a timer callback is running.
59 // All callbacks are guaranteed to have returned before this
60 // destructor returns.
61 ~TimerManager();
62
63 /**
64 Create a new timer using milliseconds, and add it to the internal queue.
65
66 @param msDelay Callback starts firing this many milliseconds from now
67 @param msPeriod If non-zero, callback is fired again after this period
68 @param func The callback to run at the specified interval
69
70 @return Id used to identify the timer for later use
71 */
72 TimerId addTimer(millisec msDelay, millisec msPeriod, const TFunction& func);
73
74 /**
75 Convenience function; setInterval API like browser javascript.
76
77 Call function every 'period' ms, starting 'period' ms from now.
78 */
79 TimerId setInterval(const TFunction& func, millisec period) {
80 return addTimer(period, period, std::move(func));
81 }
82
83 /**
84 Convenience function; setTimeout API like browser javascript.
85
86 Call function once 'timeout' ms from now.
87 */
88 TimerId setTimeout(const TFunction& func, millisec timeout) {
89 return addTimer(timeout, 0, std::move(func));
90 }
91
92 /**
93 Destroy the specified timer.
94
95 Synchronizes with the worker thread if the callback for this timer
96 is running, which guarantees that the handler for that callback is
97 not running before clear() returns.
98
99 You are not required to clear any timers. You can forget their
100 TimerId if you do not need to cancel them.
101
102 The only time you need this is when you want to stop a timer that
103 has a repetition period, or you want to cancel a timeout that has
104 not fired yet.
105 */
106 bool clear(TimerId id);
107
108 /**
109 Destroy all timers, but preserve id uniqueness.
110 This carefully makes sure every timer is not executing its callback
111 before destructing it.
112 */
113 void clear();
114
115 // Peek at current state
116 std::size_t size() const noexcept;
117 bool empty() const noexcept;
118
119 // Returns lazily initialized singleton
120 static TimerManager& global();
121
122 /**
123 This method returns number of ticks in microseconds since some
124 pre-defined time in the past. *NOTE*: it is necessary that this
125 pre-defined time exists between runs of the application, and must
126 be (relatively) unique. For example, the time since the system
127 started running is not a good choice, since it can be duplicated.
128 The current implementation uses time since the UNIX epoch.
129
130 @return Current time in microseconds.
131 */
132 static uInt64 getTicks() {
133 using namespace std::chrono;
134 return duration_cast<duration<uInt64, std::ratio<1, 1000000> > >
135 (system_clock::now().time_since_epoch()).count();
136 }
137
138 private:
139 using Lock = std::mutex;
140 using ScopedLock = std::unique_lock<Lock>;
141 using ConditionVar = std::condition_variable;
142
143 using Clock = std::chrono::steady_clock;
144 using Timestamp = std::chrono::time_point<Clock>;
145 using Duration = std::chrono::milliseconds;
146
147 struct Timer
148 {
149 explicit Timer(TimerId id = 0);
150 Timer(Timer&& r) noexcept;
151 Timer& operator=(Timer&& r) noexcept;
152
153 Timer(TimerId id, Timestamp next, Duration period, const TFunction& func) noexcept;
154
155 // Never called
156 Timer(Timer const& r) = delete;
157 Timer& operator=(Timer const& r) = delete;
158
159 TimerId id;
160 Timestamp next;
161 Duration period;
162 TFunction handler;
163
164 // You must be holding the 'sync' lock to assign waitCond
165 std::unique_ptr<ConditionVar> waitCond;
166
167 bool running;
168 };
169
170 // Comparison functor to sort the timer "queue" by Timer::next
171 struct NextActiveComparator
172 {
173 bool operator()(Timer const& a, Timer const& b) const noexcept
174 {
175 return a.next < b.next;
176 }
177 };
178
179 // Queue is a set of references to Timer objects, sorted by next
180 using QueueValue = std::reference_wrapper<Timer>;
181 using Queue = std::multiset<QueueValue, NextActiveComparator>;
182 using TimerMap = std::unordered_map<TimerId, Timer>;
183
184 void timerThreadWorker();
185 bool destroy_impl(ScopedLock& lock, TimerMap::iterator i, bool notify);
186
187 // Inexhaustible source of unique IDs
188 TimerId nextId;
189
190 // The Timer objects are physically stored in this map
191 TimerMap active;
192
193 // The ordering queue holds references to items in 'active'
194 Queue queue;
195
196 // One worker thread for an unlimited number of timers is acceptable
197 // Lazily started when first timer is started
198 // TODO: Implement auto-stopping the timer thread when it is idle for
199 // a configurable period.
200 mutable Lock sync;
201 ConditionVar wakeUp;
202 std::thread worker;
203 bool done;
204
205 // Valid IDs are guaranteed not to be this value
206 static TimerId constexpr no_timer = TimerId(0);
207};
208
209#endif // TIMERTHREAD_H
210