From f9628b7d029864c47abdd38c03bfcb6f0e16b18d Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Sat, 24 Jan 2026 15:17:50 +0400 Subject: [PATCH] fix: memory issue --- Core/Main.lua | 22 +++++++ UI/BagFrame.lua | 67 ++++++++------------ UI/ItemButton.lua | 153 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 192 insertions(+), 50 deletions(-) diff --git a/Core/Main.lua b/Core/Main.lua index d724770..8cfa4a6 100644 --- a/Core/Main.lua +++ b/Core/Main.lua @@ -194,6 +194,12 @@ function Main:SetupSlashCommands() addon:Print("ItemDetection Cache: %d hits, %d misses (%.1f%% hit rate, %d items)", stats.hits, stats.misses, stats.hitRate, stats.size) end + -- Show button pool stats + if Guda_GetButtonPoolStats then + local stats = Guda_GetButtonPoolStats() + addon:Print("Button Pool: %d total (%d shown, %d hidden, %d inUse, %d available, max %d)", + stats.total, stats.shown, stats.hidden, stats.inUse, stats.available, stats.maxSize) + end elseif msg == "perfreset" then -- Reset performance statistics @@ -214,6 +220,21 @@ function Main:SetupSlashCommands() addon.Modules.ItemDetection:ClearCache() end + elseif msg == "poolreset" then + -- Reset button pool (only safe when no frames are visible) + local bagFrame = getglobal("Guda_BagFrame") + local bankFrame = getglobal("Guda_BankFrame") + if (bagFrame and bagFrame:IsShown()) or (bankFrame and bankFrame:IsShown()) then + addon:Print("Cannot reset pool while bag/bank frames are open. Close them first.") + else + if Guda_ResetButtonPool then + Guda_ResetButtonPool() + addon:Print("Button pool reset. Pool is now empty.") + else + addon:Print("Button pool reset function not available.") + end + end + elseif msg == "help" then -- Show help addon:Print("Commands:") @@ -229,6 +250,7 @@ function Main:SetupSlashCommands() addon:Print("/guda cleanup - Remove old characters") addon:Print("/guda perf - Show performance stats") addon:Print("/guda perfreset - Reset performance stats") + addon:Print("/guda poolreset - Reset button pool (debug)") else addon:Print("Unknown command. Type /guda help for commands") diff --git a/UI/BagFrame.lua b/UI/BagFrame.lua index af86edf..3c17b9b 100644 --- a/UI/BagFrame.lua +++ b/UI/BagFrame.lua @@ -144,10 +144,10 @@ function Guda_BagFrame_OnHide(self) end -- Clean up all buttons when frame is hidden (safe since we're not displaying) + -- Use itemButtons hash instead of GetChildren() to avoid table allocation for _, bagParent in pairs(bagParents) do - if bagParent then - local buttons = { bagParent:GetChildren() } - for _, button in ipairs(buttons) do + if bagParent and bagParent.itemButtons then + for button in pairs(bagParent.itemButtons) do if button.hasItem ~= nil then button:Hide() button:ClearAllPoints() @@ -434,11 +434,11 @@ function BagFrame:Update() -- BUT only if we already have items displayed - otherwise we need to do initial build if CursorHasItem and CursorHasItem() then -- Check if we have any displayed items + -- Use itemButtons hash instead of GetChildren() to avoid table allocation local hasDisplayedItems = false for _, bagParent in pairs(bagParents) do - if bagParent then - local children = { bagParent:GetChildren() } - for _, button in ipairs(children) do + if bagParent and bagParent.itemButtons then + for button in pairs(bagParent.itemButtons) do if button.hasItem and button:IsShown() then hasDisplayedItems = true break @@ -456,12 +456,12 @@ function BagFrame:Update() end -- Mark all existing buttons as not in use (we'll mark active ones during display) + -- Use itemButtons hash instead of GetChildren() to avoid table allocation local totalButtonsBefore = 0 local shownButtonsBefore = 0 for _, bagParent in pairs(bagParents) do - if bagParent then - local buttons = { bagParent:GetChildren() } - for _, button in ipairs(buttons) do + if bagParent and bagParent.itemButtons then + for button in pairs(bagParent.itemButtons) do if button.hasItem ~= nil then totalButtonsBefore = totalButtonsBefore + 1 if button:IsShown() then @@ -556,12 +556,12 @@ function BagFrame:Update() self:UpdateBaglineLayout() -- Clean up unused buttons AFTER display is complete (prevents drag/drop issues) + -- Use itemButtons hash instead of GetChildren() to avoid table allocation local hiddenCount = 0 local stillShownCount = 0 for _, bagParent in pairs(bagParents) do - if bagParent then - local buttons = { bagParent:GetChildren() } - for _, button in ipairs(buttons) do + if bagParent and bagParent.itemButtons then + for button in pairs(bagParent.itemButtons) do if button.hasItem ~= nil and not button.inUse then button:Hide() button:ClearAllPoints() @@ -2543,13 +2543,12 @@ end -- Highlight all item slots belonging to a specific bag by dimming others function Guda_BagFrame_HighlightBagSlots(bagID) - -- Iterate through bagParents to get all item buttons (same approach as BankFrame) + -- Use itemButtons hash instead of GetChildren() to avoid table allocation local highlightCount, dimCount = 0, 0 for _, bagParent in pairs(bagParents) do - if bagParent then - local children = { bagParent:GetChildren() } - for _, button in ipairs(children) do + if bagParent and bagParent.itemButtons then + for button in pairs(bagParent.itemButtons) do if button and button:IsShown() and button.hasItem ~= nil and not button.isBagSlot then if button.bagID == bagID then button:SetAlpha(1.0) @@ -2567,12 +2566,12 @@ end -- Clear all highlighting by restoring full opacity to all slots function Guda_BagFrame_ClearHighlightedSlots() -- Restore alpha to whatever the search filter dictates (pfUI style). If no search, full opacity. + -- Use itemButtons hash instead of GetChildren() to avoid table allocation local searchActive = BagFrame and BagFrame.IsSearchActive and BagFrame:IsSearchActive() for _, bagParent in pairs(bagParents) do - if bagParent then - local children = { bagParent:GetChildren() } - for _, button in ipairs(children) do + if bagParent and bagParent.itemButtons then + for button in pairs(bagParent.itemButtons) do if button and button:IsShown() and button.hasItem ~= nil and not button.isBagSlot then if searchActive and BagFrame and BagFrame.PassesSearchFilter then local matches = BagFrame:PassesSearchFilter(button.itemData) @@ -2813,32 +2812,14 @@ end -- Refresh cooldown overlays for all visible item buttons function BagFrame:RefreshCooldowns() - local itemContainer = getglobal("Guda_BagFrame_ItemContainer") - if not itemContainer then return end - - -- Iterate through all children; our item buttons are direct children of per-bag parents inside the container, - -- so iterate all descendants by scanning children of children as well. - local function refreshChildren(parent) - local children = { parent:GetChildren() } - for _, child in ipairs(children) do - if child and child.hasItem ~= nil then - if child:IsShown() and Guda_ItemButton_UpdateCooldown then - Guda_ItemButton_UpdateCooldown(child) - end - end - -- Recurse one level to reach actual buttons under bag parents - local grandChildren = { child:GetChildren() } - if table.getn(grandChildren) > 0 then - for _, gc in ipairs(grandChildren) do - if gc and gc.hasItem ~= nil then - if gc:IsShown() and Guda_ItemButton_UpdateCooldown then - Guda_ItemButton_UpdateCooldown(gc) - end - end + -- Use itemButtons hash instead of GetChildren() to avoid table allocation + for _, bagParent in pairs(bagParents) do + if bagParent and bagParent.itemButtons then + for button in pairs(bagParent.itemButtons) do + if button and button.hasItem and button:IsShown() and Guda_ItemButton_UpdateCooldown then + Guda_ItemButton_UpdateCooldown(button) end end end end - - refreshChildren(itemContainer) end \ No newline at end of file diff --git a/UI/ItemButton.lua b/UI/ItemButton.lua index 6b4874f..8384fb1 100644 --- a/UI/ItemButton.lua +++ b/UI/ItemButton.lua @@ -4,6 +4,58 @@ local addon = Guda -- Item button pool local buttonPool = {} local nextButtonID = 1 +local BUTTON_POOL_MAX = 350 -- Maximum buttons to create (bags ~80 + bank ~124 + keyring ~96 + buffer) + +-- Get button pool statistics (for /guda perf command) +function Guda_GetButtonPoolStats() + local total = 0 + local shown = 0 + local hidden = 0 + local inUse = 0 + local available = 0 + for _, button in pairs(buttonPool) do + total = total + 1 + if button:IsShown() then + shown = shown + 1 + else + hidden = hidden + 1 + end + if button.inUse then + inUse = inUse + 1 + else + available = available + 1 + end + end + return { + total = total, + shown = shown, + hidden = hidden, + inUse = inUse, + available = available, + maxSize = BUTTON_POOL_MAX, + } +end + +-- Reset button pool (for testing/debugging) +-- WARNING: Only call this when no bag/bank frames are visible! +function Guda_ResetButtonPool() + -- Hide and clear all buttons + for id, button in pairs(buttonPool) do + button:Hide() + button:ClearAllPoints() + -- Clear from parent tracking + local parent = button:GetParent() + if parent and parent.itemButtons then + parent.itemButtons[button] = nil + end + end + -- Clear the pool table + for k in pairs(buttonPool) do + buttonPool[k] = nil + end + -- Reset counter + nextButtonID = 1 +end -- Use shared tooltip from Utils module (retrieved on-demand to ensure Utils is loaded) @@ -172,30 +224,117 @@ local function Guda_ItemButton_UpdateUsableTint(self) end +-- Check if a button is available for reuse +-- A button is available if it's either: +-- 1. Hidden (not shown) +-- 2. Marked as not in use (inUse == false) during an update cycle +local function IsButtonAvailable(button) + if not button:IsShown() then + return true + end + -- During update cycles, buttons are marked inUse = false before display + -- and inUse = true when assigned. This allows reuse without hiding first. + if button.inUse == false then + return true + end + return false +end + -- Create or get a button from the pool function Guda_GetItemButton(parent) - -- Try to reuse existing button from same parent + local reuseCandidate = nil -- Button from different parent we can reparent + + -- Try to reuse existing button from pool for _, button in pairs(buttonPool) do -- Skip bag slot buttons - if not button.isBagSlot and not button:IsShown() and button:GetParent() == parent then - -- Re-register with parent (itemButtons hash may have been cleared) + if not button.isBagSlot and IsButtonAvailable(button) then + if button:GetParent() == parent then + -- Same parent - best case, reuse immediately + -- Mark as in use to prevent double-assignment + button.inUse = true + -- Re-register with parent (itemButtons hash may have been cleared) + if Guda_RegisterItemButton then + Guda_RegisterItemButton(parent, button) + end + return button + elseif not reuseCandidate then + -- Different parent but available - save as candidate for reparenting + reuseCandidate = button + end + end + end + + -- If we found an available button from a different parent, reparent it + if reuseCandidate then + -- Mark as in use + reuseCandidate.inUse = true + + -- Unregister from old parent + local oldParent = reuseCandidate:GetParent() + if oldParent and oldParent.itemButtons then + oldParent.itemButtons[reuseCandidate] = nil + end + + -- Reparent to new parent + reuseCandidate:SetParent(parent) + + -- Register with new parent + if Guda_RegisterItemButton then + Guda_RegisterItemButton(parent, reuseCandidate) + end + + return reuseCandidate + end + + -- Only create new button if under pool limit + if nextButtonID <= BUTTON_POOL_MAX then + local button = CreateFrame("Button", "Guda_ItemButton" .. nextButtonID, parent, "Guda_ItemButtonTemplate") + buttonPool[nextButtonID] = button + nextButtonID = nextButtonID + 1 + button.inUse = true + + -- Register button with parent for tracking (avoids GetChildren() allocation) + if Guda_RegisterItemButton then + Guda_RegisterItemButton(parent, button) + end + + return button + end + + -- Pool is at max and no available button found - this shouldn't normally happen + -- but as a fallback, force-reuse the first non-bag-slot button we find + for _, button in pairs(buttonPool) do + if not button.isBagSlot then + -- Hide it first (in case it was shown) + button:Hide() + button.inUse = true + + -- Unregister from old parent + local oldParent = button:GetParent() + if oldParent and oldParent.itemButtons then + oldParent.itemButtons[button] = nil + end + + -- Reparent + button:SetParent(parent) + + -- Register with new parent if Guda_RegisterItemButton then Guda_RegisterItemButton(parent, button) end + return button end end - -- Create new button + -- Ultimate fallback (should never reach here) - create one more button local button = CreateFrame("Button", "Guda_ItemButton" .. nextButtonID, parent, "Guda_ItemButtonTemplate") buttonPool[nextButtonID] = button nextButtonID = nextButtonID + 1 - - -- Register button with parent for tracking (avoids GetChildren() allocation) + button.inUse = true if Guda_RegisterItemButton then Guda_RegisterItemButton(parent, button) end - return button end