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