188 lines
5.4 KiB
Lua
188 lines
5.4 KiB
Lua
-- Guda low-latency work queue
|
|
-- Replaces Utils' original table.remove(1) queue with an O(1) FIFO and uses
|
|
-- a much smaller frame budget suitable for WoW 1.12.1.
|
|
|
|
local addon = Guda
|
|
local Utils = addon.Modules.Utils
|
|
if not Utils then return end
|
|
|
|
local FRAME_BUDGET_SECONDS = 0.003 -- 3 ms target budget per frame
|
|
local MAX_WORK_PER_FRAME = 8
|
|
|
|
local callbacks = {}
|
|
local contexts = {}
|
|
local queueHead = 1
|
|
local queueTail = 0
|
|
local workQueueFrame = nil
|
|
local workQueuePaused = false
|
|
local lastEntryTime = 0
|
|
|
|
local performanceStats = {
|
|
budgetExceededCount = 0,
|
|
totalUpdates = 0,
|
|
lastUpdateDuration = 0,
|
|
averageUpdateDuration = 0,
|
|
}
|
|
|
|
local function QueueSize()
|
|
if queueTail < queueHead then return 0 end
|
|
return queueTail - queueHead + 1
|
|
end
|
|
|
|
local function ResetQueueStorage()
|
|
callbacks = {}
|
|
contexts = {}
|
|
queueHead = 1
|
|
queueTail = 0
|
|
end
|
|
|
|
function Utils:ReportEntry()
|
|
lastEntryTime = GetTime()
|
|
end
|
|
|
|
function Utils:CheckTimeout()
|
|
return (GetTime() - lastEntryTime) >= FRAME_BUDGET_SECONDS
|
|
end
|
|
|
|
local function EnsureWorkQueueFrame()
|
|
if workQueueFrame then return end
|
|
|
|
workQueueFrame = CreateFrame("Frame", "Guda_WorkQueueFrame_ClassicAPI", UIParent)
|
|
workQueueFrame:Hide()
|
|
|
|
workQueueFrame:SetScript("OnUpdate", function()
|
|
Utils:ReportEntry()
|
|
|
|
local processedCount = 0
|
|
while queueHead <= queueTail and processedCount < MAX_WORK_PER_FRAME do
|
|
-- Always allow at least one unit of work to make forward progress.
|
|
if processedCount > 0 and Utils:CheckTimeout() then
|
|
performanceStats.budgetExceededCount =
|
|
performanceStats.budgetExceededCount + 1
|
|
break
|
|
end
|
|
|
|
local callback = callbacks[queueHead]
|
|
local context = contexts[queueHead]
|
|
callbacks[queueHead] = nil
|
|
contexts[queueHead] = nil
|
|
queueHead = queueHead + 1
|
|
|
|
if callback then
|
|
local success, err = pcall(callback)
|
|
if not success then
|
|
addon:Error("QueueWork callback error [%s]: %s",
|
|
tostring(context or "unknown"), tostring(err))
|
|
end
|
|
end
|
|
|
|
processedCount = processedCount + 1
|
|
end
|
|
|
|
local duration = GetTime() - lastEntryTime
|
|
performanceStats.lastUpdateDuration = duration
|
|
performanceStats.totalUpdates = performanceStats.totalUpdates + 1
|
|
performanceStats.averageUpdateDuration =
|
|
performanceStats.averageUpdateDuration * 0.9 + duration * 0.1
|
|
|
|
if queueHead > queueTail then
|
|
ResetQueueStorage()
|
|
this:Hide()
|
|
end
|
|
end)
|
|
end
|
|
|
|
function Utils:QueueWork(callback, context)
|
|
if type(callback) ~= "function" then
|
|
addon:Error("QueueWork: callback must be a function")
|
|
return
|
|
end
|
|
|
|
queueTail = queueTail + 1
|
|
callbacks[queueTail] = callback
|
|
contexts[queueTail] = context or "unknown"
|
|
|
|
EnsureWorkQueueFrame()
|
|
if not workQueuePaused then
|
|
workQueueFrame:Show()
|
|
end
|
|
end
|
|
|
|
function Utils:ClearWorkQueue()
|
|
ResetQueueStorage()
|
|
if workQueueFrame then
|
|
workQueueFrame:Hide()
|
|
end
|
|
end
|
|
|
|
function Utils:PauseWorkQueue()
|
|
workQueuePaused = true
|
|
if workQueueFrame then
|
|
workQueueFrame:Hide()
|
|
end
|
|
end
|
|
|
|
function Utils:ResumeWorkQueue()
|
|
workQueuePaused = false
|
|
if QueueSize() > 0 then
|
|
EnsureWorkQueueFrame()
|
|
workQueueFrame:Show()
|
|
end
|
|
end
|
|
|
|
function Utils:GetWorkQueueSize()
|
|
return QueueSize()
|
|
end
|
|
|
|
function Utils:GetFrameBudget()
|
|
return FRAME_BUDGET_SECONDS
|
|
end
|
|
|
|
function Utils:SetFrameBudget(seconds)
|
|
if type(seconds) ~= "number" then return end
|
|
FRAME_BUDGET_SECONDS = math.max(0.001, math.min(0.05, seconds))
|
|
addon:Debug("Frame budget set to %.3f seconds", FRAME_BUDGET_SECONDS)
|
|
end
|
|
|
|
function Utils:RecordUpdateEnd()
|
|
local duration = GetTime() - lastEntryTime
|
|
performanceStats.lastUpdateDuration = duration
|
|
performanceStats.totalUpdates = performanceStats.totalUpdates + 1
|
|
performanceStats.averageUpdateDuration =
|
|
performanceStats.averageUpdateDuration * 0.9 + duration * 0.1
|
|
|
|
if duration > FRAME_BUDGET_SECONDS then
|
|
performanceStats.budgetExceededCount =
|
|
performanceStats.budgetExceededCount + 1
|
|
end
|
|
end
|
|
|
|
function Utils:GetPerformanceStats()
|
|
return {
|
|
frameBudget = FRAME_BUDGET_SECONDS,
|
|
lastUpdateDuration = performanceStats.lastUpdateDuration,
|
|
averageUpdateDuration = performanceStats.averageUpdateDuration,
|
|
totalUpdates = performanceStats.totalUpdates,
|
|
budgetExceededCount = performanceStats.budgetExceededCount,
|
|
workQueueSize = QueueSize(),
|
|
}
|
|
end
|
|
|
|
function Utils:ResetPerformanceStats()
|
|
performanceStats.budgetExceededCount = 0
|
|
performanceStats.totalUpdates = 0
|
|
performanceStats.lastUpdateDuration = 0
|
|
performanceStats.averageUpdateDuration = 0
|
|
end
|
|
|
|
function Utils:PrintPerformanceStats()
|
|
local stats = self:GetPerformanceStats()
|
|
addon:Print(Guda_L["=== Guda Performance Stats ==="])
|
|
addon:Print(Guda_L["Frame Budget: %.0fms"], stats.frameBudget * 1000)
|
|
addon:Print(Guda_L["Last Update: %.1fms"], stats.lastUpdateDuration * 1000)
|
|
addon:Print("Avg Update: %.1fms", stats.averageUpdateDuration * 1000)
|
|
addon:Print(Guda_L["Total Updates: %d"], stats.totalUpdates)
|
|
addon:Print(Guda_L["Budget Exceeded: %d times"], stats.budgetExceededCount)
|
|
addon:Print("Work Queue: %d items", stats.workQueueSize)
|
|
end
|