performance: sort engine performance

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