Stroika Library 3.0d25
 
Loading...
Searching...
No Matches
IntervalTimer.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
6#include <condition_variable>
7#include <mutex>
8#include <random>
9
12#include "Stroika/Foundation/Containers/Collection.h"
13#include "Stroika/Foundation/Debug/Main.h"
20
21#include "IntervalTimer.h"
22
23using namespace Stroika::Foundation;
26using namespace Stroika::Foundation::Execution;
27using namespace Stroika::Foundation::Time;
28
29using Memory::MakeSharedPtr;
30
31namespace {
32 thread_local const void* tRunnerOf_{}; // on a timer thread, the manager (DefaultRep::Rep_) it runs timers for
33}
34
35/*
36 ********************************************************************************
37 *********************** IntervalTimer::RegisteredTask **************************
38 ********************************************************************************
39 */
41{
43 sb << "{"sv;
44 sb << "id: "sv << fID;
45 sb << ", callNextAt: "sv << fCallNextAt;
46 sb << ", frequency: "sv << fFrequency;
47 sb << ", hysteresis: "sv << fHysteresis;
48 sb << "}"sv;
49 return sb;
50}
51
52/*
53 ********************************************************************************
54 ******************** IntervalTimer::Manager::DefaultRep ************************
55 ********************************************************************************
56 */
58 Rep_ () = default;
59 virtual ~Rep_ () = default;
60 void AddOneShot (TimerID timer, const TimerCallback& intervalTimer, const Time::Duration& when)
61 {
62 Debug::TraceContextBumper ctx{"IntervalTimer::Manager: default implementation: AddOneShot"};
63 fData_.rwget ()->Add (RegisteredTask{timer, intervalTimer, Time::GetTickCount () + when});
64 DataChanged_ ();
65 }
66 void AddRepeating (TimerID timer, const TimerCallback& intervalTimer, const Time::Duration& repeatInterval, const optional<Time::Duration>& hysteresis)
67 {
68 Debug::TraceContextBumper ctx{"IntervalTimer::Manager: default implementation: AddRepeating"};
69 fData_.rwget ()->Add ({timer, intervalTimer, Time::GetTickCount () + repeatInterval, repeatInterval, hysteresis});
70 DataChanged_ ();
71 }
72 bool Remove (TimerID timer) noexcept
73 {
74 Debug::TraceContextBumper ctx{"IntervalTimer::Manager: default implementation: Remove"};
75 bool wasThere;
76 {
77 unique_lock runningLock{fRunningMutex_};
78 // not there: a one-shot already called (the timer thread removes it once its call ends), or removed already
79 wasThere = fData_.rwget ()->RemoveIf (timer);
80 // if it is running now, wait for that call to finish: once removed, it is not running, and not called again. But not
81 // on the timer thread - the callback removing itself - which would wait for itself, forever
82 if (tRunnerOf_ != this) {
83 fRunningChanged_.wait (runningLock, [&] () { return fRunning_ != timer; });
84 }
85 }
86 DataChanged_ ();
87 return wasThere;
88 }
89 RegisteredTaskCollection GetAllRegisteredTasks () const
90 {
91 Debug::TraceContextBumper ctx{"IntervalTimer::Manager: default implementation: GetAllRegisteredTasks"};
92 return fData_.load ();
93 }
94
95 // the timer whose callback the timer thread is running now, if any - so Remove can wait for it to finish. A
96 // std::condition_variable, not a Stroika ConditionVariable: Remove is noexcept, so its wait must not be interruptible
97 mutex fRunningMutex_; // taken before fData_'s
98 condition_variable fRunningChanged_;
99 optional<TimerID> fRunning_;
100
101 // @todo - re-implement using priority q, with next time at top of q
103 WaitableEvent fDataChanged_{};
104 Synchronized<shared_ptr<Thread::CleanupPtr>> fThread_{}; // last, so destroyed first: the thread is stopped before what it uses goes
105
106 // this is where a priorityq would be better
107 TimePointSeconds GetNextWakeupTime_ ()
108 {
109 return fData_.cget ()->Map<Iterable<TimePointSeconds>> ([] (const RegisteredTask& i) { return i.fCallNextAt; }).MinValue (TimePointSeconds{kInfinity});
110 }
111 void RunnerLoop_ ()
112 {
113 tRunnerOf_ = this; // so a callback removing a timer is known to be on this thread
114 // keep checking for timer events to run
115 random_device rd;
116 mt19937 gen{rd ()};
117 while (true) {
118 Require (Debug::AppearsDuringMainLifetime ());
119 fDataChanged_.WaitUntilQuietly (GetNextWakeupTime_ ());
120 fDataChanged_.Reset (); // SUBTLE why this is not a race. Yes, another thread could post new data, but we haven't yet looked through the data so OK; and only one reader (us)
121 // now process any timer events that are ready (could easily be more than one).
122 // if we had a priority q, we would do them in order, but for now, just do all that are ready
123 // NOTE - to avoid holding a lock (in case these guys remove themselves or whatever) - run them from a copy of the list;
124 // so each is looked up again before it is run, and again after - it may have been removed meanwhile
125 TimePointSeconds now = Time::GetTickCount ();
126 Collection<RegisteredTask> elts2Run = fData_.cget ()->Where ([=] (const RegisteredTask& i) { return i.fCallNextAt <= now; });
127 // note - this could EASILY be empty, for example, if fDataChanged_ wakes too early due to a change/Signal/Set
128 for (const RegisteredTask& i : elts2Run) {
129 {
130 lock_guard runningLock{fRunningMutex_};
131 if (not fData_.cget ()->Contains (i.fID)) {
132 continue; // removed since the copy was taken
133 }
134 fRunning_ = i.fID;
135 }
136 [[maybe_unused]] auto&& doneRunning = Finally ([this] () noexcept {
137 {
138 lock_guard runningLock{fRunningMutex_};
139 fRunning_ = nullopt;
140 }
141 fRunningChanged_.notify_all (); // for a Remove waiting for it to finish
142 });
143 IgnoreExceptionsExceptThreadAbortForCall (i.fCallback ());
144 // set its next time - from when it finished - unless it was removed while running: adding it then would bring it back
145 auto rwDataLock = fData_.rwget ();
146 if (optional<RegisteredTask> current = rwDataLock->Lookup (i.fID)) {
147 if (current->fFrequency.has_value ()) {
148 current->fCallNextAt = Time::GetTickCount () + *current->fFrequency;
149 if (current->fHysteresis) {
150 uniform_real_distribution<> dis{-current->fHysteresis->count (), current->fHysteresis->count ()};
151 current->fCallNextAt += Time::DurationSeconds{dis (gen)}; // can use fCallNextAt to be called immediately again... or even be < now
152 }
153 rwDataLock->Add (*current); // replaces it
154 }
155 else {
156 rwDataLock->Remove (i.fID); // a one-shot: done
157 }
158 }
159 }
160 }
161 }
162 // The timer thread starts with the first timer, and stays - idle while there are none - until this goes. Stopping it as the
163 // last timer went (before v3.0d25) waited for it holding fThread_'s lock: a deadlock, with a callback adding or removing a
164 // timer just then, which needs that lock too.
165 void DataChanged_ ()
166 {
167 auto lk = fThread_.rwget ();
168 if (lk.cref () == nullptr) {
169 if (not fData_.cget ()->empty ()) {
170 using namespace Execution;
171 lk.store (MakeSharedPtr<Thread::CleanupPtr> (Thread::CleanupPtr::eAbortBeforeWaiting,
172 Thread::New ([this] () { RunnerLoop_ (); }, Thread::eAutoStart, "Default-Interval-Timer"sv)));
173 }
174 }
175 else {
176 fDataChanged_.Set (); // it may be sleeping too long - or with no timer to wait for - so wake it up
177 }
178 }
179};
180
181IntervalTimer::Manager::DefaultRep::DefaultRep ()
182 : fHiddenRep_{MakeSharedPtr<Rep_> ()}
183{
184}
185
186void IntervalTimer::Manager::DefaultRep::AddOneShot (TimerID timer, const TimerCallback& intervalTimer, const Time::Duration& when)
187{
188 AssertNotNull (fHiddenRep_);
189 fHiddenRep_->AddOneShot (timer, intervalTimer, when);
190}
191
192void IntervalTimer::Manager::DefaultRep::AddRepeating (TimerID timer, const TimerCallback& intervalTimer,
193 const Time::Duration& repeatInterval, const optional<Time::Duration>& hysteresis)
194{
195 AssertNotNull (fHiddenRep_);
196 fHiddenRep_->AddRepeating (timer, intervalTimer, repeatInterval, hysteresis);
197}
198
200{
201 AssertNotNull (fHiddenRep_);
202 return fHiddenRep_->Remove (timer);
203}
204
205auto IntervalTimer::Manager::DefaultRep::GetAllRegisteredTasks () const -> RegisteredTaskCollection
206{
207 AssertNotNull (fHiddenRep_);
208 return fHiddenRep_->GetAllRegisteredTasks ();
209}
210
211/*
212 ********************************************************************************
213 ********************* IntervalTimer::Manager::Activator ************************
214 ********************************************************************************
215 */
216IntervalTimer::Manager::Activator::Activator ()
217{
218 Debug::TraceContextBumper ctx{"IntervalTimer::Manager::Activator::Activator"};
219 Require (Manager::sThe.fRep_ == nullptr); // only one activator object allowed
220 Require (Debug::AppearsDuringMainLifetime ());
221 Manager::sThe = Manager{MakeSharedPtr<IntervalTimer::Manager::DefaultRep> ()};
222}
223
224IntervalTimer::Manager::Activator::~Activator ()
225{
226 Debug::TraceContextBumper ctx{"IntervalTimer::Manager::Activator::~Activator"};
227 RequireNotNull (Manager::sThe.fRep_); // this is the only way to remove, and so must not be null here
228 Require (Debug::AppearsDuringMainLifetime ());
229 Manager::sThe.fRep_.reset ();
230}
231
232/*
233 ********************************************************************************
234 ***************************** IntervalTimer::Adder *****************************
235 ********************************************************************************
236 */
237IntervalTimer::Adder::Adder (IntervalTimer::Manager& manager, const TimerCallback& f, const Time::Duration& repeatInterval,
238 RunImmediatelyFlag runImmediately, const optional<Time::Duration>& hysteresis)
239 : fManager_{&manager}
240 , fFunction_{f}
241 , fTimer_{manager.AddRepeating (f, repeatInterval, hysteresis)}
242{
243 if (runImmediately == RunImmediatelyFlag::eRunImmediately) {
244 IgnoreExceptionsExceptThreadAbortForCall (fFunction_ ());
245 }
246}
#define AssertNotNull(p)
Definition Assertions.h:336
#define RequireNotNull(p)
Definition Assertions.h:350
auto MakeSharedPtr(ARGS_TYPE &&... args) -> shared_ptr< T >
same as make_shared, but if type T has block allocation, then use block allocation for the 'shared pa...
time_point< RealtimeClock, DurationSeconds > TimePointSeconds
TimePointSeconds is a simpler approach to chrono::time_point, which doesn't require using templates e...
Definition Realtime.h:82
constexpr DurationSeconds kInfinity
Definition Realtime.h:106
chrono::duration< double > DurationSeconds
chrono::duration<double> - a time span (length of time) measured in seconds, but high precision.
Definition Realtime.h:57
Similar to String, but intended to more efficiently construct a String. Mutable type (String is large...
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
A Collection<T> is a container to manage an un-ordered collection of items, without equality defined ...
a cross between Mapping<KEY, T> and Collection<T> and Set<T>
virtual bool Remove(TimerID timer) noexcept override
As Manager::Remove says: once it returns, the timer's callback is not running, and is not called agai...
Common::UniqueID< IntervalTimer > TimerID
Names a timer Manager::AddOneShot or AddRepeating added - for Remove. No two the same in a process,...
Wrap any object with Synchronized<> and it can be used similarly to the base type,...
nonvirtual WritableReference rwget()
get a read-write smart pointer to the underlying Synchronized<> object, holding the full lock the who...
nonvirtual ReadableReference cget() const
get a read-only smart pointer to the underlying Synchronized<> object, holding the readlock the whole...
Duration is a chrono::duration<double> (=.
Definition Duration.h:96
Iterable<T> is a base class for containers which easily produce an Iterator<T> to traverse them.
Definition Iterable.h:234
Ptr New(const function< void()> &fun2CallOnce, const optional< Characters::String > &name, const optional< Configuration > &configuration)
Definition Thread.cpp:887
auto Finally(FUNCTION &&f) -> Private_::FinallySentry< FUNCTION >
Definition Finally.inl:31