diff --git a/Core/CacheWarmer.lua b/Core/CacheWarmer.lua index a8a3da8..b328fa6 100644 --- a/Core/CacheWarmer.lua +++ b/Core/CacheWarmer.lua @@ -1,114 +1,89 @@ -- Guda CacheWarmer --- Pre-warms ItemDetection's tooltip-scan cache and BagScanner's bag-data --- cache shortly after PLAYER_LOGIN, in the background, so the FIRST time the --- user opens the bag frame they don't pay for ~80 synchronous tooltip scans. +-- Pre-warms ItemDetection and category-related tooltip caches shortly after +-- PLAYER_LOGIN so the first bag open does not pay for cold tooltip scans. -- --- Reuses Utils:QueueWork (Core/Utils.lua) for frame-budgeted background --- processing — no new scheduler. +-- Guda ClassicAPI uses the low-latency FIFO in Core/Performance.lua. Heavy +-- tooltip operations stay as separate work items so a single callback cannot +-- accidentally bundle several expensive tooltip scans into one frame. local addon = Guda local CacheWarmer = {} addon.Modules.CacheWarmer = CacheWarmer --- Walk player bags 0-4 and warm BagScanner per-bag cache. Cheap, runs inline. +-- Populate the real BagScanner cache once. The original implementation called +-- ScanBag() for bags 0-4 but discarded every returned table, then GetBagData() +-- immediately scanned the bags again. Returning GetBagData() here makes the +-- warmup useful and gives the later stages the exact cached snapshot. function CacheWarmer:WarmBagScanner() local BagScanner = Guda.Modules.BagScanner - if not BagScanner or not BagScanner.ScanBag then return end - -- ScanBag populates the per-bag cache used by GetBagData later. - for bagID = 0, 4 do - local ok = pcall(function() BagScanner:ScanBag(bagID) end) - if not ok then break end - end + if not BagScanner or not BagScanner.GetBagData then return nil end + return BagScanner:GetBagData() end --- Walk player bags 0-4 and queue tooltip-scan work for every cache the --- bag-open path reads, so the first DisplayItemsByCategory never blocks on --- a cold lookup. Targets: --- * ItemDetection.detectionCache — used for isJunk / isQuestItem rules --- * Utils.tooltipCache.restoreTag — used for consumable food/drink grouping --- * Utils.tooltipCache.bindOnEquip — used for the isBoE rule --- * CategoryManager.categoryCache — used directly by the layout path --- Queue order is FIFO and budget-limited to 100ms/frame, so this all runs --- in the 0.5s–~2s window after PLAYER_LOGIN. -function CacheWarmer:WarmItemDetectionCache() +-- Queue tooltip/cache work using the already-cached BagScanner snapshot. +-- This avoids another round of GetContainerNumSlots/GetContainerItemLink and +-- avoids GetItemInfo calls just to discover the item class. +function CacheWarmer:WarmItemDetectionCache(bagData) local Utils = Guda.Modules.Utils local ItemDetection = Guda.Modules.ItemDetection local CategoryManager = Guda.Modules.CategoryManager local BagScanner = Guda.Modules.BagScanner + if not (Utils and Utils.QueueWork) then return end if not (ItemDetection and ItemDetection.GetItemProperties) then return end - -- WarmBagScanner ran just before us, so GetBagData() is a cache hit and - -- returns the already-populated itemData with class/subclass/quality. - local bagData = BagScanner and BagScanner.GetBagData and BagScanner:GetBagData() + bagData = bagData or (BagScanner and BagScanner.GetBagData + and BagScanner:GetBagData()) + if not bagData then return end for bagID = 0, 4 do - local numSlots = GetContainerNumSlots(bagID) - if numSlots and numSlots > 0 then - for slotID = 1, numSlots do - local link = GetContainerItemLink(bagID, slotID) - if link then - -- Capture upvalues so the closure has stable bag/slot/link. - local b, s, l = bagID, slotID, link + local bag = bagData[bagID] + if bag and bag.slots then + for slotID, slotData in pairs(bag.slots) do + if slotData and slotData.link then + local b, s = bagID, slotID + local snapshot = { + link = slotData.link, + itemID = slotData.itemID, + name = slotData.name, + class = slotData.class, + type = slotData.type, + subclass = slotData.subclass, + quality = slotData.quality, + texture = slotData.texture, + equipSlot = slotData.equipSlot, + } + -- Central property scan. Keep this as its own queue item: + -- SetBagItem can be expensive on a cold client cache. Utils:QueueWork(function() - -- Minimal itemData — GetItemProperties only needs - -- .link to compute the cache key; bagID/slotID feed - -- the tooltip scan. - ItemDetection:GetItemProperties({ link = l }, b, s) + ItemDetection:GetItemProperties(snapshot, b, s) end, "CacheWarmer.itemDetection") - -- Consumable restoreTag cache (only consumables carry - -- "while eating" / "while drinking" / "use: restores"). - Utils:QueueWork(function() - local _, _, _, _, itemType = GetItemInfo(l) - if itemType == "Consumable" and Utils.GetConsumableRestoreTag then - Utils:GetConsumableRestoreTag(b, s, l) - end - end, "CacheWarmer.restoreTag") + -- Only consumables need the restore-text tooltip cache. + if snapshot.class == "Consumable" + and Utils.GetConsumableRestoreTag then + Utils:QueueWork(function() + snapshot.restoreTag = + Utils:GetConsumableRestoreTag(b, s, snapshot.link) + end, "CacheWarmer.restoreTag") + end - -- BoE tooltip cache. Only weapons/armor can be BoE; skip - -- everything else so we don't scan consumables / trade - -- goods tooltips for nothing. - Utils:QueueWork(function() - local _, _, _, _, _, class = GetItemInfo(l) - if (class == "Weapon" or class == "Armor") and Utils.IsBindOnEquip then - Utils:IsBindOnEquip(b, s, l) - end - end, "CacheWarmer.boe") + -- Only equipment can be Bind on Equip. + if (snapshot.class == "Weapon" or snapshot.class == "Armor") + and Utils.IsBindOnEquip then + Utils:QueueWork(function() + Utils:IsBindOnEquip(b, s, snapshot.link) + end, "CacheWarmer.boe") + end - -- CategoryManager cache. Snapshot itemData fields because - -- BagScanner uses a pool and may recycle the table later. - -- The restoreTag field is populated inside the callback - -- (not here) because the restoreTag warmer runs first via - -- FIFO queue order — so by the time this fires, the - -- tooltipCache.restoreTag lookup is a cache hit. - if CategoryManager and CategoryManager.CategorizeItem and bagData then - local bag = bagData[b] - local slotData = bag and bag.slots and bag.slots[s] - if slotData and slotData.link then - local snapshot = { - link = slotData.link, - name = slotData.name, - class = slotData.class, - type = slotData.type, - subclass = slotData.subclass, - quality = slotData.quality, - texture = slotData.texture, - equipSlot = slotData.equipSlot, - } - Utils:QueueWork(function() - -- Pull restoreTag from warm cache so rules that - -- key on it (Food/Drink categories) match. - if snapshot.class == "Consumable" - and Utils.GetConsumableRestoreTag then - snapshot.restoreTag = - Utils:GetConsumableRestoreTag(b, s, snapshot.link) - end - CategoryManager:CategorizeItem(snapshot, b, s, false) - end, "CacheWarmer.category") - end + -- Runs after the relevant warmers because the queue is FIFO, + -- so category rules hit warm ItemDetection/tooltip caches. + if CategoryManager and CategoryManager.CategorizeItem then + Utils:QueueWork(function() + CategoryManager:CategorizeItem(snapshot, b, s, false) + end, "CacheWarmer.category") end end end @@ -117,17 +92,12 @@ function CacheWarmer:WarmItemDetectionCache() end function CacheWarmer:Initialize() - -- Defer ~0.5s so we don't compete with the busy login frame. The user - -- typically opens bags several seconds later, so the cache will be hot. + -- Defer startup work so we do not compete with the login frame. Guda_ScheduleTimer(0.5, function() - CacheWarmer:WarmBagScanner() - CacheWarmer:WarmItemDetectionCache() + local bagData = CacheWarmer:WarmBagScanner() + CacheWarmer:WarmItemDetectionCache(bagData) - -- Completion marker — runs after all prior warmers drain. Re-layouts - -- the bag so items that fell back to their class bucket on the first - -- (cold-cache) render now land in their real categories, and re-sweeps - -- tints for items whose detection finished while the bag was already - -- open. + -- Completion marker runs after all prior FIFO work drains. local Utils = Guda.Modules.Utils if Utils and Utils.QueueWork then Utils:QueueWork(function()