Mercurial > wow > itemauditor
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. |