diff options
Diffstat (limited to 'indra/llcommon/llcallbacklist.cpp')
-rw-r--r-- | indra/llcommon/llcallbacklist.cpp | 549 |
1 files changed, 443 insertions, 106 deletions
diff --git a/indra/llcommon/llcallbacklist.cpp b/indra/llcommon/llcallbacklist.cpp index b5a58e90b3..015475a903 100644 --- a/indra/llcommon/llcallbacklist.cpp +++ b/indra/llcommon/llcallbacklist.cpp @@ -24,18 +24,22 @@ * $/LicenseInfo$ */ +#include "lazyeventapi.h" #include "llcallbacklist.h" -#include "lleventtimer.h" -#include "llerrorlegacy.h" - -// Globals -// -LLCallbackList gIdleCallbacks; +#include "llerror.h" +#include "llexception.h" +#include "llsdutil.h" +#include <boost/container_hash/hash.hpp> +#include <iomanip> +#include <vector> // // Member functions // +/***************************************************************************** +* LLCallbackList +*****************************************************************************/ LLCallbackList::LLCallbackList() { // nothing @@ -45,186 +49,519 @@ LLCallbackList::~LLCallbackList() { } - -void LLCallbackList::addFunction( callback_t func, void *data) +LLCallbackList::handle_t LLCallbackList::addFunction( callback_t func, void *data) { if (!func) { - return; + return {}; } // only add one callback per func/data pair // if (containsFunction(func, data)) { - return; + return {}; } - callback_pair_t t(func, data); - mCallbackList.push_back(t); + auto handle = addFunction([func, data]{ func(data); }); + mLookup.emplace(callback_pair_t(func, data), handle); + return handle; } -bool LLCallbackList::containsFunction( callback_t func, void *data) +LLCallbackList::handle_t LLCallbackList::addFunction( const callable_t& func ) { - callback_pair_t t(func, data); - callback_list_t::iterator iter = find(func,data); - if (iter != mCallbackList.end()) - { - return TRUE; - } - else - { - return FALSE; - } + return mCallbackList.connect(func); } +bool LLCallbackList::containsFunction( callback_t func, void *data) +{ + return mLookup.find(callback_pair_t(func, data)) != mLookup.end(); +} bool LLCallbackList::deleteFunction( callback_t func, void *data) { - callback_list_t::iterator iter = find(func,data); - if (iter != mCallbackList.end()) + auto found = mLookup.find(callback_pair_t(func, data)); + if (found != mLookup.end()) { - mCallbackList.erase(iter); - return TRUE; + mLookup.erase(found); + deleteFunction(found->second); + return true; } else { - return FALSE; + return false; } } -inline -LLCallbackList::callback_list_t::iterator -LLCallbackList::find(callback_t func, void *data) +void LLCallbackList::deleteFunction( const handle_t& handle ) { - callback_pair_t t(func, data); - return std::find(mCallbackList.begin(), mCallbackList.end(), t); + handle.disconnect(); } void LLCallbackList::deleteAllFunctions() { - mCallbackList.clear(); + mCallbackList = {}; + mLookup.clear(); } - void LLCallbackList::callFunctions() { - for (callback_list_t::iterator iter = mCallbackList.begin(); iter != mCallbackList.end(); ) + mCallbackList(); +} + +LLCallbackList::handle_t LLCallbackList::doOnIdleOneTime( const callable_t& func ) +{ + // connect_extended() passes the connection to the callback + return mCallbackList.connect_extended( + [func](const handle_t& handle) + { + handle.disconnect(); + func(); + }); +} + +LLCallbackList::handle_t LLCallbackList::doOnIdleRepeating( const bool_func_t& func ) +{ + return mCallbackList.connect_extended( + [func](const handle_t& handle) + { + if (func()) + { + handle.disconnect(); + } + }); +} + +/***************************************************************************** +* LL::Timers +*****************************************************************************/ +namespace LL +{ + +Timers::Timers() {} + +// Call a given callable once at specified timestamp. +Timers::handle_t Timers::scheduleAt(nullary_func_t callable, LLDate::timestamp time) +{ + // tick() assumes you want to run periodically until you return true. + // Schedule a task that returns true after a single call. + return scheduleAtEvery(once(callable), time, 0); +} + +// Call a given callable once after specified interval. +Timers::handle_t Timers::scheduleAfter(nullary_func_t callable, F32 seconds) +{ + return scheduleEvery(once(callable), seconds); +} + +// Call a given callable every specified number of seconds, until it returns true. +Timers::handle_t Timers::scheduleEvery(bool_func_t callable, F32 seconds) +{ + return scheduleAtEvery(callable, now() + seconds, seconds); +} + +Timers::handle_t Timers::scheduleAtEvery(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()) { - callback_list_t::iterator curiter = iter++; - curiter->first(curiter->second); + // If this is our first entry, register for regular callbacks. + mLive = LLCallbackList::instance().doOnIdleRepeating([this]{ return tick(); }); } + // Make an Timers::handle_t from token. + return { token }; } -// Shim class to allow arbitrary boost::bind -// expressions to be run as one-time idle callbacks. -class OnIdleCallbackOneTime +bool Timers::isRunning(handle_t timer) const { -public: - OnIdleCallbackOneTime(nullary_func_t callable): - mCallable(callable) + // A default-constructed timer isn't running. + // A timer we don't find in mMeta has fired or been canceled. + return timer && mMeta.find(timer.token) != mMeta.end(); +} + +F32 Timers::timeUntilCall(handle_t timer) const +{ + MetaMap::const_iterator found; + if ((! timer) || (found = mMeta.find(timer.token)) == mMeta.end()) { + return 0.f; } - static void onIdle(void *data) + else { - gIdleCallbacks.deleteFunction(onIdle, data); - OnIdleCallbackOneTime* self = reinterpret_cast<OnIdleCallbackOneTime*>(data); - self->call(); - delete self; + return found->second.mTime - now(); } - void call() +} + +// Cancel a future timer set by scheduleAt(), scheduleAfter(), scheduleEvery() +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. + const handle_t ctimer{ timer }; + timer = handle_t(); + return cancel(ctimer); +} + +bool Timers::cancel(const handle_t& timer) +{ + if (! timer) { - mCallable(); + return false; } -private: - nullary_func_t mCallable; -}; -void doOnIdleOneTime(nullary_func_t callable) + // fibonacci_heap documentation does not address the question of what + // happens if you call erase() twice with the same handle. Is it a no-op? + // Does it invalidate the heap? Is it UB? + + // 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 + // 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 mMeta, that would be our only chance + // to validate. + 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; + } + + // 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.mHandle); + // before erasing the mMeta entry + mMeta.erase(found); + if (mQueue.empty()) + { + // If that was the last active timer, unregister for callbacks. + //LLCallbackList::instance().deleteFunction(mLive); + // Since we're in the source file that knows the true identity of an + // LLCallbackList::handle_t, we don't even need to call instance(). + mLive.disconnect(); + } + return true; +} + +void Timers::setTimeslice(F32 timeslice) { - OnIdleCallbackOneTime* cb_functor = new OnIdleCallbackOneTime(callable); - gIdleCallbacks.addFunction(&OnIdleCallbackOneTime::onIdle,cb_functor); + if (timeslice < MINIMUM_TIMESLICE) + { + // use stringize() so setprecision() affects only the temporary + // ostream, not the common logging ostream + LL_WARNS("Timers") << "LL::Timers::setTimeslice(" + << stringize(std::setprecision(4), timeslice) + << ") less than " + << stringize(std::setprecision(4), MINIMUM_TIMESLICE) + << ", ignoring" << LL_ENDL; + } + else + { + mTimeslice = timeslice; + } } -// Shim class to allow generic boost functions to be run as -// recurring idle callbacks. Callable should return true when done, -// false to continue getting called. -class OnIdleCallbackRepeating +// RAII class to set specified variable to specified value +// only for the duration of containing scope +template <typename VAR, typename VALUE> +class TempSet { public: - OnIdleCallbackRepeating(bool_func_t callable): - mCallable(callable) + TempSet(VAR& var, const VALUE& value): + mVar(var), + mOldValue(mVar) { + mVar = value; } - // Will keep getting called until the callable returns true. - static void onIdle(void *data) + + TempSet(const TempSet&) = delete; + TempSet& operator=(const TempSet&) = delete; + + ~TempSet() { - OnIdleCallbackRepeating* self = reinterpret_cast<OnIdleCallbackRepeating*>(data); - bool done = self->call(); - if (done) + 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 + // sharing this thread with everything else, and there's a risk we might + // starve it if we have a sequence of tasks that take nontrivial time. + auto now{ LLDate::now().secondsSinceEpoch() }; + auto cutoff{ now + mTimeslice }; + + // 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() + break; + } + if (LLDate::now().secondsSinceEpoch() > cutoff) { - gIdleCallbacks.deleteFunction(onIdle, data); - delete self; + // we still have ready tasks, but we've already eaten too much + // time this tick() -- defer until next tick() + break; } + + // 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(); } - bool call() + + // Now reschedule any tasks that need to be rescheduled. + for (const auto& [meta, task] : deferred) { - return mCallable(); + auto handle{ mQueue.push(task) }; + // track this new mQueue handle_type + meta->second.mHandle = handle; } -private: - bool_func_t mCallable; -}; -void doOnIdleRepeating(bool_func_t callable) -{ - OnIdleCallbackRepeating* cb_functor = new OnIdleCallbackRepeating(callable); - gIdleCallbacks.addFunction(&OnIdleCallbackRepeating::onIdle,cb_functor); + // If, after all the twiddling above, our queue ended up empty, + // stop calling every tick. + return mQueue.empty(); } -class NullaryFuncEventTimer: public LLEventTimer +/***************************************************************************** +* TimersListener +*****************************************************************************/ + +class TimersListener: public LLEventAPI { public: - NullaryFuncEventTimer(nullary_func_t callable, F32 seconds): - LLEventTimer(seconds), - mCallable(callable) + TimersListener(const LazyEventAPIParams& params): LLEventAPI(params) {} + + // Forbid a script from requesting callbacks too quickly. + static constexpr LLSD::Real MINTIMER{ 1.0 }; + + void scheduleAfter(const LLSD& params); + void scheduleEvery(const LLSD& params); + LLSD cancel(const LLSD& params); + LLSD isRunning(const LLSD& params); + LLSD timeUntilCall(const LLSD& params); + +private: + // We use the incoming reqid to distinguish different timers -- but reqid + // by itself is not unique! Each reqid is local to a calling script. + // Distinguish scripts by reply-pump name, then reqid within script. + // "Additional specializations for std::pair and the standard container + // types, as well as utility functions to compose hashes are available in + // boost::hash." + // https://en.cppreference.com/w/cpp/utility/hash + using HandleKey = std::pair<LLSD::String, LLSD::Integer>; + using HandleMap = std::unordered_map<HandleKey, Timers::temp_handle_t, + boost::hash<HandleKey>>; + HandleMap mHandles; +}; + +void TimersListener::scheduleAfter(const LLSD& params) +{ + // Timer creation functions respond immediately with the reqid of the + // created timer, as well as later when the timer fires. That lets the + // requester invoke cancel, isRunning or timeUntilCall. + Response response(LLSD(), params); + LLSD::Real after{ params["after"] }; + if (after < MINTIMER) { + return response.error(stringize("after must be at least ", MINTIMER)); } -private: - BOOL tick() + HandleKey key{ params["reply"], params["reqid"] }; + mHandles.emplace( + key, + Timers::instance().scheduleAfter( + [this, params, key] + { + // we don't need any content save for the "reqid" + sendReply({}, params); + // ditch mHandles entry + mHandles.erase(key); + }, + after)); +} + +void TimersListener::scheduleEvery(const LLSD& params) +{ + // Timer creation functions respond immediately with the reqid of the + // created timer, as well as later when the timer fires. That lets the + // requester invoke cancel, isRunning or timeUntilCall. + Response response(LLSD(), params); + LLSD::Real every{ params["every"] }; + if (every < MINTIMER) { - mCallable(); - return TRUE; + return response.error(stringize("every must be at least ", MINTIMER)); } - nullary_func_t mCallable; -}; + mHandles.emplace( + HandleKey{ params["reply"], params["reqid"] }, + Timers::instance().scheduleEvery( + [params, i=0]() mutable + { + // we don't need any content save for the "reqid" + sendReply(llsd::map("i", i++), params); + // we can't use a handshake -- always keep the ball rolling + return false; + }, + every)); +} -// Call a given callable once after specified interval. -void doAfterInterval(nullary_func_t callable, F32 seconds) +LLSD TimersListener::cancel(const LLSD& params) { - new NullaryFuncEventTimer(callable, seconds); + auto found{ mHandles.find({params["reply"], params["id"]}) }; + bool ok = false; + if (found != mHandles.end()) + { + ok = true; + Timers::instance().cancel(found->second); + mHandles.erase(found); + } + return llsd::map("ok", ok); } -class BoolFuncEventTimer: public LLEventTimer +LLSD TimersListener::isRunning(const LLSD& params) { -public: - BoolFuncEventTimer(bool_func_t callable, F32 seconds): - LLEventTimer(seconds), - mCallable(callable) + auto found{ mHandles.find({params["reply"], params["id"]}) }; + bool running = false; + if (found != mHandles.end()) { + running = Timers::instance().isRunning(found->second); } -private: - BOOL tick() + return llsd::map("running", running); +} + +LLSD TimersListener::timeUntilCall(const LLSD& params) +{ + auto found{ mHandles.find({params["reply"], params["id"]}) }; + bool ok = false; + LLSD::Real remaining = 0; + if (found != mHandles.end()) { - return mCallable(); + ok = true; + remaining = Timers::instance().timeUntilCall(found->second); } + return llsd::map("ok", ok, "remaining", remaining); +} - bool_func_t mCallable; +class TimersRegistrar: public LazyEventAPI<TimersListener> +{ + using super = LazyEventAPI<TimersListener>; + using super::listener; + +public: + TimersRegistrar(): + super("Timers", "Provide access to viewer timer functionality.") + { + add("scheduleAfter", +R"-(Create a timer with ID "reqid". Post response after "after" seconds.)-", + &listener::scheduleAfter, + llsd::map("reqid", LLSD::Integer(), "after", LLSD::Real())); + add("scheduleEvery", +R"-(Create a timer with ID "reqid". Post response every "every" seconds +until cancel().)-", + &listener::scheduleEvery, + llsd::map("reqid", LLSD::Integer(), "every", LLSD::Real())); + add("cancel", +R"-(Cancel the timer with ID "id". Respond "ok"=true if "id" identifies +a live timer.)-", + &listener::cancel, + llsd::map("reqid", LLSD::Integer(), "id", LLSD::Integer())); + add("isRunning", +R"-(Query the timer with ID "id": respond "running"=true if "id" identifies +a live timer.)-", + &listener::isRunning, + llsd::map("reqid", LLSD::Integer(), "id", LLSD::Integer())); + add("timeUntilCall", +R"-(Query the timer with ID "id": if "id" identifies a live timer, respond +"ok"=true, "remaining"=seconds with the time left before timer expiry; +otherwise "ok"=false, "remaining"=0.)-", + &listener::timeUntilCall, + llsd::map("reqid", LLSD::Integer())); + } }; +static TimersRegistrar registrar; -// Call a given callable every specified number of seconds, until it returns true. -void doPeriodically(bool_func_t callable, F32 seconds) -{ - new BoolFuncEventTimer(callable, seconds); -} +} // namespace LL |