---@class DBMCoreNamespace local private = select(2, ...) local twipe, tremove, unpack = table.wipe, table.remove, unpack local floor = math.floor local test = private:GetPrototype("DBMTest") local GetTime = GetTime local pairs, next = pairs, next local LastInstanceMapID = -1 local schedulerFrame = CreateFrame("Frame", "DBMScheduler") schedulerFrame:Hide() test:RegisterTimeWarpFrame(schedulerFrame) ---@class DBMScheduler: DBMModule local module = private:NewModule("DBMScheduler") -------------------------- -- OnUpdate/Scheduler -- -------------------------- -- stack that stores a few tables (up to 8) which will be recycled local popCachedTable, pushCachedTable local numChachedTables = 0 local scheduleTraceId = 0 do local tableCache -- gets a table from the stack, it will then be recycled. function popCachedTable() local t = tableCache if t then tableCache = t.next numChachedTables = numChachedTables - 1 end return t end -- tries to push a table on the stack -- only tables with <= 4 array entries are accepted as cached tables are only used for tasks with few arguments as we don't want to have big arrays wasting our precious memory space doing nothing... -- also, the maximum number of cached tables is limited to 8 as DBM rarely has more than eight scheduled tasks with less than 4 arguments at the same time -- this is just to re-use all the tables of the small tasks that are scheduled all the time (like the wipe detection) -- note that the cache does not use weak references anywhere for performance reasons, so a cached table will never be deleted by the garbage collector function pushCachedTable(t) if numChachedTables < 8 and #t <= 4 then twipe(t) t.next = tableCache tableCache = t numChachedTables = numChachedTables + 1 end end end -- priority queue (min-heap) that stores all scheduled tasks. -- insert: O(log n) -- deleteMin: O(log n) -- getMin: O(1) -- removeAllMatching: O(n) local insert, removeAllMatching, getMin, deleteMin do local heap = {} local firstFree = 1 -- gets the next task function getMin() return heap[1] end -- restores the heap invariant by moving an item up local function siftUp(n) local parent = floor(n / 2) while n > 1 and heap[parent].time > heap[n].time do -- move the element up until the heap invariant is restored, meaning the element is at the top or the element's parent is <= the element heap[n], heap[parent] = heap[parent], heap[n] -- swap the element with its parent n = parent parent = floor(n / 2) end end -- restores the heap invariant by moving an item down local function siftDown(n) local m -- position of the smaller child while 2 * n < firstFree do -- #children >= 1 -- swap the element with its smaller child if 2 * n + 1 == firstFree then -- n does not have a right child --> it only has a left child as #children >= 1 m = 2 * n -- left child elseif heap[2 * n].time < heap[2 * n + 1].time then -- #children = 2 and left child < right child m = 2 * n -- left child else -- #children = 2 and right child is smaller than the left one m = 2 * n + 1 -- right end if heap[n].time <= heap[m].time then -- n is <= its smallest child --> heap invariant restored return end heap[n], heap[m] = heap[m], heap[n] n = m end end -- inserts a new element into the heap function insert(ele) heap[firstFree] = ele siftUp(firstFree) firstFree = firstFree + 1 end -- deletes the min element function deleteMin() local min = heap[1] firstFree = firstFree - 1 heap[1] = heap[firstFree] heap[firstFree] = nil siftDown(1) return min end -- removes multiple scheduled tasks from the heap -- note that this function is comparatively slow by design as it has to check all tasks and allows partial matches function removeAllMatching(f, mod, ...) -- remove all elements that match the signature, this destroyes the heap and leaves a normal array local v, match local foundMatch = false for i = #heap, 1, -1 do -- iterate backwards over the array to allow usage of table.remove v = heap[i] if (not f or v.func == f) and (not mod or v.mod == mod) then match = true for j = 1, select("#", ...) do if select(j, ...) ~= v[j] then match = false break end end if match then tremove(heap, i) firstFree = firstFree - 1 foundMatch = true if v.traceId then test:Trace(v.mod, "UnscheduleTask", v.traceId, unpack(v, 1, v.n)) end end end end -- rebuild the heap from the array in O(n) if foundMatch then for i = floor((firstFree - 1) / 2), 1, -1 do siftDown(i) end end end end local nextModSyncSpamUpdate = 0 --mainFrame:SetScript("OnUpdate", function(self, elapsed) local function onUpdate(self, elapsed) local time = GetTime() -- execute scheduled tasks local nextTask = getMin() while nextTask and nextTask.func and nextTask.time <= time do deleteMin() if nextTask.traceId then test:Trace(nextTask.mod, "ExecuteScheduledTaskPre", nextTask.traceId, unpack(nextTask, 1, nextTask.n)) end nextTask.func(unpack(nextTask, 1, nextTask.n)) if nextTask.traceId then test:Trace(nextTask.mod, "ExecuteScheduledTaskPost", nextTask.traceId, unpack(nextTask, 1, nextTask.n)) end pushCachedTable(nextTask) nextTask = getMin() end -- execute OnUpdate handlers of all modules local foundModFunctions = 0 for i, v in pairs(private.updateFunctions) do foundModFunctions = foundModFunctions + 1 if i.Options.Enabled and (not i.zones or i.zones[LastInstanceMapID]) then i.elapsed = (i.elapsed or 0) + elapsed if i.elapsed >= (i.updateInterval or 0) then v(i, i.elapsed) i.elapsed = 0 end end end -- clean up sync spam timers and auto respond spam blockers if time > nextModSyncSpamUpdate then nextModSyncSpamUpdate = time + 20 -- TODO: optimize this; using next(t, k) all the time on nearly empty hash tables is not a good idea...doesn't really matter here as modSyncSpam only very rarely contains more than 4 entries... -- we now do this just every 20 seconds since the earlier assumption about modSyncSpam isn't true any longer -- note that not removing entries at all would be just a small memory leak and not a problem (the sync functions themselves check the timestamp) local k, v = next(private.modSyncSpam, nil) if v and (time - v > 8) then private.modSyncSpam[k] = nil end end if not nextTask and foundModFunctions == 0 then--Nothing left, stop scheduler schedulerFrame:SetScript("OnUpdate", nil) schedulerFrame:Hide() end end function module:StartScheduler() if not schedulerFrame:IsShown() then schedulerFrame:Show() schedulerFrame:SetScript("OnUpdate", onUpdate) end end --For updating zone cache locally --without needing to monitor for changes in onupdate functions or registering zone change events function module:UpdateZone() LastInstanceMapID = DBM and DBM:GetCurrentArea() or -1 end local function schedule(t, f, mod, ...) if type(f) ~= "function" then error("usage: schedule(time, func, [mod, args...])", 2) end module:StartScheduler() local v if numChachedTables > 0 and select("#", ...) <= 4 then -- a cached table is available and all arguments fit into an array with four slots v = popCachedTable() v.time = GetTime() + t v.func = f v.mod = mod v.n = select("#", ...) for i = 1, v.n do v[i] = select(i, ...) end -- clear slots if necessary for i = v.n + 1, 4 do v[i] = nil end else -- create a new table v = {time = GetTime() + t, func = f, mod = mod, n = select("#", ...), ...} end if test.testRunning then scheduleTraceId = scheduleTraceId + 1 v.traceId = scheduleTraceId test:Trace(mod, "ScheduleTask", scheduleTraceId, t, f, ...) end insert(v) return test.testRunning and scheduleTraceId or nil end --Boss mod prototype usage methods (for announces countdowns and yell scheduling function module:ScheduleCountdown(time, numAnnounces, func, mod, prototype, ...) time = time or 5 numAnnounces = numAnnounces or 3 for i = 1, numAnnounces do --In event time is < numbmer of announces (ie 2 second time, with 3 announces) local validTime = time - i if validTime >= 1 then local id = schedule(validTime, func, mod, prototype, i, ...) test:Trace(mod, "SetScheduleMethodName", id, prototype, "ScheduleCountdown", i, ...) end end end local function unschedule(f, mod, ...) if not f and not mod then --Dangerous to use, should only ever happen with a force disable removeAllMatching() end return removeAllMatching(f, mod, ...) end function module:Schedule(t, f, mod, ...) if type(f) ~= "function" then error("usage: DBM:Schedule(time, func, [args...])", 2) end return schedule(t, f, mod, ...) end function module:Unschedule(f, mod, ...) return unschedule(f, mod, ...) end -- Expose locals for testing. test:RegisterLocalHook("GetTime", function(val) local old = GetTime GetTime = val return old end)