Zerotorescue@0: --- **AceTimer-3.0** provides a central facility for registering timers. Zerotorescue@0: -- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient Zerotorescue@0: -- data structure that allows easy dispatching and fast rescheduling. Timers can be registered, rescheduled Zerotorescue@0: -- or canceled at any time, even from within a running timer, without conflict or large overhead.\\ Zerotorescue@0: -- AceTimer is currently limited to firing timers at a frequency of 0.1s. This constant may change Zerotorescue@0: -- in the future, but for now it seemed like a good compromise in efficiency and accuracy. Zerotorescue@0: -- Zerotorescue@0: -- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you Zerotorescue@0: -- need to cancel or reschedule the timer you just registered. Zerotorescue@0: -- Zerotorescue@0: -- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by Zerotorescue@0: -- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object Zerotorescue@0: -- and can be accessed directly, without having to explicitly call AceTimer itself.\\ Zerotorescue@0: -- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you Zerotorescue@0: -- make into AceTimer. Zerotorescue@0: -- @class file Zerotorescue@0: -- @name AceTimer-3.0 Zerotorescue@0: -- @release $Id: AceTimer-3.0.lua 895 2009-12-06 16:28:55Z nevcairiel $ Zerotorescue@0: Zerotorescue@0: --[[ Zerotorescue@0: Basic assumptions: Zerotorescue@0: * In a typical system, we do more re-scheduling per second than there are timer pulses per second Zerotorescue@0: * Regardless of timer implementation, we cannot guarantee timely delivery due to FPS restriction (may be as low as 10) Zerotorescue@0: Zerotorescue@0: This implementation: Zerotorescue@0: CON: The smallest timer interval is constrained by HZ (currently 1/10s). Zerotorescue@0: PRO: It will still correctly fire any timer slower than HZ over a length of time, e.g. 0.11s interval -> 90 times over 10 seconds Zerotorescue@0: PRO: In lag bursts, the system simly skips missed timer intervals to decrease load Zerotorescue@0: CON: Algorithms depending on a timer firing "N times per minute" will fail Zerotorescue@0: PRO: (Re-)scheduling is O(1) with a VERY small constant. It's a simple linked list insertion in a hash bucket. Zerotorescue@0: CAUTION: The BUCKETS constant constrains how many timers can be efficiently handled. With too many hash collisions, performance will decrease. Zerotorescue@0: Zerotorescue@0: Major assumptions upheld: Zerotorescue@0: - ALLOWS scheduling multiple timers with the same funcref/method Zerotorescue@0: - ALLOWS scheduling more timers during OnUpdate processing Zerotorescue@0: - ALLOWS unscheduling ANY timer (including the current running one) at any time, including during OnUpdate processing Zerotorescue@0: ]] Zerotorescue@0: Zerotorescue@0: local MAJOR, MINOR = "AceTimer-3.0", 5 Zerotorescue@0: local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR) Zerotorescue@0: Zerotorescue@0: if not AceTimer then return end -- No upgrade needed Zerotorescue@0: Zerotorescue@0: AceTimer.hash = AceTimer.hash or {} -- Array of [0..BUCKET-1] = linked list of timers (using .next member) Zerotorescue@0: -- Linked list gets around ACE-88 and ACE-90. Zerotorescue@0: AceTimer.selfs = AceTimer.selfs or {} -- Array of [self]={[handle]=timerobj, [handle2]=timerobj2, ...} Zerotorescue@0: AceTimer.frame = AceTimer.frame or CreateFrame("Frame", "AceTimer30Frame") Zerotorescue@0: Zerotorescue@0: -- Lua APIs Zerotorescue@0: local assert, error, loadstring = assert, error, loadstring Zerotorescue@0: local setmetatable, rawset, rawget = setmetatable, rawset, rawget Zerotorescue@0: local select, pairs, type, next, tostring = select, pairs, type, next, tostring Zerotorescue@0: local floor, max, min = math.floor, math.max, math.min Zerotorescue@0: local tconcat = table.concat Zerotorescue@0: Zerotorescue@0: -- WoW APIs Zerotorescue@0: local GetTime = GetTime Zerotorescue@0: Zerotorescue@0: -- Global vars/functions that we don't upvalue since they might get hooked, or upgraded Zerotorescue@0: -- List them here for Mikk's FindGlobals script Zerotorescue@0: -- GLOBALS: DEFAULT_CHAT_FRAME, geterrorhandler Zerotorescue@0: Zerotorescue@0: -- Simple ONE-SHOT timer cache. Much more efficient than a full compost for our purposes. Zerotorescue@0: local timerCache = nil Zerotorescue@0: Zerotorescue@0: --[[ Zerotorescue@0: Timers will not be fired more often than HZ-1 times per second. Zerotorescue@0: Keep at intended speed PLUS ONE or we get bitten by floating point rounding errors (n.5 + 0.1 can be n.599999) Zerotorescue@0: If this is ever LOWERED, all existing timers need to be enforced to have a delay >= 1/HZ on lib upgrade. Zerotorescue@0: If this number is ever changed, all entries need to be rehashed on lib upgrade. Zerotorescue@0: ]] Zerotorescue@0: local HZ = 11 Zerotorescue@0: Zerotorescue@0: --[[ Zerotorescue@0: Prime for good distribution Zerotorescue@0: If this number is ever changed, all entries need to be rehashed on lib upgrade. Zerotorescue@0: ]] Zerotorescue@0: local BUCKETS = 131 Zerotorescue@0: Zerotorescue@0: local hash = AceTimer.hash Zerotorescue@0: for i=1,BUCKETS do Zerotorescue@0: hash[i] = hash[i] or false -- make it an integer-indexed array; it's faster than hashes Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: --[[ Zerotorescue@0: xpcall safecall implementation Zerotorescue@0: ]] Zerotorescue@0: local xpcall = xpcall Zerotorescue@0: Zerotorescue@0: local function errorhandler(err) Zerotorescue@0: return geterrorhandler()(err) Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: local function CreateDispatcher(argCount) Zerotorescue@0: local code = [[ Zerotorescue@0: local xpcall, eh = ... -- our arguments are received as unnamed values in "..." since we don't have a proper function declaration Zerotorescue@0: local method, ARGS Zerotorescue@0: local function call() return method(ARGS) end Zerotorescue@0: Zerotorescue@0: local function dispatch(func, ...) Zerotorescue@0: method = func Zerotorescue@0: if not method then return end Zerotorescue@0: ARGS = ... Zerotorescue@0: return xpcall(call, eh) Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: return dispatch Zerotorescue@0: ]] Zerotorescue@0: Zerotorescue@0: local ARGS = {} Zerotorescue@0: for i = 1, argCount do ARGS[i] = "arg"..i end Zerotorescue@0: code = code:gsub("ARGS", tconcat(ARGS, ", ")) Zerotorescue@0: return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(xpcall, errorhandler) Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: local Dispatchers = setmetatable({}, { Zerotorescue@0: __index=function(self, argCount) Zerotorescue@0: local dispatcher = CreateDispatcher(argCount) Zerotorescue@0: rawset(self, argCount, dispatcher) Zerotorescue@0: return dispatcher Zerotorescue@0: end Zerotorescue@0: }) Zerotorescue@0: Dispatchers[0] = function(func) Zerotorescue@0: return xpcall(func, errorhandler) Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: local function safecall(func, ...) Zerotorescue@0: return Dispatchers[select('#', ...)](func, ...) Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: local lastint = floor(GetTime() * HZ) Zerotorescue@0: Zerotorescue@0: -- -------------------------------------------------------------------- Zerotorescue@0: -- OnUpdate handler Zerotorescue@0: -- Zerotorescue@0: -- traverse buckets, always chasing "now", and fire timers that have expired Zerotorescue@0: Zerotorescue@0: local function OnUpdate() Zerotorescue@0: local now = GetTime() Zerotorescue@0: local nowint = floor(now * HZ) Zerotorescue@0: Zerotorescue@0: -- Have we passed into a new hash bucket? Zerotorescue@0: if nowint == lastint then return end Zerotorescue@0: Zerotorescue@0: local soon = now + 1 -- +1 is safe as long as 1 < HZ < BUCKETS/2 Zerotorescue@0: Zerotorescue@0: -- Pass through each bucket at most once Zerotorescue@0: -- Happens on e.g. instance loads, but COULD happen on high local load situations also Zerotorescue@0: for curint = (max(lastint, nowint - BUCKETS) + 1), nowint do -- loop until we catch up with "now", usually only 1 iteration Zerotorescue@0: local curbucket = (curint % BUCKETS)+1 Zerotorescue@0: -- Yank the list of timers out of the bucket and empty it. This allows reinsertion in the currently-processed bucket from callbacks. Zerotorescue@0: local nexttimer = hash[curbucket] Zerotorescue@0: hash[curbucket] = false -- false rather than nil to prevent the array from becoming a hash Zerotorescue@0: Zerotorescue@0: while nexttimer do Zerotorescue@0: local timer = nexttimer Zerotorescue@0: nexttimer = timer.next Zerotorescue@0: local when = timer.when Zerotorescue@0: Zerotorescue@0: if when < soon then Zerotorescue@0: -- Call the timer func, either as a method on given object, or a straight function ref Zerotorescue@0: local callback = timer.callback Zerotorescue@0: if type(callback) == "string" then Zerotorescue@0: safecall(timer.object[callback], timer.object, timer.arg) Zerotorescue@0: elseif callback then Zerotorescue@0: safecall(callback, timer.arg) Zerotorescue@0: else Zerotorescue@0: -- probably nilled out by CancelTimer Zerotorescue@0: timer.delay = nil -- don't reschedule it Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: local delay = timer.delay -- NOW make a local copy, can't do it earlier in case the timer cancelled itself in the callback Zerotorescue@0: Zerotorescue@0: if not delay then Zerotorescue@0: -- single-shot timer (or cancelled) Zerotorescue@0: AceTimer.selfs[timer.object][tostring(timer)] = nil Zerotorescue@0: timerCache = timer Zerotorescue@0: else Zerotorescue@0: -- repeating timer Zerotorescue@0: local newtime = when + delay Zerotorescue@0: if newtime < now then -- Keep lag from making us firing a timer unnecessarily. (Note that this still won't catch too-short-delay timers though.) Zerotorescue@0: newtime = now + delay Zerotorescue@0: end Zerotorescue@0: timer.when = newtime Zerotorescue@0: Zerotorescue@0: -- add next timer execution to the correct bucket Zerotorescue@0: local bucket = (floor(newtime * HZ) % BUCKETS) + 1 Zerotorescue@0: timer.next = hash[bucket] Zerotorescue@0: hash[bucket] = timer Zerotorescue@0: end Zerotorescue@0: else -- if when>=soon Zerotorescue@0: -- reinsert (yeah, somewhat expensive, but shouldn't be happening too often either due to hash distribution) Zerotorescue@0: timer.next = hash[curbucket] Zerotorescue@0: hash[curbucket] = timer Zerotorescue@0: end -- if whenhandle->timer registry Zerotorescue@0: local handle = tostring(timer) Zerotorescue@0: Zerotorescue@0: local selftimers = AceTimer.selfs[self] Zerotorescue@0: if not selftimers then Zerotorescue@0: selftimers = {} Zerotorescue@0: AceTimer.selfs[self] = selftimers Zerotorescue@0: end Zerotorescue@0: selftimers[handle] = timer Zerotorescue@0: selftimers.__ops = (selftimers.__ops or 0) + 1 Zerotorescue@0: Zerotorescue@0: return handle Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: --- Schedule a new one-shot timer. Zerotorescue@0: -- The timer will fire once in `delay` seconds, unless canceled before. Zerotorescue@0: -- @param callback Callback function for the timer pulse (funcref or method name). Zerotorescue@0: -- @param delay Delay for the timer, in seconds. Zerotorescue@0: -- @param arg An optional argument to be passed to the callback function. Zerotorescue@0: -- @usage Zerotorescue@0: -- MyAddon = LibStub("AceAddon-3.0"):NewAddon("TimerTest", "AceTimer-3.0") Zerotorescue@0: -- Zerotorescue@0: -- function MyAddon:OnEnable() Zerotorescue@0: -- self:ScheduleTimer("TimerFeedback", 5) Zerotorescue@0: -- end Zerotorescue@0: -- Zerotorescue@0: -- function MyAddon:TimerFeedback() Zerotorescue@0: -- print("5 seconds passed") Zerotorescue@0: -- end Zerotorescue@0: function AceTimer:ScheduleTimer(callback, delay, arg) Zerotorescue@0: return Reg(self, callback, delay, arg) Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: --- Schedule a repeating timer. Zerotorescue@0: -- The timer will fire every `delay` seconds, until canceled. Zerotorescue@0: -- @param callback Callback function for the timer pulse (funcref or method name). Zerotorescue@0: -- @param delay Delay for the timer, in seconds. Zerotorescue@0: -- @param arg An optional argument to be passed to the callback function. Zerotorescue@0: -- @usage Zerotorescue@0: -- MyAddon = LibStub("AceAddon-3.0"):NewAddon("TimerTest", "AceTimer-3.0") Zerotorescue@0: -- Zerotorescue@0: -- function MyAddon:OnEnable() Zerotorescue@0: -- self.timerCount = 0 Zerotorescue@0: -- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5) Zerotorescue@0: -- end Zerotorescue@0: -- Zerotorescue@0: -- function MyAddon:TimerFeedback() Zerotorescue@0: -- self.timerCount = self.timerCount + 1 Zerotorescue@0: -- print(("%d seconds passed"):format(5 * self.timerCount)) Zerotorescue@0: -- -- run 30 seconds in total Zerotorescue@0: -- if self.timerCount == 6 then Zerotorescue@0: -- self:CancelTimer(self.testTimer) Zerotorescue@0: -- end Zerotorescue@0: -- end Zerotorescue@0: function AceTimer:ScheduleRepeatingTimer(callback, delay, arg) Zerotorescue@0: return Reg(self, callback, delay, arg, true) Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: --- Cancels a timer with the given handle, registered by the same addon object as used for `:ScheduleTimer` Zerotorescue@0: -- Both one-shot and repeating timers can be canceled with this function, as long as the `handle` is valid Zerotorescue@0: -- and the timer has not fired yet or was canceled before. Zerotorescue@0: -- @param handle The handle of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer` Zerotorescue@0: -- @param silent If true, no error is raised if the timer handle is invalid (expired or already canceled) Zerotorescue@0: -- @return True if the timer was successfully cancelled. Zerotorescue@0: function AceTimer:CancelTimer(handle, silent) Zerotorescue@0: if not handle then return end -- nil handle -> bail out without erroring Zerotorescue@0: if type(handle) ~= "string" then Zerotorescue@0: error(MAJOR..": CancelTimer(handle): 'handle' - expected a string", 2) -- for now, anyway Zerotorescue@0: end Zerotorescue@0: local selftimers = AceTimer.selfs[self] Zerotorescue@0: local timer = selftimers and selftimers[handle] Zerotorescue@0: if silent then Zerotorescue@0: if timer then Zerotorescue@0: timer.callback = nil -- don't run it again Zerotorescue@0: timer.delay = nil -- if this is the currently-executing one: don't even reschedule Zerotorescue@0: -- The timer object is removed in the OnUpdate loop Zerotorescue@0: end Zerotorescue@0: return not not timer -- might return "true" even if we double-cancel. we'll live. Zerotorescue@0: else Zerotorescue@0: if not timer then Zerotorescue@0: geterrorhandler()(MAJOR..": CancelTimer(handle[, silent]): '"..tostring(handle).."' - no such timer registered") Zerotorescue@0: return false Zerotorescue@0: end Zerotorescue@0: if not timer.callback then Zerotorescue@0: geterrorhandler()(MAJOR..": CancelTimer(handle[, silent]): '"..tostring(handle).."' - timer already cancelled or expired") Zerotorescue@0: return false Zerotorescue@0: end Zerotorescue@0: timer.callback = nil -- don't run it again Zerotorescue@0: timer.delay = nil -- if this is the currently-executing one: don't even reschedule Zerotorescue@0: return true Zerotorescue@0: end Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: --- Cancels all timers registered to the current addon object ('self') Zerotorescue@0: function AceTimer:CancelAllTimers() Zerotorescue@0: if not(type(self) == "string" or type(self) == "table") then Zerotorescue@0: error(MAJOR..": CancelAllTimers(): 'self' - must be a string or a table",2) Zerotorescue@0: end Zerotorescue@0: if self == AceTimer then Zerotorescue@0: error(MAJOR..": CancelAllTimers(): supply a meaningful 'self'", 2) Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: local selftimers = AceTimer.selfs[self] Zerotorescue@0: if selftimers then Zerotorescue@0: for handle,v in pairs(selftimers) do Zerotorescue@0: if type(v) == "table" then -- avoid __ops, etc Zerotorescue@0: AceTimer.CancelTimer(self, handle, true) Zerotorescue@0: end Zerotorescue@0: end Zerotorescue@0: end Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: --- Returns the time left for a timer with the given handle, registered by the current addon object ('self'). Zerotorescue@0: -- This function will raise a warning when the handle is invalid, but not stop execution. Zerotorescue@0: -- @param handle The handle of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer` Zerotorescue@0: -- @return The time left on the timer, or false if the handle is invalid. Zerotorescue@0: function AceTimer:TimeLeft(handle) Zerotorescue@0: if not handle then return end Zerotorescue@0: if type(handle) ~= "string" then Zerotorescue@0: error(MAJOR..": TimeLeft(handle): 'handle' - expected a string", 2) -- for now, anyway Zerotorescue@0: end Zerotorescue@0: local selftimers = AceTimer.selfs[self] Zerotorescue@0: local timer = selftimers and selftimers[handle] Zerotorescue@0: if not timer then Zerotorescue@0: geterrorhandler()(MAJOR..": TimeLeft(handle): '"..tostring(handle).."' - no such timer registered") Zerotorescue@0: return false Zerotorescue@0: end Zerotorescue@0: return timer.when - GetTime() Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: Zerotorescue@0: -- --------------------------------------------------------------------- Zerotorescue@0: -- PLAYER_REGEN_ENABLED: Run through our .selfs[] array step by step Zerotorescue@0: -- and clean it out - otherwise the table indices can grow indefinitely Zerotorescue@0: -- if an addon starts and stops a lot of timers. AceBucket does this! Zerotorescue@0: -- Zerotorescue@0: -- See ACE-94 and tests/AceTimer-3.0-ACE-94.lua Zerotorescue@0: Zerotorescue@0: local lastCleaned = nil Zerotorescue@0: Zerotorescue@0: local function OnEvent(this, event) Zerotorescue@0: if event~="PLAYER_REGEN_ENABLED" then Zerotorescue@0: return Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: -- Get the next 'self' to process Zerotorescue@0: local selfs = AceTimer.selfs Zerotorescue@0: local self = next(selfs, lastCleaned) Zerotorescue@0: if not self then Zerotorescue@0: self = next(selfs) Zerotorescue@0: end Zerotorescue@0: lastCleaned = self Zerotorescue@0: if not self then -- should only happen if .selfs[] is empty Zerotorescue@0: return Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: -- Time to clean it out? Zerotorescue@0: local list = selfs[self] Zerotorescue@0: if (list.__ops or 0) < 250 then -- 250 slosh indices = ~10KB wasted (max!). For one 'self'. Zerotorescue@0: return Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: -- Create a new table and copy all members over Zerotorescue@0: local newlist = {} Zerotorescue@0: local n=0 Zerotorescue@0: for k,v in pairs(list) do Zerotorescue@0: newlist[k] = v Zerotorescue@0: n=n+1 Zerotorescue@0: end Zerotorescue@0: newlist.__ops = 0 -- Reset operation count Zerotorescue@0: Zerotorescue@0: -- And since we now have a count of the number of live timers, check that it's reasonable. Emit a warning if not. Zerotorescue@0: if n>BUCKETS then Zerotorescue@0: DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: The addon/module '"..tostring(self).."' has "..n.." live timers. Surely that's not intended?") Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: selfs[self] = newlist Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: -- --------------------------------------------------------------------- Zerotorescue@0: -- Embed handling Zerotorescue@0: Zerotorescue@0: AceTimer.embeds = AceTimer.embeds or {} Zerotorescue@0: Zerotorescue@0: local mixins = { Zerotorescue@0: "ScheduleTimer", "ScheduleRepeatingTimer", Zerotorescue@0: "CancelTimer", "CancelAllTimers", Zerotorescue@0: "TimeLeft" Zerotorescue@0: } Zerotorescue@0: Zerotorescue@0: function AceTimer:Embed(target) Zerotorescue@0: AceTimer.embeds[target] = true Zerotorescue@0: for _,v in pairs(mixins) do Zerotorescue@0: target[v] = AceTimer[v] Zerotorescue@0: end Zerotorescue@0: return target Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: -- AceTimer:OnEmbedDisable( target ) Zerotorescue@0: -- target (object) - target object that AceTimer is embedded in. Zerotorescue@0: -- Zerotorescue@0: -- cancel all timers registered for the object Zerotorescue@0: function AceTimer:OnEmbedDisable( target ) Zerotorescue@0: target:CancelAllTimers() Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: Zerotorescue@0: for addon in pairs(AceTimer.embeds) do Zerotorescue@0: AceTimer:Embed(addon) Zerotorescue@0: end Zerotorescue@0: Zerotorescue@0: -- --------------------------------------------------------------------- Zerotorescue@0: -- Debug tools (expose copies of internals to test suites) Zerotorescue@0: AceTimer.debug = AceTimer.debug or {} Zerotorescue@0: AceTimer.debug.HZ = HZ Zerotorescue@0: AceTimer.debug.BUCKETS = BUCKETS Zerotorescue@0: Zerotorescue@0: -- --------------------------------------------------------------------- Zerotorescue@0: -- Finishing touchups Zerotorescue@0: Zerotorescue@0: AceTimer.frame:SetScript("OnUpdate", OnUpdate) Zerotorescue@0: AceTimer.frame:SetScript("OnEvent", OnEvent) Zerotorescue@0: AceTimer.frame:RegisterEvent("PLAYER_REGEN_ENABLED") Zerotorescue@0: Zerotorescue@0: -- In theory, we should hide&show the frame based on there being timers or not. Zerotorescue@0: -- However, this job is fairly expensive, and the chance that there will Zerotorescue@0: -- actually be zero timers running is diminuitive to say the lest.