From 846edb8d4816090865794dd81f6d5b3718b705db Mon Sep 17 00:00:00 2001 From: Salikh Gurgenidze Date: Sat, 24 Jan 2026 03:33:23 +0400 Subject: [PATCH] performance: sort engine performance --- Core/Utils.lua | 146 ++++++++++++++++++++++------------------- Data/BagScanner.lua | 78 +++++++++++++++++----- Sorting/SortEngine.lua | 94 ++++++++++++++++++-------- UI/BankFrame.lua | 15 ++--- UI/FrameHelpers.lua | 80 ++++++++++++++++------ UI/ItemButton.lua | 70 ++++++++++++-------- 6 files changed, 322 insertions(+), 161 deletions(-) diff --git a/Core/Utils.lua b/Core/Utils.lua index 522f4ff..6a9980b 100644 --- a/Core/Utils.lua +++ b/Core/Utils.lua @@ -482,20 +482,23 @@ function Utils:DeepCopy(orig) return copy end --- Get class color +-- Class colors (cached at module level to avoid table creation per call) +local CLASS_COLORS = { + WARRIOR = {r = 0.78, g = 0.61, b = 0.43}, + PALADIN = {r = 0.96, g = 0.55, b = 0.73}, + HUNTER = {r = 0.67, g = 0.83, b = 0.45}, + ROGUE = {r = 1.00, g = 0.96, b = 0.41}, + PRIEST = {r = 1.00, g = 1.00, b = 1.00}, + SHAMAN = {r = 0.00, g = 0.44, b = 0.87}, + MAGE = {r = 0.41, g = 0.80, b = 0.94}, + WARLOCK = {r = 0.58, g = 0.51, b = 0.79}, + DRUID = {r = 1.00, g = 0.49, b = 0.04}, + _default = {r = 0.5, g = 0.5, b = 0.5}, +} + +-- Get class color (uses cached table) function Utils:GetClassColor(class) - local colors = { - WARRIOR = {r = 0.78, g = 0.61, b = 0.43}, - PALADIN = {r = 0.96, g = 0.55, b = 0.73}, - HUNTER = {r = 0.67, g = 0.83, b = 0.45}, - ROGUE = {r = 1.00, g = 0.96, b = 0.41}, - PRIEST = {r = 1.00, g = 1.00, b = 1.00}, - SHAMAN = {r = 0.00, g = 0.44, b = 0.87}, - MAGE = {r = 0.41, g = 0.80, b = 0.94}, - WARLOCK = {r = 0.58, g = 0.51, b = 0.79}, - DRUID = {r = 1.00, g = 0.49, b = 0.04}, - } - return colors[class] or {r = 0.5, g = 0.5, b = 0.5} + return CLASS_COLORS[class] or CLASS_COLORS._default end -- Format time ago @@ -536,52 +539,65 @@ local function IsGreenColor(r, g, b) return r < 0.4 and g > 0.7 and b < 0.4 end --- Patterns that indicate an item has special functionality (not junk) -local SPECIAL_TEXT_PATTERNS = { - -- Use effects - "use:", - "use :", - -- Equip effects - "equip:", - "equip :", - -- Proc effects - "chance on hit:", - "chance on hit :", - "chance to", - "chance on", - -- Stat effects - "increases", - "improves", - "restores", - "regenerate", - "generates", - "absorbs", - "reduces", - "grants", - "gives", - -- Learning - "teaches", - "learn", - -- Special actions - "creates", - "summons", - "teleports", - "opens", - "activates", - -- Resistance/stats - "resistance", - "armor", - "damage", - "healing", - "mana", - "health", - "spirit", - "intellect", - "stamina", - "strength", - "agility", +-- Fast pattern lookup table for special text detection (Lua 5.0 optimized) +-- Uses prefix-based lookup to avoid iterating all patterns +local SPECIAL_TEXT_PREFIXES = { + ["use"] = true, -- use:, use : + ["equ"] = true, -- equip:, equip : + ["cha"] = true, -- chance on hit, chance to + ["inc"] = true, -- increases + ["imp"] = true, -- improves + ["res"] = true, -- restores, resistance + ["reg"] = true, -- regenerate + ["gen"] = true, -- generates + ["abs"] = true, -- absorbs + ["red"] = true, -- reduces + ["gra"] = true, -- grants + ["giv"] = true, -- gives + ["tea"] = true, -- teaches + ["lea"] = true, -- learn + ["cre"] = true, -- creates + ["sum"] = true, -- summons + ["tel"] = true, -- teleports + ["ope"] = true, -- opens + ["act"] = true, -- activates + ["arm"] = true, -- armor + ["dam"] = true, -- damage + ["hea"] = true, -- healing, health + ["man"] = true, -- mana + ["spi"] = true, -- spirit + ["int"] = true, -- intellect + ["sta"] = true, -- stamina + ["str"] = true, -- strength + ["agi"] = true, -- agility } +-- Full patterns for validation after prefix match (only checked if prefix matches) +local SPECIAL_TEXT_PATTERNS = { + "use:", "equip:", "chance on", "chance to", + "increases", "improves", "restores", "regenerate", "generates", + "absorbs", "reduces", "grants", "gives", "teaches", "learn", + "creates", "summons", "teleports", "opens", "activates", + "resistance", "armor", "damage", "healing", "mana", "health", + "spirit", "intellect", "stamina", "strength", "agility", +} + +-- Fast check if text contains special patterns (uses prefix lookup first) +local function HasSpecialTextPattern(textLower) + -- Quick prefix check (3 chars) to avoid full pattern scan + local prefix = string.sub(textLower, 1, 3) + if not SPECIAL_TEXT_PREFIXES[prefix] then + return false + end + -- Prefix matched - do full pattern check + for _, pattern in ipairs(SPECIAL_TEXT_PATTERNS) do + if string.find(textLower, pattern) then + return true + end + end + return false +end + -- Check if an item's tooltip contains yellow or green description text -- This indicates the item has a use effect, equip effect, or special property -- Returns: hasSpecialText (boolean), textType ("yellow", "green", or nil) @@ -644,16 +660,14 @@ function Utils:HasSpecialTooltipText(bagID, slotID, itemLink) -- Check for yellow/gold text (Use:, Equip:, Chance on hit:, etc.) if IsYellowColor(r, g, b) then - -- Check if it matches any special text pattern - for _, pattern in ipairs(SPECIAL_TEXT_PATTERNS) do - if string.find(textLower, pattern) then - addon:Debug("HasSpecialTooltipText: YELLOW match '%s' in: %s", pattern, text) - -- Cache positive result - if cacheKey then - tooltipCache.specialText[cacheKey] = { hasSpecial = true, textType = "yellow" } - end - return true, "yellow" + -- Use optimized prefix-based pattern check + if HasSpecialTextPattern(textLower) then + addon:Debug("HasSpecialTooltipText: YELLOW match in: %s", text) + -- Cache positive result + if cacheKey then + tooltipCache.specialText[cacheKey] = { hasSpecial = true, textType = "yellow" } end + return true, "yellow" end end diff --git a/Data/BagScanner.lua b/Data/BagScanner.lua index ffd3b77..051229b 100644 --- a/Data/BagScanner.lua +++ b/Data/BagScanner.lua @@ -14,8 +14,55 @@ local cacheValid = false local eventPending = false local dirtySlots = {} -- Track specific slots that changed: dirtySlots[bagID][slotID] = true --- Clear the bag cache +--===================================================== +-- Item Data Pool (Baganator-inspired memory optimization) +-- Reuses item data tables instead of creating new ones +--===================================================== +local itemDataPool = {} +local ITEM_DATA_POOL_MAX = 200 -- Limit pool size to prevent unbounded growth + +-- Acquire item data table from pool +local function AcquireItemData() + return table.remove(itemDataPool) or {} +end + +-- Release item data table back to pool +local function ReleaseItemData(data) + if data and table.getn(itemDataPool) < ITEM_DATA_POOL_MAX then + -- Clear all fields for reuse + data.link = nil + data.texture = nil + data.count = nil + data.quality = nil + data.name = nil + data.iLevel = nil + data.type = nil + data.class = nil + data.subclass = nil + data.equipSlot = nil + data.locked = nil + table.insert(itemDataPool, data) + end +end + +-- Release all item data in a bag cache entry +local function ReleaseBagCacheData(bagData) + if bagData and bagData.slots then + for slotID, itemData in pairs(bagData.slots) do + if itemData then + ReleaseItemData(itemData) + end + end + end +end + +-- Clear the bag cache (releases pooled data) function BagScanner:ClearCache() + if bagCache then + for bagID, bagData in pairs(bagCache) do + ReleaseBagCacheData(bagData) + end + end bagCache = nil cacheValid = false dirtySlots = {} @@ -142,7 +189,7 @@ function BagScanner:ScanBag(bagID) return bag end --- Scan a single slot +-- Scan a single slot (uses item data pooling) function BagScanner:ScanSlot(bagID, slot) -- Validate parameters to prevent API errors if bagID == nil or slot == nil or slot < 1 then @@ -164,19 +211,20 @@ function BagScanner:ScanSlot(bagID, slot) name, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(itemLink) end - return { - link = itemLink, - texture = texture, - count = itemCount or 1, - quality = quality or itemQuality or 0, - name = name, - iLevel = iLevel, - type = itemType, - class = itemCategory, - subclass = itemSubType, - equipSlot = itemEquipLoc, - locked = locked, - } + -- Use pooled item data table instead of creating new one + local itemData = AcquireItemData() + itemData.link = itemLink + itemData.texture = texture + itemData.count = itemCount or 1 + itemData.quality = quality or itemQuality or 0 + itemData.name = name + itemData.iLevel = iLevel + itemData.type = itemType + itemData.class = itemCategory + itemData.subclass = itemSubType + itemData.equipSlot = itemEquipLoc + itemData.locked = locked + return itemData end -- Save current bags to database diff --git a/Sorting/SortEngine.lua b/Sorting/SortEngine.lua index 31e15b0..f806dc5 100644 --- a/Sorting/SortEngine.lua +++ b/Sorting/SortEngine.lua @@ -12,6 +12,10 @@ SortEngine.sortingInProgress = false -- Performance: Max items to move per cycle -- Baganator uses 5 for manual transfers, but sorting needs more for smooth operation local MAX_MOVES_PER_CYCLE = 20 +local MAX_BANK_MOVES_PER_CYCLE = 30 -- Bank has 50% more slots, needs more moves per cycle + +-- Current sort context (set by ExecuteSort, used by ApplySort) +local currentSortType = "bags" -- Transfer status constants (like Baganator's SortStatus) local TransferStatus = { @@ -131,10 +135,22 @@ local GEM_PATTERNS = { --=========================================================================== -- Property cache to prevent race conditions during rapid moves +-- With size limits to prevent unbounded memory growth (Baganator pattern) local propertyCache = {} +local propertyCacheSize = 0 +local PROPERTY_CACHE_MAX = 500 -- Prevent memory leaks from unbounded cache function SortEngine:ClearCache() propertyCache = {} + propertyCacheSize = 0 +end + +-- Get cache statistics for performance monitoring +function SortEngine:GetCacheStats() + return { + size = propertyCacheSize, + maxSize = PROPERTY_CACHE_MAX, + } end local function GetItemProperties(bagID, slotID, itemLink) @@ -148,6 +164,12 @@ local function GetItemProperties(bagID, slotID, itemLink) return propertyCache[cacheKey] end + -- Evict cache if it exceeds max size to prevent memory leaks + if propertyCacheSize >= PROPERTY_CACHE_MAX then + propertyCache = {} + propertyCacheSize = 0 + end + local props = { isQuest = false, isQuestStarter = false, @@ -231,6 +253,7 @@ local function GetItemProperties(bagID, slotID, itemLink) end propertyCache[cacheKey] = props + propertyCacheSize = propertyCacheSize + 1 return props end @@ -990,12 +1013,15 @@ local function BuildTargetPositions(bagIDs, itemCount) }) end - table.sort(sortedBags, function(a, b) - if a.priority ~= b.priority then - return a.priority > b.priority - end - return a.bagID < b.bagID - end) + -- Only sort if we have more than one element + if table.getn(sortedBags) > 1 then + table.sort(sortedBags, function(a, b) + if a.priority ~= b.priority then + return a.priority > b.priority + end + return a.bagID < b.bagID + end) + end -- Build positions in priority order for _, bagInfo in ipairs(sortedBags) do @@ -1034,6 +1060,9 @@ local function ApplySort(bagIDs, items, targetPositions) local moveToEmpty = {} local swapOccupied = {} + -- Use bank-specific move limit (bank has 50% more slots) + local maxMoves = (currentSortType == "bank") and MAX_BANK_MOVES_PER_CYCLE or MAX_MOVES_PER_CYCLE + -- Build move queues for i, item in ipairs(items) do local target = targetPositions[i] @@ -1071,7 +1100,7 @@ local function ApplySort(bagIDs, items, targetPositions) for _, move in ipairs(moveToEmpty) do -- Limit moves per cycle to prevent slot locking (like Baganator) - if moveCount >= MAX_MOVES_PER_CYCLE then + if moveCount >= maxMoves then break end @@ -1089,7 +1118,7 @@ local function ApplySort(bagIDs, items, targetPositions) -- Execute swaps with occupied slots (if we haven't hit the limit) for _, move in ipairs(swapOccupied) do -- Limit moves per cycle - if moveCount >= MAX_MOVES_PER_CYCLE then + if moveCount >= maxMoves then break end @@ -1137,12 +1166,15 @@ local function BuildGreyTailPositions(bagIDs, greyCount) end end - table.sort(ordered, function(a, b) - if a.priority ~= b.priority then - return a.priority < b.priority -- lowest first - end - return a.bagID > b.bagID -- higher bagID later (treated as further to the right) - end) + -- Only sort if we have more than one element + if table.getn(ordered) > 1 then + table.sort(ordered, function(a, b) + if a.priority ~= b.priority then + return a.priority < b.priority -- lowest first + end + return a.bagID > b.bagID -- higher bagID later (treated as further to the right) + end) + end -- Collect tail slots from end to start, spilling to previous bags as needed. local tailSlots = {} @@ -1160,17 +1192,19 @@ local function BuildGreyTailPositions(bagIDs, greyCount) -- STABILITY FIX: Sort the collected tail slots to match the ascending scan order. -- This ensures that identical items don't swap places every pass. -- Ascending order: Priority DESC, BagID ASC, Slot ASC (matching BuildTargetPositions) - table.sort(tailSlots, function(a, b) - local aPrio = tonumber(addon.Modules.Utils:GetContainerPriority(a.bag)) or 0 - local bPrio = tonumber(addon.Modules.Utils:GetContainerPriority(b.bag)) or 0 - if aPrio ~= bPrio then - return aPrio > bPrio - end - if a.bag ~= b.bag then - return a.bag < b.bag - end - return a.slot < b.slot - end) + if table.getn(tailSlots) > 1 then + table.sort(tailSlots, function(a, b) + local aPrio = tonumber(addon.Modules.Utils:GetContainerPriority(a.bag)) or 0 + local bPrio = tonumber(addon.Modules.Utils:GetContainerPriority(b.bag)) or 0 + if aPrio ~= bPrio then + return aPrio > bPrio + end + if a.bag ~= b.bag then + return a.bag < b.bag + end + return a.slot < b.slot + end) + end return tailSlots end @@ -1622,6 +1656,9 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort return false, "sorting in progress" end + -- Set current sort context so ApplySort uses correct move limits + currentSortType = sortType or "bags" + -- Clear property cache at the start of a sort operation self:ClearCache() @@ -1630,6 +1667,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort -- Check if already sorted if analysis.alreadySorted then + currentSortType = "bags" -- Reset to default return false, "already sorted" end @@ -1643,7 +1681,8 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort local passCount = 0 local maxPasses = math.max(analysis.passes, 1) -- Use estimated passes, minimum 1 - local safetyLimit = math.max(maxPasses * 3, 10) -- Reasonable upper bound + -- Bank has 50% more slots (240 vs 160), so needs higher safety limit + local safetyLimit = (sortType == "bank") and math.max(maxPasses * 4, 15) or math.max(maxPasses * 3, 10) local totalMoves = 0 local noProgressPasses = 0 @@ -1671,6 +1710,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort frame:SetScript("OnUpdate", nil) SortEngine.sortingInProgress = false SortEngine:UpdateSortButtonState(false) + currentSortType = "bags" -- Reset sort context -- Clear caches after sorting to ensure fresh detection if sortType == "bank" then addon.Modules.BankScanner:ClearCache() @@ -1696,6 +1736,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort frame:SetScript("OnUpdate", nil) SortEngine.sortingInProgress = false SortEngine:UpdateSortButtonState(false) + currentSortType = "bags" -- Reset sort context -- Clear caches after sorting to ensure fresh detection if sortType == "bank" then addon.Modules.BankScanner:ClearCache() @@ -1728,6 +1769,7 @@ function SortEngine:ExecuteSort(sortFunction, analyzeFunction, updateFrame, sort frame:SetScript("OnUpdate", nil) SortEngine.sortingInProgress = false SortEngine:UpdateSortButtonState(false) + currentSortType = "bags" -- Reset sort context -- Clear caches after sorting to ensure fresh detection if sortType == "bank" then addon.Modules.BankScanner:ClearCache() diff --git a/UI/BankFrame.lua b/UI/BankFrame.lua index fc9b482..5cba0db 100644 --- a/UI/BankFrame.lua +++ b/UI/BankFrame.lua @@ -173,9 +173,8 @@ function BankFrame:Update() -- Check if we have any displayed items local hasDisplayedItems = false for _, bankBagParent in pairs(bankBagParents) do - if bankBagParent then - local children = { bankBagParent:GetChildren() } - for _, button in ipairs(children) do + if bankBagParent and bankBagParent.itemButtons then + for button in pairs(bankBagParent.itemButtons) do if button.hasItem and button:IsShown() then hasDisplayedItems = true break @@ -194,9 +193,8 @@ function BankFrame:Update() -- Mark all existing buttons as not in use (we'll mark active ones during display) for _, bankBagParent in pairs(bankBagParents) do - if bankBagParent then - local buttons = { bankBagParent:GetChildren() } - for _, button in ipairs(buttons) do + if bankBagParent and bankBagParent.itemButtons then + for button in pairs(bankBagParent.itemButtons) do if button.hasItem ~= nil then button.inUse = false end @@ -274,9 +272,8 @@ function BankFrame:Update() -- Clean up unused buttons AFTER display is complete (prevents drag/drop issues) for _, bankBagParent in pairs(bankBagParents) do - if bankBagParent then - local buttons = { bankBagParent:GetChildren() } - for _, button in ipairs(buttons) do + if bankBagParent and bankBagParent.itemButtons then + for button in pairs(bankBagParent.itemButtons) do if button.hasItem ~= nil and not button.inUse then button:Hide() button:ClearAllPoints() diff --git a/UI/FrameHelpers.lua b/UI/FrameHelpers.lua index 6b9736b..912fb6a 100644 --- a/UI/FrameHelpers.lua +++ b/UI/FrameHelpers.lua @@ -135,44 +135,77 @@ function Guda_CategorizeItem(itemData, bagID, slotID, categories, specialItems, table.insert(categories[cat], {bagID = bagID, slotID = slotID, itemData = itemData}) end --- Initialize empty category tables +--===================================================== +-- Category Table Pooling (memory optimization) +-- Reuses category tables instead of creating new ones +--===================================================== +local categoriesCache = nil +local specialItemsCache = nil + +-- Clear a table without creating a new one (Lua 5.0 compatible) +local function WipeTable(t) + if not t then return end + for k in pairs(t) do + t[k] = nil + end +end + +-- Initialize empty category tables (reuses cached tables) function Guda_InitCategories() -- Refresh category list from CategoryManager (enabled categories only for display) Guda_RefreshCategoryList() - -- Create tables for ALL categories (not just enabled) so items can be categorized - -- Items in disabled categories simply won't be displayed - local categories = {} + -- Reuse or create categories table + if not categoriesCache then + categoriesCache = {} + end + + -- Clear existing category arrays (don't recreate the main table) + for cat, items in pairs(categoriesCache) do + WipeTable(items) + end if addon.Modules.CategoryManager then -- Get full category order (all categories, not just enabled) local allCategories = addon.Modules.CategoryManager:GetCategoryOrder() for _, cat in ipairs(allCategories) do - categories[cat] = {} + if not categoriesCache[cat] then + categoriesCache[cat] = {} + end end else -- Fallback: use the display list for _, cat in ipairs(Guda_CategoryList) do - categories[cat] = {} + if not categoriesCache[cat] then + categoriesCache[cat] = {} + end end end -- Always ensure Miscellaneous exists as fallback - if not categories["Miscellaneous"] then - categories["Miscellaneous"] = {} + if not categoriesCache["Miscellaneous"] then + categoriesCache["Miscellaneous"] = {} end -- Always ensure Keyring exists (handled specially in BagFrame) - if not categories["Keyring"] then - categories["Keyring"] = {} + if not categoriesCache["Keyring"] then + categoriesCache["Keyring"] = {} end - local specialItems = { - Hearthstone = {}, - Mount = {}, - Tools = {} - } - return categories, specialItems + -- Reuse or create specialItems table + if not specialItemsCache then + specialItemsCache = { + Hearthstone = {}, + Mount = {}, + Tools = {} + } + else + WipeTable(specialItemsCache.Hearthstone) + WipeTable(specialItemsCache.Mount) + WipeTable(specialItemsCache.Tools) + end + + return categoriesCache, specialItemsCache end -- Sort items within a category @@ -254,17 +287,26 @@ function Guda_GetBagParent(framePrefix, parentsTable, bagID, containerName) if parentsTable[bagID].SetID then parentsTable[bagID]:SetID(bagID) end + -- Track item buttons to avoid GetChildren() table allocation + parentsTable[bagID].itemButtons = {} end return parentsTable[bagID] end +-- Register an item button with its parent for tracking +function Guda_RegisterItemButton(parent, button) + if parent and parent.itemButtons and button then + parent.itemButtons[button] = true + end +end + -- Update lock/desaturation states for a table of parent frames +-- Uses itemButtons tracking to avoid GetChildren() table allocation function Guda_UpdateLockStates(parentsTable) if not parentsTable then return end for _, parent in pairs(parentsTable) do - if parent then - local buttons = { parent:GetChildren() } - for _, button in ipairs(buttons) do + if parent and parent.itemButtons then + for button in pairs(parent.itemButtons) do if button.hasItem ~= nil and button:IsShown() and button.bagID and button.slotID then -- GetContainerItemInfo returns: texture, itemCount, locked, quality, readable -- The 3rd return value is the lock state (boolean or nil) diff --git a/UI/ItemButton.lua b/UI/ItemButton.lua index dccf978..87b9f95 100644 --- a/UI/ItemButton.lua +++ b/UI/ItemButton.lua @@ -23,37 +23,45 @@ local function IsQuestItem(bagID, slotID, isBank, itemData) end --===================================================== --- Junk Icon Helper Functions (reusable for all views) --- Shows a vendor sell icon on junk items +-- Junk Icon Pool (Baganator-inspired memory optimization) +-- Uses frame pooling to avoid creating new frames per button --===================================================== +local junkIconPool = {} --- Create junk icon for a button (parented to UIParent for full opacity) -local function CreateJunkIcon(button) - if button.junkIcon then return button.junkIcon end +-- Get a junk icon from pool or create new one +local function AcquireJunkIcon() + local icon = table.remove(junkIconPool) + if not icon then + icon = CreateFrame("Frame", nil, UIParent) + icon:SetFrameStrata("HIGH") + icon:SetWidth(14) + icon:SetHeight(14) - local junkFrame = CreateFrame("Frame", nil, UIParent) - junkFrame:SetFrameStrata("HIGH") - junkFrame:SetWidth(14) - junkFrame:SetHeight(14) - - local texture = junkFrame:CreateTexture(nil, "OVERLAY") - texture:SetAllPoints(junkFrame) - texture:SetTexture("Interface\\GossipFrame\\VendorGossipIcon") - texture:SetTexCoord(0, 1, 0, 1) - - junkFrame:Hide() - button.junkIcon = junkFrame - return junkFrame + local texture = icon:CreateTexture(nil, "OVERLAY") + texture:SetAllPoints(icon) + texture:SetTexture("Interface\\GossipFrame\\VendorGossipIcon") + texture:SetTexCoord(0, 1, 0, 1) + icon.texture = texture + end + return icon end --- Update junk icon visibility and position -local function UpdateJunkIcon(button, isJunk, iconSize) - -- Create icon if it doesn't exist - if not button.junkIcon then - CreateJunkIcon(button) +-- Release a junk icon back to the pool +local function ReleaseJunkIcon(icon) + if icon then + icon:Hide() + icon:ClearAllPoints() + table.insert(junkIconPool, icon) end +end +-- Update junk icon visibility and position (uses pooling) +local function UpdateJunkIcon(button, isJunk, iconSize) if isJunk then + -- Acquire from pool if needed + if not button.junkIcon then + button.junkIcon = AcquireJunkIcon() + end -- Scale icon size based on button size local junkIconSize = math.max(10, math.min(14, iconSize * 0.30)) button.junkIcon:SetWidth(junkIconSize) @@ -63,14 +71,19 @@ local function UpdateJunkIcon(button, isJunk, iconSize) button.junkIcon:SetAlpha(1.0) button.junkIcon:Show() else - button.junkIcon:Hide() + -- Release back to pool when not needed + if button.junkIcon then + ReleaseJunkIcon(button.junkIcon) + button.junkIcon = nil + end end end --- Hide junk icon (for cleanup/reset) +-- Hide junk icon (releases to pool) local function HideJunkIcon(button) if button.junkIcon then - button.junkIcon:Hide() + ReleaseJunkIcon(button.junkIcon) + button.junkIcon = nil end end @@ -251,6 +264,11 @@ function Guda_GetItemButton(parent) buttonPool[nextButtonID] = button nextButtonID = nextButtonID + 1 + -- Register button with parent for tracking (avoids GetChildren() allocation) + if Guda_RegisterItemButton then + Guda_RegisterItemButton(parent, button) + end + return button end