diff options
-rw-r--r-- | indra/llcommon/llcallbacklist.cpp | 266 | ||||
-rw-r--r-- | indra/llcommon/llcallbacklist.h | 94 | ||||
-rw-r--r-- | indra/llcommon/lleventfilter.cpp | 10 | ||||
-rw-r--r-- | indra/llcommon/lleventfilter.h | 4 | ||||
-rw-r--r-- | indra/llcommon/lleventtimer.cpp | 8 | ||||
-rw-r--r-- | indra/llcommon/lleventtimer.h | 2 |
6 files changed, 233 insertions, 151 deletions
diff --git a/indra/llcommon/llcallbacklist.cpp b/indra/llcommon/llcallbacklist.cpp index 992c83b4d2..52e9860e02 100644 --- a/indra/llcommon/llcallbacklist.cpp +++ b/indra/llcommon/llcallbacklist.cpp @@ -25,6 +25,8 @@ */ #include "llcallbacklist.h" +#include "llexception.h" +#include <vector> // // Member functions @@ -126,116 +128,83 @@ LLCallbackList::handle_t LLCallbackList::doOnIdleRepeating( const bool_func_t& f } /***************************************************************************** -* LLLater +* LL::Timers *****************************************************************************/ -LLLater::LLLater() {} - -LLLater::HandleMap::iterator LLLater::doAtTime1(LLDate::timestamp time) +namespace LL { - // Pick token FIRST to store a self-reference in mQueue's managed node as - // well as in mHandles. Pre-increment to distinguish 0 from any live - // handle_t. - token_t token{ ++mToken }; - // For the moment, store a default-constructed mQueue handle -- - // doAtTime2() will fill in. - auto [iter, inserted]{ mHandles.emplace( - token, - HandleMap::mapped_type{ queue_t::handle_type(), time }) }; - llassert(inserted); - return iter; -} -LLLater::handle_t LLLater::doAtTime2(nullary_func_t callable, HandleMap::iterator iter) -{ - bool first{ mQueue.empty() }; - // HandleMap::iterator references (token, (handle, time)) pair - auto handle{ mQueue.emplace(callable, iter->first, iter->second.second) }; - // Now that we have an mQueue handle_type, store it in mHandles entry. - iter->second.first = handle; - if (first && ! mLive.connected()) - { - // If this is our first entry, register for regular callbacks. - mLive = LLCallbackList::instance().doOnIdleRepeating([this]{ return tick(); }); - } - // Make an LLLater::handle_t from token. - return { iter->first }; -} +Timers::Timers() {} // Call a given callable once at specified timestamp. -LLLater::handle_t LLLater::doAtTime(nullary_func_t callable, LLDate::timestamp time) +Timers::handle_t Timers::scheduleAt(nullary_func_t callable, LLDate::timestamp time) { - return doAtTime2(callable, doAtTime1(time)); + // tick() assumes you want to run periodically until you return true. + // Schedule a task that returns true after a single call. + return scheduleAtRepeating(once(callable), time, 0); } // Call a given callable once after specified interval. -LLLater::handle_t LLLater::doAfterInterval(nullary_func_t callable, F32 seconds) +Timers::handle_t Timers::scheduleAfter(nullary_func_t callable, F32 seconds) { - // Passing 0 is a slightly more expensive way of calling - // LLCallbackList::doOnIdleOneTime(). Are we sure the caller is correct? - // (If there's a valid use case, remove the llassert() and carry on.) - llassert(seconds > 0); - return doAtTime(callable, LLDate::now().secondsSinceEpoch() + seconds); + return scheduleRepeating(once(callable), seconds); } -// For doPeriodically(), we need a struct rather than a lambda because a -// struct, unlike a lambda, has access to 'this'. -struct LLLater::Periodic +// Call a given callable every specified number of seconds, until it returns true. +Timers::handle_t Timers::scheduleRepeating(bool_func_t callable, F32 seconds) { - LLLater* mLater; - HandleMap::iterator mHandleEntry; - bool_func_t mCallable; - F32 mSeconds; + return scheduleAtRepeating(callable, now() + seconds, seconds); +} - void operator()() +Timers::handle_t Timers::scheduleAtRepeating(bool_func_t callable, + LLDate::timestamp time, F32 interval) +{ + // Pick token FIRST to store a self-reference in mQueue's managed node as + // well as in mMeta. Pre-increment to distinguish 0 from any live + // handle_t. + token_t token{ ++mToken }; + // For the moment, store a default-constructed mQueue handle -- + // we'll fill in later. + auto [iter, inserted] = mMeta.emplace(token, + Metadata{ queue_t::handle_type(), time, interval }); + // It's important that our token is unique. + llassert(inserted); + + // Remember whether this is the first entry in mQueue + bool first{ mQueue.empty() }; + auto handle{ mQueue.emplace(callable, token, time) }; + // Now that we have an mQueue handle_type, store it in mMeta entry. + iter->second.mHandle = handle; + if (first && ! mLive.connected()) { - if (! mCallable()) - { - // Returning false means please schedule another call. - // Don't call doAfterInterval(), which rereads LLDate::now(), - // since that would defer by however long it took us to wake - // up and notice plus however long callable() took to run. - // Bump the time in our mHandles entry so getRemaining() can see. - // HandleMap::iterator references (token, (handle, time)) pair. - mHandleEntry->second.second += mSeconds; - mLater->doAtTime2(*this, mHandleEntry); - } + // If this is our first entry, register for regular callbacks. + mLive = LLCallbackList::instance().doOnIdleRepeating([this]{ return tick(); }); } -}; - -// Call a given callable every specified number of seconds, until it returns true. -LLLater::handle_t LLLater::doPeriodically(bool_func_t callable, F32 seconds) -{ - // Passing seconds <= 0 will produce an infinite loop. - llassert(seconds > 0); - auto iter{ doAtTime1(LLDate::now().secondsSinceEpoch() + seconds) }; - // The whole reason we split doAtTime() into doAtTime1() and doAtTime2() - // is to be able to bind the mHandles entry into Periodic. - return doAtTime2(Periodic{ this, iter, callable, seconds }, iter); + // Make an Timers::handle_t from token. + return { token }; } -bool LLLater::isRunning(handle_t timer) const +bool Timers::isRunning(handle_t timer) const { // A default-constructed timer isn't running. - // A timer we don't find in mHandles has fired or been canceled. - return timer && mHandles.find(timer.token) != mHandles.end(); + // A timer we don't find in mMeta has fired or been canceled. + return timer && mMeta.find(timer.token) != mMeta.end(); } -F32 LLLater::getRemaining(handle_t timer) const +F32 Timers::timeUntilCall(handle_t timer) const { - auto found{ mHandles.find(timer.token) }; - if (found == mHandles.end()) + MetaMap::const_iterator found; + if ((! timer) || (found = mMeta.find(timer.token)) == mMeta.end()) { return 0.f; } else { - // HandleMap::iterator references (token, (handle, time)) pair - return found->second.second - LLDate::now().secondsSinceEpoch(); + return found->second.mTime - now(); } } -// Cancel a future timer set by doAtTime(), doAfterInterval(), doPeriodically() -bool LLLater::cancel(handle_t& timer) +// Cancel a future timer set by scheduleAt(), scheduleAfter(), scheduleRepeating() +bool Timers::cancel(handle_t& timer) { // For exception safety, capture and clear timer before canceling. // Once we've canceled this handle, don't retain the live handle. @@ -244,7 +213,7 @@ bool LLLater::cancel(handle_t& timer) return cancel(ctimer); } -bool LLLater::cancel(const handle_t& timer) +bool Timers::cancel(const handle_t& timer) { if (! timer) { @@ -257,27 +226,38 @@ bool LLLater::cancel(const handle_t& timer) // Nor do we find any documented way to ask whether a given handle still // tracks a valid heap node. That's why we capture all returned handles in - // mHandles and validate against that collection. What about the pop() + // mMeta and validate against that collection. What about the pop() // call in tick()? How to map from the top() value back to the // corresponding handle_t? That's why we store func_at::mToken. // fibonacci_heap provides a pair of begin()/end() methods to iterate over // all nodes (NOT in heap order), plus a function to convert from such - // iterators to handles. Without mHandles, that would be our only chance + // iterators to handles. Without mMeta, that would be our only chance // to validate. - auto found{ mHandles.find(timer.token) }; - if (found == mHandles.end()) + auto found{ mMeta.find(timer.token) }; + if (found == mMeta.end()) { // we don't recognize this handle -- maybe the timer has already // fired, maybe it was previously canceled. return false; } - // HandleMap::iterator references (token, (handle, time)) pair. + // Funny case: what if the callback directly or indirectly reaches a + // cancel() call for its own handle? + if (found->second.mRunning) + { + // tick() has special logic to defer the actual deletion until the + // callback has returned + found->second.mCancel = true; + // this handle does in fact reference a live timer, + // which we're going to cancel when we get a chance + return true; + } + // Erase from mQueue the handle_type referenced by timer.token. - mQueue.erase(found->second.first); - // before erasing timer.token from mHandles - mHandles.erase(found); + mQueue.erase(found->second.mHandle); + // before erasing the mMeta entry + mMeta.erase(found); if (mQueue.empty()) { // If that was the last active timer, unregister for callbacks. @@ -289,7 +269,33 @@ bool LLLater::cancel(const handle_t& timer) return true; } -bool LLLater::tick() +// RAII class to set specified variable to specified value +// only for the duration of containing scope +template <typename VAR, typename VALUE> +class TempSet +{ +public: + TempSet(VAR& var, const VALUE& value): + mVar(var), + mOldValue(mVar) + { + mVar = value; + } + + TempSet(const TempSet&) = delete; + TempSet& operator=(const TempSet&) = delete; + + ~TempSet() + { + mVar = mOldValue; + } + +private: + VAR& mVar; + VALUE mOldValue; +}; + +bool Timers::tick() { // Fetch current time only on entry, even though running some mQueue task // may take long enough that the next one after would become ready. We're @@ -297,34 +303,84 @@ bool LLLater::tick() // starve it if we have a sequence of tasks that take nontrivial time. auto now{ LLDate::now().secondsSinceEpoch() }; auto cutoff{ now + TIMESLICE }; + + // Capture tasks we've processed but that want to be rescheduled. + // Defer rescheduling them immediately to avoid getting stuck looping over + // a recurring task with a nonpositive interval. + std::vector<std::pair<MetaMap::iterator, func_at>> deferred; + while (! mQueue.empty()) { auto& top{ mQueue.top() }; if (top.mTime > now) { // we've hit an entry that's still in the future: - // done with this tick(), but schedule another call - return false; + // done with this tick() + break; } if (LLDate::now().secondsSinceEpoch() > cutoff) { // we still have ready tasks, but we've already eaten too much - // time this tick() -- defer until next tick() -- call again - return false; + // time this tick() -- defer until next tick() + break; } - // Found a ready task. Hate to copy stuff, but -- what if the task - // indirectly ends up trying to cancel a handle referencing its own - // node in mQueue? If the task has any state, that would be Bad. Copy - // the node before running it. - auto current{ top }; - // remove the mHandles entry referencing this task - mHandles.erase(current.mToken); - // before removing the mQueue task entry itself + // Found a ready task. Look up its corresponding mMeta entry. + auto meta{ mMeta.find(top.mToken) }; + llassert(meta != mMeta.end()); + bool done; + { + // Mark our mMeta entry so we don't cancel this timer while its + // callback is running, but unmark it even in case of exception. + TempSet running(meta->second.mRunning, true); + // run the callback and capture its desire to end repetition + try + { + done = top.mFunc(); + } + catch (...) + { + // Don't crash if a timer callable throws. + // But don't continue calling that callable, either. + done = true; + LOG_UNHANDLED_EXCEPTION("LL::Timers"); + } + } // clear mRunning + + // If mFunc() returned true (all done, stop calling me) or + // meta->mCancel (somebody tried to cancel this timer during the + // callback call), then we're done: clean up both entries. + if (done || meta->second.mCancel) + { + // remove the mMeta entry referencing this task + mMeta.erase(meta); + } + else + { + // mFunc returned false, and nobody asked to cancel: + // continue calling this task at a future time. + meta->second.mTime += meta->second.mInterval; + // capture this task to reschedule once we break loop + deferred.push_back({meta, top}); + // update func_at's mTime to match meta's + deferred.back().second.mTime = meta->second.mTime; + } + // Remove the mQueue entry regardless, or we risk stalling the + // queue right here if we have a nonpositive interval. mQueue.pop(); - // okay, NOW run - current.mFunc(); } - // queue is empty: stop callbacks - return true; + + // Now reschedule any tasks that need to be rescheduled. + for (const auto& [meta, task] : deferred) + { + auto handle{ mQueue.push(task) }; + // track this new mQueue handle_type + meta->second.mHandle = handle; + } + + // If, after all the twiddling above, our queue ended up empty, + // stop calling every tick. + return mQueue.empty(); } + +} // namespace LL diff --git a/indra/llcommon/llcallbacklist.h b/indra/llcommon/llcallbacklist.h index 3ff1aad04e..f9b15867ef 100644 --- a/indra/llcommon/llcallbacklist.h +++ b/indra/llcommon/llcallbacklist.h @@ -101,11 +101,14 @@ LLCallbackList::handle_t doOnIdleRepeating(bool_func_t callable) } /***************************************************************************** -* LLLater: callbacks at some future time +* LL::Timers: callbacks at some future time *****************************************************************************/ -class LLLater: public LLSingleton<LLLater> +namespace LL { - LLSINGLETON(LLLater); + +class Timers: public LLSingleton<Timers> +{ + LLSINGLETON(Timers); using token_t = U32; @@ -113,11 +116,14 @@ class LLLater: public LLSingleton<LLLater> // a tuple, because we need to define the comparison operator. struct func_at { - nullary_func_t mFunc; + // callback to run when this timer fires + bool_func_t mFunc; + // key to look up metadata in mHandles token_t mToken; + // time at which this timer is supposed to fire LLDate::timestamp mTime; - func_at(const nullary_func_t& func, token_t token, LLDate::timestamp tm): + func_at(const bool_func_t& func, token_t token, LLDate::timestamp tm): mFunc(func), mToken(token), mTime(tm) @@ -146,7 +152,7 @@ public: class handle_t { private: - friend class LLLater; + friend class Timers; token_t token; public: handle_t(token_t token=0): token(token) {} @@ -156,33 +162,33 @@ public: }; // Call a given callable once at specified timestamp. - handle_t doAtTime(nullary_func_t callable, LLDate::timestamp time); + handle_t scheduleAt(nullary_func_t callable, LLDate::timestamp time); // Call a given callable once after specified interval. - handle_t doAfterInterval(nullary_func_t callable, F32 seconds); + handle_t scheduleAfter(nullary_func_t callable, F32 seconds); // Call a given callable every specified number of seconds, until it returns true. - handle_t doPeriodically(bool_func_t callable, F32 seconds); + handle_t scheduleRepeating(bool_func_t callable, F32 seconds); // test whether specified handle is still live bool isRunning(handle_t timer) const; // check remaining time - F32 getRemaining(handle_t timer) const; + F32 timeUntilCall(handle_t timer) const; - // Cancel a future timer set by doAtTime(), doAfterInterval(), doPeriodically(). - // Return true iff the handle corresponds to a live timer. + // Cancel a future timer set by scheduleAt(), scheduleAfter(), scheduleRepeating(). + // Return true if and only if the handle corresponds to a live timer. bool cancel(const handle_t& timer); // If we're canceling a non-const handle_t, also clear it so we need not // cancel again. bool cancel(handle_t& timer); - // Store a handle_t returned by doAtTime(), doAfterInterval() or - // doPeriodically() in a temp_handle_t to cancel() automatically on + // Store a handle_t returned by scheduleAt(), scheduleAfter() or + // scheduleRepeating() in a temp_handle_t to cancel() automatically on // destruction of the temp_handle_t. class temp_handle_t { public: - temp_handle_t() {} + temp_handle_t() = default; temp_handle_t(const handle_t& hdl): mHandle(hdl) {} temp_handle_t(const temp_handle_t&) = delete; temp_handle_t(temp_handle_t&&) = default; @@ -204,11 +210,11 @@ public: // temp_handle_t should be usable wherever handle_t is operator handle_t() const { return mHandle; } // If we're dealing with a non-const temp_handle_t, pass a reference - // to our handle_t member (e.g. to LLLater::cancel()). + // to our handle_t member (e.g. to Timers::cancel()). operator handle_t&() { return mHandle; } // For those in the know, provide a cancel() method of our own that - // avoids LLLater::instance() lookup when mHandle isn't live. + // avoids Timers::instance() lookup when mHandle isn't live. bool cancel() { if (! mHandle) @@ -217,7 +223,7 @@ public: } else { - return LLLater::instance().cancel(mHandle); + return Timers::instance().cancel(mHandle); } } @@ -231,44 +237,64 @@ public: }; private: + handle_t scheduleAtRepeating(bool_func_t callable, LLDate::timestamp time, F32 interval); + LLDate::timestamp now() const { return LLDate::now().secondsSinceEpoch(); } + // wrap a nullary_func_t with a bool_func_t that will only execute once + bool_func_t once(nullary_func_t callable) + { + return [callable] + { + callable(); + return true; + }; + } bool tick(); // NOTE: We don't lock our data members because it doesn't make sense to - // register cross-thread callbacks. If we start wanting to use them on + // register cross-thread callbacks. If we start wanting to use Timers on // threads other than the main thread, it would make more sense to make // our data members thread_local than to lock them. // the heap aka priority queue queue_t mQueue; - // handles we've returned that haven't yet canceled - using HandleMap = std::unordered_map< - token_t, - std::pair<queue_t::handle_type, LLDate::timestamp>>; - HandleMap mHandles; + + // metadata about a given task + struct Metadata + { + // handle to mQueue entry + queue_t::handle_type mHandle; + // time at which this timer is supposed to fire + LLDate::timestamp mTime; + // interval at which this timer is supposed to fire repeatedly + F32 mInterval{ 0 }; + // mFunc is currently running: don't delete this entry + bool mRunning{ false }; + // cancel() was called while mFunc was running: deferred cancel + bool mCancel{ false }; + }; + + using MetaMap = std::unordered_map<token_t, Metadata>; + MetaMap mMeta; token_t mToken{ 0 }; // While mQueue is non-empty, register for regular callbacks. LLCallbackList::temp_handle_t mLive; - - struct Periodic; - - // internal implementation for doAtTime() - HandleMap::iterator doAtTime1(LLDate::timestamp time); - handle_t doAtTime2(nullary_func_t callable, HandleMap::iterator iter); }; +} // namespace LL + /*-------------------- legacy names in global namespace --------------------*/ // Call a given callable once after specified interval. inline -LLLater::handle_t doAfterInterval(nullary_func_t callable, F32 seconds) +LL::Timers::handle_t doAfterInterval(nullary_func_t callable, F32 seconds) { - return LLLater::instance().doAfterInterval(callable, seconds); + return LL::Timers::instance().scheduleAfter(callable, seconds); } // Call a given callable every specified number of seconds, until it returns true. inline -LLLater::handle_t doPeriodically(bool_func_t callable, F32 seconds) +LL::Timers::handle_t doPeriodically(bool_func_t callable, F32 seconds) { - return LLLater::instance().doPeriodically(callable, seconds); + return LL::Timers::instance().scheduleRepeating(callable, seconds); } #endif diff --git a/indra/llcommon/lleventfilter.cpp b/indra/llcommon/lleventfilter.cpp index e72ae7ad33..ad61e9298a 100644 --- a/indra/llcommon/lleventfilter.cpp +++ b/indra/llcommon/lleventfilter.cpp @@ -87,7 +87,7 @@ LLEventTimeout::LLEventTimeout(LLEventPump& source): void LLEventTimeout::actionAfter(F32 seconds, const Action& action) { - mTimer = LLLater::instance().doAfterInterval(action, seconds); + mTimer = LL::Timers::instance().scheduleAfter(action, seconds); } void LLEventTimeout::errorAfter(F32 seconds, const std::string& message) @@ -118,7 +118,7 @@ void LLEventTimeout::cancel() bool LLEventTimeout::running() const { - return LLLater::instance().isRunning(mTimer); + return LL::Timers::instance().isRunning(mTimer); } /***************************************************************************** @@ -277,17 +277,17 @@ F32 LLEventThrottle::getDelay() const void LLEventThrottle::alarmActionAfter(F32 interval, const LLEventTimeout::Action& action) { - mAlarm = LLLater::instance().doAfterInterval(action, interval); + mAlarm = LL::Timers::instance().scheduleAfter(action, interval); } bool LLEventThrottle::alarmRunning() const { - return LLLater::instance().isRunning(mAlarm); + return LL::Timers::instance().isRunning(mAlarm); } void LLEventThrottle::alarmCancel() { - LLLater::instance().cancel(mAlarm); + LL::Timers::instance().cancel(mAlarm); } void LLEventThrottle::timerSet(F32 interval) diff --git a/indra/llcommon/lleventfilter.h b/indra/llcommon/lleventfilter.h index 1deb6f0f4c..b39791c560 100644 --- a/indra/llcommon/lleventfilter.h +++ b/indra/llcommon/lleventfilter.h @@ -191,7 +191,7 @@ public: private: // Use a temp_handle_t so it's canceled on destruction. - LLLater::temp_handle_t mTimer; + LL::Timers::temp_handle_t mTimer; }; /** @@ -300,7 +300,7 @@ private: F32 mInterval; // use this to arrange a deferred flush() call - LLLater::handle_t mAlarm; + LL::Timers::handle_t mAlarm; }; /** diff --git a/indra/llcommon/lleventtimer.cpp b/indra/llcommon/lleventtimer.cpp index 0f8d1e636f..1d2da93683 100644 --- a/indra/llcommon/lleventtimer.cpp +++ b/indra/llcommon/lleventtimer.cpp @@ -49,20 +49,20 @@ LLEventTimer::~LLEventTimer() void LLEventTimer::start() { - mTimer = LLLater::instance().doPeriodically([this]{ return tick(); }, mPeriod); + mTimer = LL::Timers::instance().scheduleRepeating([this]{ return tick(); }, mPeriod); } void LLEventTimer::stop() { - LLLater::instance().cancel(mTimer); + LL::Timers::instance().cancel(mTimer); } bool LLEventTimer::isRunning() { - return LLLater::instance().isRunning(mTimer); + return LL::Timers::instance().isRunning(mTimer); } F32 LLEventTimer::getRemaining() { - return LLLater::instance().getRemaining(mTimer); + return LL::Timers::instance().timeUntilCall(mTimer); } diff --git a/indra/llcommon/lleventtimer.h b/indra/llcommon/lleventtimer.h index 05d8bc038d..a325c704e0 100644 --- a/indra/llcommon/lleventtimer.h +++ b/indra/llcommon/lleventtimer.h @@ -50,7 +50,7 @@ public: virtual bool tick() = 0; protected: - LLLater::temp_handle_t mTimer; + LL::Timers::temp_handle_t mTimer; F32 mPeriod; }; |