comparison Libs/AceTimer-3.0/AceTimer-3.0.lua @ 0:169f5211fc7f

First public revision. At this point ItemAuditor watches mail for auctions sold or purchased, watches for buy/sell (money and 1 item type change) and conversions/tradeskills. Milling isn't working yet because there is too much time between the first event and the last event.
author Asa Ayers <Asa.Ayers@Gmail.com>
date Thu, 20 May 2010 19:22:19 -0700
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:169f5211fc7f
1 --- **AceTimer-3.0** provides a central facility for registering timers.
2 -- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient
3 -- data structure that allows easy dispatching and fast rescheduling. Timers can be registered, rescheduled
4 -- or canceled at any time, even from within a running timer, without conflict or large overhead.\\
5 -- AceTimer is currently limited to firing timers at a frequency of 0.1s. This constant may change
6 -- in the future, but for now it seemed like a good compromise in efficiency and accuracy.
7 --
8 -- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you
9 -- need to cancel or reschedule the timer you just registered.
10 --
11 -- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by
12 -- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
13 -- and can be accessed directly, without having to explicitly call AceTimer itself.\\
14 -- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you
15 -- make into AceTimer.
16 -- @class file
17 -- @name AceTimer-3.0
18 -- @release $Id: AceTimer-3.0.lua 895 2009-12-06 16:28:55Z nevcairiel $
19
20 --[[
21 Basic assumptions:
22 * In a typical system, we do more re-scheduling per second than there are timer pulses per second
23 * Regardless of timer implementation, we cannot guarantee timely delivery due to FPS restriction (may be as low as 10)
24
25 This implementation:
26 CON: The smallest timer interval is constrained by HZ (currently 1/10s).
27 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
28 PRO: In lag bursts, the system simly skips missed timer intervals to decrease load
29 CON: Algorithms depending on a timer firing "N times per minute" will fail
30 PRO: (Re-)scheduling is O(1) with a VERY small constant. It's a simple linked list insertion in a hash bucket.
31 CAUTION: The BUCKETS constant constrains how many timers can be efficiently handled. With too many hash collisions, performance will decrease.
32
33 Major assumptions upheld:
34 - ALLOWS scheduling multiple timers with the same funcref/method
35 - ALLOWS scheduling more timers during OnUpdate processing
36 - ALLOWS unscheduling ANY timer (including the current running one) at any time, including during OnUpdate processing
37 ]]
38
39 local MAJOR, MINOR = "AceTimer-3.0", 5
40 local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
41
42 if not AceTimer then return end -- No upgrade needed
43
44 AceTimer.hash = AceTimer.hash or {} -- Array of [0..BUCKET-1] = linked list of timers (using .next member)
45 -- Linked list gets around ACE-88 and ACE-90.
46 AceTimer.selfs = AceTimer.selfs or {} -- Array of [self]={[handle]=timerobj, [handle2]=timerobj2, ...}
47 AceTimer.frame = AceTimer.frame or CreateFrame("Frame", "AceTimer30Frame")
48
49 -- Lua APIs
50 local assert, error, loadstring = assert, error, loadstring
51 local setmetatable, rawset, rawget = setmetatable, rawset, rawget
52 local select, pairs, type, next, tostring = select, pairs, type, next, tostring
53 local floor, max, min = math.floor, math.max, math.min
54 local tconcat = table.concat
55
56 -- WoW APIs
57 local GetTime = GetTime
58
59 -- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
60 -- List them here for Mikk's FindGlobals script
61 -- GLOBALS: DEFAULT_CHAT_FRAME, geterrorhandler
62
63 -- Simple ONE-SHOT timer cache. Much more efficient than a full compost for our purposes.
64 local timerCache = nil
65
66 --[[
67 Timers will not be fired more often than HZ-1 times per second.
68 Keep at intended speed PLUS ONE or we get bitten by floating point rounding errors (n.5 + 0.1 can be n.599999)
69 If this is ever LOWERED, all existing timers need to be enforced to have a delay >= 1/HZ on lib upgrade.
70 If this number is ever changed, all entries need to be rehashed on lib upgrade.
71 ]]
72 local HZ = 11
73
74 --[[
75 Prime for good distribution
76 If this number is ever changed, all entries need to be rehashed on lib upgrade.
77 ]]
78 local BUCKETS = 131
79
80 local hash = AceTimer.hash
81 for i=1,BUCKETS do
82 hash[i] = hash[i] or false -- make it an integer-indexed array; it's faster than hashes
83 end
84
85 --[[
86 xpcall safecall implementation
87 ]]
88 local xpcall = xpcall
89
90 local function errorhandler(err)
91 return geterrorhandler()(err)
92 end
93
94 local function CreateDispatcher(argCount)
95 local code = [[
96 local xpcall, eh = ... -- our arguments are received as unnamed values in "..." since we don't have a proper function declaration
97 local method, ARGS
98 local function call() return method(ARGS) end
99
100 local function dispatch(func, ...)
101 method = func
102 if not method then return end
103 ARGS = ...
104 return xpcall(call, eh)
105 end
106
107 return dispatch
108 ]]
109
110 local ARGS = {}
111 for i = 1, argCount do ARGS[i] = "arg"..i end
112 code = code:gsub("ARGS", tconcat(ARGS, ", "))
113 return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(xpcall, errorhandler)
114 end
115
116 local Dispatchers = setmetatable({}, {
117 __index=function(self, argCount)
118 local dispatcher = CreateDispatcher(argCount)
119 rawset(self, argCount, dispatcher)
120 return dispatcher
121 end
122 })
123 Dispatchers[0] = function(func)
124 return xpcall(func, errorhandler)
125 end
126
127 local function safecall(func, ...)
128 return Dispatchers[select('#', ...)](func, ...)
129 end
130
131 local lastint = floor(GetTime() * HZ)
132
133 -- --------------------------------------------------------------------
134 -- OnUpdate handler
135 --
136 -- traverse buckets, always chasing "now", and fire timers that have expired
137
138 local function OnUpdate()
139 local now = GetTime()
140 local nowint = floor(now * HZ)
141
142 -- Have we passed into a new hash bucket?
143 if nowint == lastint then return end
144
145 local soon = now + 1 -- +1 is safe as long as 1 < HZ < BUCKETS/2
146
147 -- Pass through each bucket at most once
148 -- Happens on e.g. instance loads, but COULD happen on high local load situations also
149 for curint = (max(lastint, nowint - BUCKETS) + 1), nowint do -- loop until we catch up with "now", usually only 1 iteration
150 local curbucket = (curint % BUCKETS)+1
151 -- Yank the list of timers out of the bucket and empty it. This allows reinsertion in the currently-processed bucket from callbacks.
152 local nexttimer = hash[curbucket]
153 hash[curbucket] = false -- false rather than nil to prevent the array from becoming a hash
154
155 while nexttimer do
156 local timer = nexttimer
157 nexttimer = timer.next
158 local when = timer.when
159
160 if when < soon then
161 -- Call the timer func, either as a method on given object, or a straight function ref
162 local callback = timer.callback
163 if type(callback) == "string" then
164 safecall(timer.object[callback], timer.object, timer.arg)
165 elseif callback then
166 safecall(callback, timer.arg)
167 else
168 -- probably nilled out by CancelTimer
169 timer.delay = nil -- don't reschedule it
170 end
171
172 local delay = timer.delay -- NOW make a local copy, can't do it earlier in case the timer cancelled itself in the callback
173
174 if not delay then
175 -- single-shot timer (or cancelled)
176 AceTimer.selfs[timer.object][tostring(timer)] = nil
177 timerCache = timer
178 else
179 -- repeating timer
180 local newtime = when + delay
181 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.)
182 newtime = now + delay
183 end
184 timer.when = newtime
185
186 -- add next timer execution to the correct bucket
187 local bucket = (floor(newtime * HZ) % BUCKETS) + 1
188 timer.next = hash[bucket]
189 hash[bucket] = timer
190 end
191 else -- if when>=soon
192 -- reinsert (yeah, somewhat expensive, but shouldn't be happening too often either due to hash distribution)
193 timer.next = hash[curbucket]
194 hash[curbucket] = timer
195 end -- if when<soon ... else
196 end -- while nexttimer do
197 end -- for curint=lastint,nowint
198
199 lastint = nowint
200 end
201
202 -- ---------------------------------------------------------------------
203 -- Reg( callback, delay, arg, repeating )
204 --
205 -- callback( function or string ) - direct function ref or method name in our object for the callback
206 -- delay(int) - delay for the timer
207 -- arg(variant) - any argument to be passed to the callback function
208 -- repeating(boolean) - repeating timer, or oneshot
209 --
210 -- returns the handle of the timer for later processing (canceling etc)
211 local function Reg(self, callback, delay, arg, repeating)
212 if type(callback) ~= "string" and type(callback) ~= "function" then
213 local error_origin = repeating and "ScheduleRepeatingTimer" or "ScheduleTimer"
214 error(MAJOR..": " .. error_origin .. "(callback, delay, arg): 'callback' - function or method name expected.", 3)
215 end
216 if type(callback) == "string" then
217 if type(self)~="table" then
218 local error_origin = repeating and "ScheduleRepeatingTimer" or "ScheduleTimer"
219 error(MAJOR..": " .. error_origin .. "(\"methodName\", delay, arg): 'self' - must be a table.", 3)
220 end
221 if type(self[callback]) ~= "function" then
222 local error_origin = repeating and "ScheduleRepeatingTimer" or "ScheduleTimer"
223 error(MAJOR..": " .. error_origin .. "(\"methodName\", delay, arg): 'methodName' - method not found on target object.", 3)
224 end
225 end
226
227 if delay < (1 / (HZ - 1)) then
228 delay = 1 / (HZ - 1)
229 end
230
231 -- Create and stuff timer in the correct hash bucket
232 local now = GetTime()
233
234 local timer = timerCache or {} -- Get new timer object (from cache if available)
235 timerCache = nil
236
237 timer.object = self
238 timer.callback = callback
239 timer.delay = (repeating and delay)
240 timer.arg = arg
241 timer.when = now + delay
242
243 local bucket = (floor((now+delay)*HZ) % BUCKETS) + 1
244 timer.next = hash[bucket]
245 hash[bucket] = timer
246
247 -- Insert timer in our self->handle->timer registry
248 local handle = tostring(timer)
249
250 local selftimers = AceTimer.selfs[self]
251 if not selftimers then
252 selftimers = {}
253 AceTimer.selfs[self] = selftimers
254 end
255 selftimers[handle] = timer
256 selftimers.__ops = (selftimers.__ops or 0) + 1
257
258 return handle
259 end
260
261 --- Schedule a new one-shot timer.
262 -- The timer will fire once in `delay` seconds, unless canceled before.
263 -- @param callback Callback function for the timer pulse (funcref or method name).
264 -- @param delay Delay for the timer, in seconds.
265 -- @param arg An optional argument to be passed to the callback function.
266 -- @usage
267 -- MyAddon = LibStub("AceAddon-3.0"):NewAddon("TimerTest", "AceTimer-3.0")
268 --
269 -- function MyAddon:OnEnable()
270 -- self:ScheduleTimer("TimerFeedback", 5)
271 -- end
272 --
273 -- function MyAddon:TimerFeedback()
274 -- print("5 seconds passed")
275 -- end
276 function AceTimer:ScheduleTimer(callback, delay, arg)
277 return Reg(self, callback, delay, arg)
278 end
279
280 --- Schedule a repeating timer.
281 -- The timer will fire every `delay` seconds, until canceled.
282 -- @param callback Callback function for the timer pulse (funcref or method name).
283 -- @param delay Delay for the timer, in seconds.
284 -- @param arg An optional argument to be passed to the callback function.
285 -- @usage
286 -- MyAddon = LibStub("AceAddon-3.0"):NewAddon("TimerTest", "AceTimer-3.0")
287 --
288 -- function MyAddon:OnEnable()
289 -- self.timerCount = 0
290 -- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5)
291 -- end
292 --
293 -- function MyAddon:TimerFeedback()
294 -- self.timerCount = self.timerCount + 1
295 -- print(("%d seconds passed"):format(5 * self.timerCount))
296 -- -- run 30 seconds in total
297 -- if self.timerCount == 6 then
298 -- self:CancelTimer(self.testTimer)
299 -- end
300 -- end
301 function AceTimer:ScheduleRepeatingTimer(callback, delay, arg)
302 return Reg(self, callback, delay, arg, true)
303 end
304
305 --- Cancels a timer with the given handle, registered by the same addon object as used for `:ScheduleTimer`
306 -- Both one-shot and repeating timers can be canceled with this function, as long as the `handle` is valid
307 -- and the timer has not fired yet or was canceled before.
308 -- @param handle The handle of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
309 -- @param silent If true, no error is raised if the timer handle is invalid (expired or already canceled)
310 -- @return True if the timer was successfully cancelled.
311 function AceTimer:CancelTimer(handle, silent)
312 if not handle then return end -- nil handle -> bail out without erroring
313 if type(handle) ~= "string" then
314 error(MAJOR..": CancelTimer(handle): 'handle' - expected a string", 2) -- for now, anyway
315 end
316 local selftimers = AceTimer.selfs[self]
317 local timer = selftimers and selftimers[handle]
318 if silent then
319 if timer then
320 timer.callback = nil -- don't run it again
321 timer.delay = nil -- if this is the currently-executing one: don't even reschedule
322 -- The timer object is removed in the OnUpdate loop
323 end
324 return not not timer -- might return "true" even if we double-cancel. we'll live.
325 else
326 if not timer then
327 geterrorhandler()(MAJOR..": CancelTimer(handle[, silent]): '"..tostring(handle).."' - no such timer registered")
328 return false
329 end
330 if not timer.callback then
331 geterrorhandler()(MAJOR..": CancelTimer(handle[, silent]): '"..tostring(handle).."' - timer already cancelled or expired")
332 return false
333 end
334 timer.callback = nil -- don't run it again
335 timer.delay = nil -- if this is the currently-executing one: don't even reschedule
336 return true
337 end
338 end
339
340 --- Cancels all timers registered to the current addon object ('self')
341 function AceTimer:CancelAllTimers()
342 if not(type(self) == "string" or type(self) == "table") then
343 error(MAJOR..": CancelAllTimers(): 'self' - must be a string or a table",2)
344 end
345 if self == AceTimer then
346 error(MAJOR..": CancelAllTimers(): supply a meaningful 'self'", 2)
347 end
348
349 local selftimers = AceTimer.selfs[self]
350 if selftimers then
351 for handle,v in pairs(selftimers) do
352 if type(v) == "table" then -- avoid __ops, etc
353 AceTimer.CancelTimer(self, handle, true)
354 end
355 end
356 end
357 end
358
359 --- Returns the time left for a timer with the given handle, registered by the current addon object ('self').
360 -- This function will raise a warning when the handle is invalid, but not stop execution.
361 -- @param handle The handle of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
362 -- @return The time left on the timer, or false if the handle is invalid.
363 function AceTimer:TimeLeft(handle)
364 if not handle then return end
365 if type(handle) ~= "string" then
366 error(MAJOR..": TimeLeft(handle): 'handle' - expected a string", 2) -- for now, anyway
367 end
368 local selftimers = AceTimer.selfs[self]
369 local timer = selftimers and selftimers[handle]
370 if not timer then
371 geterrorhandler()(MAJOR..": TimeLeft(handle): '"..tostring(handle).."' - no such timer registered")
372 return false
373 end
374 return timer.when - GetTime()
375 end
376
377
378 -- ---------------------------------------------------------------------
379 -- PLAYER_REGEN_ENABLED: Run through our .selfs[] array step by step
380 -- and clean it out - otherwise the table indices can grow indefinitely
381 -- if an addon starts and stops a lot of timers. AceBucket does this!
382 --
383 -- See ACE-94 and tests/AceTimer-3.0-ACE-94.lua
384
385 local lastCleaned = nil
386
387 local function OnEvent(this, event)
388 if event~="PLAYER_REGEN_ENABLED" then
389 return
390 end
391
392 -- Get the next 'self' to process
393 local selfs = AceTimer.selfs
394 local self = next(selfs, lastCleaned)
395 if not self then
396 self = next(selfs)
397 end
398 lastCleaned = self
399 if not self then -- should only happen if .selfs[] is empty
400 return
401 end
402
403 -- Time to clean it out?
404 local list = selfs[self]
405 if (list.__ops or 0) < 250 then -- 250 slosh indices = ~10KB wasted (max!). For one 'self'.
406 return
407 end
408
409 -- Create a new table and copy all members over
410 local newlist = {}
411 local n=0
412 for k,v in pairs(list) do
413 newlist[k] = v
414 n=n+1
415 end
416 newlist.__ops = 0 -- Reset operation count
417
418 -- And since we now have a count of the number of live timers, check that it's reasonable. Emit a warning if not.
419 if n>BUCKETS then
420 DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: The addon/module '"..tostring(self).."' has "..n.." live timers. Surely that's not intended?")
421 end
422
423 selfs[self] = newlist
424 end
425
426 -- ---------------------------------------------------------------------
427 -- Embed handling
428
429 AceTimer.embeds = AceTimer.embeds or {}
430
431 local mixins = {
432 "ScheduleTimer", "ScheduleRepeatingTimer",
433 "CancelTimer", "CancelAllTimers",
434 "TimeLeft"
435 }
436
437 function AceTimer:Embed(target)
438 AceTimer.embeds[target] = true
439 for _,v in pairs(mixins) do
440 target[v] = AceTimer[v]
441 end
442 return target
443 end
444
445 -- AceTimer:OnEmbedDisable( target )
446 -- target (object) - target object that AceTimer is embedded in.
447 --
448 -- cancel all timers registered for the object
449 function AceTimer:OnEmbedDisable( target )
450 target:CancelAllTimers()
451 end
452
453
454 for addon in pairs(AceTimer.embeds) do
455 AceTimer:Embed(addon)
456 end
457
458 -- ---------------------------------------------------------------------
459 -- Debug tools (expose copies of internals to test suites)
460 AceTimer.debug = AceTimer.debug or {}
461 AceTimer.debug.HZ = HZ
462 AceTimer.debug.BUCKETS = BUCKETS
463
464 -- ---------------------------------------------------------------------
465 -- Finishing touchups
466
467 AceTimer.frame:SetScript("OnUpdate", OnUpdate)
468 AceTimer.frame:SetScript("OnEvent", OnEvent)
469 AceTimer.frame:RegisterEvent("PLAYER_REGEN_ENABLED")
470
471 -- In theory, we should hide&show the frame based on there being timers or not.
472 -- However, this job is fairly expensive, and the chance that there will
473 -- actually be zero timers running is diminuitive to say the lest.