From 5aa80e804543d85e70d48ccd83820a293eda2274 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Sat, 5 Sep 2026 15:33:46 +0200 Subject: [PATCH] Replace work queue with low-latency FIFO scheduler --- Core/Performance.lua | 187 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 Core/Performance.lua diff --git a/Core/Performance.lua b/Core/Performance.lua new file mode 100644 index 0000000..603c89a --- /dev/null +++ b/Core/Performance.lua @@ -0,0 +1,187 @@ +-- 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