diff --git a/MikScrollingBattleText/MikScrollingBattleText.lua b/MikScrollingBattleText/MikScrollingBattleText.lua index 6594570..cbe8a75 100644 --- a/MikScrollingBattleText/MikScrollingBattleText.lua +++ b/MikScrollingBattleText/MikScrollingBattleText.lua @@ -314,9 +314,9 @@ local stillMerging = false; local activeNonStickies = {}; local activeStickies = {}; --- Create a new table recycler object that holds up to 40 table references --- in primary cache. -local eventsRecycler = MikTRO:NewRecyclerObject(40); +-- Pool of animation event tables, reused to keep garbage (and 1.12's full-heap GC pauses) down. +-- Every acquired table must be released exactly once. +local eventsPool = CreateUnsecuredObjectPool(function() return {} end, function(_, t) table.wipe(t) end); ------------------------------------------------------------------------------------- @@ -589,8 +589,8 @@ end -- Receive combat events from Mik's combat event helper. -- ********************************************************************************** function MikSBT.CombatEventsHandler(combatEvent) - -- Get a recycled table from the events recycler object. - local animationEvent = eventsRecycler:AcquireTable(); + -- Get a table from the events pool. + local animationEvent = eventsPool:Acquire(); -- Look up the scroll area and event type string via pre-built tables (no string concatenation). local dirType = combatEvent.DirectionType; @@ -712,9 +712,9 @@ function MikSBT.CombatEventsHandler(combatEvent) -- Dispatch the event to the animation system. MikSBT.DispatchAnimationEvent(animationEvent); else - -- Print an error message and reclaim the animation event table to the table recycler. + -- Print an error message and release the animation event table to the events pool. MikSBT.Print("No profile data found for " .. eventTypeString, 1, 0, 0) - eventsRecycler:ReclaimTable(animationEvent); + eventsPool:Release(animationEvent); end end @@ -724,15 +724,15 @@ end -- Receive trigger events from Mik's trigger helper. -- ********************************************************************************** function MikSBT.TriggerHandler(triggerEvent) - -- Get a recycled table from the events recycler object. - local animationEvent = eventsRecycler:AcquireTable(); + -- Get a table from the events pool. + local animationEvent = eventsPool:Acquire(); -- Check if the trigger require a stance if (MikSBT.CurrentProfile.Triggers[triggerEvent.TriggerKey].TriggerSettings.Stance) then local _,_,isActive = GetShapeshiftFormInfo(MikSBT.CurrentProfile.Triggers[triggerEvent.TriggerKey].TriggerSettings.Stance) if not isActive then - -- Reclaim the animation event table to the table recycler. - eventsRecycler:ReclaimTable(animationEvent); + -- Release the animation event table to the events pool. + eventsPool:Release(animationEvent); return end end @@ -751,9 +751,9 @@ function MikSBT.TriggerHandler(triggerEvent) -- Dispatch the event to the animation system. MikSBT.DispatchAnimationEvent(animationEvent); else - -- Print an error message and reclaim the animation event table to the table recycler. + -- Print an error message and release the animation event table to the events pool. MikSBT.Print("No profile data found for trigger key " .. triggerEvent.TriggerKey, 1, 0, 0); - eventsRecycler:ReclaimTable(animationEvent); + eventsPool:Release(animationEvent); end end @@ -913,8 +913,8 @@ function MikSBT.MergeEvents(mergeData, numEvents) for x = 1, numEvents do -- Check if the event was merged into a different animation event. if (mergeData.UnmergedEvents[x] and mergeData.UnmergedEvents[x].EventMerged) then - -- Reclaim the animation event table to the events recycler. - eventsRecycler:ReclaimTable(mergeData.UnmergedEvents[x]); + -- Release the animation event table to the events pool. + eventsPool:Release(mergeData.UnmergedEvents[x]); end mergeData.UnmergedEvents[x] = nil; @@ -1692,6 +1692,9 @@ function MikSBT.DispatchAnimationEvent(animationEvent) else MikSBT.AddAnimation(animationEvent); end + else + -- The event is hidden so release it. + eventsPool:Release(animationEvent); end end @@ -1707,17 +1710,18 @@ function MikSBT.AddAnimation(animationEvent) for _, suppressionSettings in MikSBT.CurrentProfile.Suppressions do -- Check if the suppression's search pattern is a match. if (suppressionSettings.Enabled and (string_find(animationEvent.Text, suppressionSettings.SearchPattern))) then - -- Reclaim the animation event table to the events recycler and leave the function. - eventsRecycler:ReclaimTable(animationEvent); + -- Release the animation event table to the events pool and leave the function. + eventsPool:Release(animationEvent); return; elseif (suppressionSettings.Enabled and animationEvent.EffectName and string_find(animationEvent.EffectName, suppressionSettings.SearchPattern)) then - -- Reclaim the animation event table to the events recycler and leave the function. - eventsRecycler:ReclaimTable(animationEvent); + -- Release the animation event table to the events pool and leave the function. + eventsPool:Release(animationEvent); return; end end if animationEvent.EventType == "MSBT_EVENTTYPE_NOTIFICATION_POWER_GAIN" and animationEvent.EffectName ~= 0 and MikSBT.CurrentProfile.ShowAllManaGains then + eventsPool:Release(animationEvent); return end @@ -1896,8 +1900,8 @@ function MikSBT.AddAnimation(animationEvent) MikSBT.RepositionAnimDisplayInfo(animationEvent.ScrollArea); end - -- Reclaim the animation event table to the events recycler. - eventsRecycler:ReclaimTable(animationEvent); + -- Release the animation event table to the events pool. + eventsPool:Release(animationEvent); end -- ********************************************************************************** @@ -2682,8 +2686,8 @@ function MikSBT.CommandHandler(params) showUsage = false; elseif (currentParam == MikSBT.COMMAND_STATS) then - -- Print out stats about the event recycler. - eventsRecycler:PrintStats(); + -- Print the number of event tables in use; a count that keeps growing while idle means a missed release. + MikSBT.Print("Event tables in use: " .. eventsPool:GetNumActive(), 1, 1, 1); -- Don't show the usage info. showUsage = false; @@ -2816,8 +2820,8 @@ function MikSBT.DisplayMessage(message, displayType, isSticky, colorR, colorG, c end - -- Get a recycled table from the events recycler object. - local animationEvent = eventsRecycler:AcquireTable(); + -- Get a table from the events pool. + local animationEvent = eventsPool:Acquire(); -- Populate the animation event data. animationEvent.ScrollArea = scrollArea; diff --git a/MikScrollingBattleText/MikScrollingBattleText.toc b/MikScrollingBattleText/MikScrollingBattleText.toc index 7fb9646..c2e3d6d 100644 --- a/MikScrollingBattleText/MikScrollingBattleText.toc +++ b/MikScrollingBattleText/MikScrollingBattleText.toc @@ -16,7 +16,6 @@ MikScrollingBattleText-deDE.lua MikScrollingBattleText-ruRU.lua localization.lua -MikTableRecyclerObject.lua MikCombatEventHelper.lua MikScrollingBattleText.lua MikCombatEventHelper.xml diff --git a/MikScrollingBattleText/MikTableRecyclerObject.lua b/MikScrollingBattleText/MikTableRecyclerObject.lua deleted file mode 100644 index e8846a2..0000000 --- a/MikScrollingBattleText/MikTableRecyclerObject.lua +++ /dev/null @@ -1,168 +0,0 @@ -------------------------------------------------------------------------------------- --- Title: Mik's Table Recyler Object --- Author: Mik --- Maintainer: Athene --- Credits: --- Thanks to tekkub, the author of compostLib. I adapted much of his code for --- this object. -------------------------------------------------------------------------------------- - --- Create "namespace." -MikTRO = {}; - --- Set the __index property to the recycler object namespace so --- when a new recycler object sets it metatable to the namespace --- the recycler object inherits the functions in the namespace. -MikTRO.__index = MikTRO; - - --- ********************************************************************************** --- Creates a recycler object. --- ********************************************************************************** -function MikTRO:NewRecyclerObject(maxEntries) - -- Create a table to use. - recyclerObject = {}; - - -- Create a table to hold the cache entries and set its max permanent size. - recyclerObject.PrimaryCache = {}; - recyclerObject.PrimaryCacheSize = 0; - recyclerObject.MaxEntries = maxEntries; - - -- Create some variables to hold stats. - recyclerObject.NumNew = 0; - recyclerObject.NumRecycled = 0; - recyclerObject.NumErased = 0; - recyclerObject.NumReclaimed = 0; - recyclerObject.AmountMemFreed = 0; - - - -- Create a table to hold overflow entries. If more tables are reclaimed - -- than the size of the max primary cache can hold, they will be reclaimed - -- into the overflow cache which is a weak table. This allows for spikes - -- in cache usage to still be optimized while not preventing garbage - -- collection on the extra data once there are no other references to them. - recyclerObject.OverflowCache = {}; - setmetatable(recyclerObject.OverflowCache, {__mode = "v"}); - - - -- Set the meta table for the cache object to the table recycler - -- obect's namespace so the cache object inherits the functions of - -- table recycler object. - setmetatable(recyclerObject, self); - - -- Return the recycler object. - return recyclerObject; -end - - - --- ********************************************************************************** --- Removes a table from the cache and returns it. If there are no tables available --- in the cache a new one is returned. --- ********************************************************************************** -function MikTRO:AcquireTable() - -- Check if there are any tables available in the primary cache and if so - -- return one. - if (self.PrimaryCacheSize > 0) then - -- Increment the number of recycled tables. - self.NumRecycled = self.NumRecycled + 1; - - self.PrimaryCacheSize = self.PrimaryCacheSize - 1; - return table.remove(self.PrimaryCache); - - -- Check if there are any tables available in the overflow cache and if so - -- return one. - elseif self:TablesInOverflow() then - -- Increment the number of recycled tables. - self.NumRecycled = self.NumRecycled + 1; - - -- Loop through the tables in the overflow and return one. - for i in pairs(self.OverflowCache) do - return table.remove(self.OverflowCache, i); - end - - -- No tables available so return a new one. - else - -- Increment the number of new tables created. - self.NumNew = self.NumNew + 1; - return {}; - end -end - - --- ********************************************************************************** --- Reclaims a table into the cache. --- ********************************************************************************** -function MikTRO:ReclaimTable(t) - -- Check if the passed value isn't valid. - if (type(t) ~= "table") then - return; - end - - -- Erase the passed table. - self:EraseTable(t); - - -- Check if the primary cache is already full. - if (self.PrimaryCacheSize >= self.MaxEntries) then - -- Insert the table into the overflow cache. - table.insert(self.OverflowCache, t); - else - -- Insert the table into the primary cache. - table.insert(self.PrimaryCache, t); - self.PrimaryCacheSize = self.PrimaryCacheSize + 1; - end - - -- Increment the number of reclaimed tables. - self.NumReclaimed = self.NumReclaimed + 1; -end - - --- ********************************************************************************** --- Erases the passed table. Subtables are NOT erased. --- ********************************************************************************** -function MikTRO:EraseTable(t) - -- Check if the passed value isn't valid. - if (type(t) ~= "table") then - return; - end - - -- Loop through all the keys in the table and clear it. - for key in pairs(t) do - t[key] = nil; - end - - -- Set the length of the table to 0. - table.setn(t, 0); - - -- Increment the number of tables erased. - self.NumErased = self.NumErased + 1; -end - - --- ********************************************************************************** --- Prints stats. --- ********************************************************************************** -function MikTRO:PrintStats() - local overflowSize = 0; - for i in pairs(self.OverflowCache) do - overflowSize = overflowSize + 1; - end - - DEFAULT_CHAT_FRAME:AddMessage(string.format( - "|cff00ff00New Tables: %d|r |cffffff00Recycled Tables: %d|r |cff00ffffCached Tables: %d|r |cffff0000Overflow Tables: %d|r |cff888888Erased Tables: %d|r |cffff00ffMemory Saved: %d KiB|r |cffff0088Tables Lost to GC: %d", - self.NumNew, - self.NumRecycled, - self.PrimaryCacheSize, - overflowSize, - self.NumErased, - self.AmountMemFreed + ((32/1024) * self.NumRecycled), - self.NumReclaimed - self.NumRecycled - self.PrimaryCacheSize)); -end - - --- ********************************************************************************** --- Checks if there are any tables in the overflow cache. --- ********************************************************************************** -function MikTRO:TablesInOverflow() - return next(self.OverflowCache) ~= nil; -end \ No newline at end of file diff --git a/MikScrollingBattleText/localization.lua b/MikScrollingBattleText/localization.lua index 338c076..1cae89a 100644 --- a/MikScrollingBattleText/localization.lua +++ b/MikScrollingBattleText/localization.lua @@ -48,7 +48,7 @@ MikSBT.COMMAND_USAGE = { " " .. MikSBT.COMMAND_DISABLE .. " - Disables the mod.", " " .. MikSBT.COMMAND_ENABLE .. " - Enables the mod.", " " .. MikSBT.COMMAND_SHOWVER .. " - Shows the current version.", - " " .. MikSBT.COMMAND_STATS .. " - Reports stats about table recycling.", + " " .. MikSBT.COMMAND_STATS .. " - Reports how many pooled event tables are in use.", " " .. MikSBT.COMMAND_SEARCH .. " filter - Sets a filter for searching event types.", " " .. MikSBT.COMMAND_DEBUG .. " - Toggles debug mode.", " " .. MikSBT.COMMAND_HELP .. " - Show the command usage.", diff --git a/README.md b/README.md index fa4567e..aaf9bcf 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Interface/ | `/msbt disable` | Disable the addon | | `/msbt enable` | Enable the addon | | `/msbt version` | Show the current version | -| `/msbt stats` | Report table recycling statistics | +| `/msbt stats` | Report how many pooled event tables are in use | | `/msbt search ` | Set a filter for searching event types | | `/msbt debug` | Toggle debug mode | | `/msbt help` | Show command usage | @@ -87,7 +87,7 @@ SuperWoW, UnitXP SP3 and Nampower integration has been removed in favor of Class - **Dispatch table**: Combat event routing uses O(1) table lookup instead of an if/elseif chain - **SpellId icon cache**: SpellIds from combat log events resolve icons directly through ClassicAPI and are cached, bypassing Babble-Spell dictionary lookups and tooltip scanning - **Event throttling**: UNIT_HEALTH and UNIT_MANA trigger checks are throttled to 150ms intervals -- **Table recycling**: `MikTRO` (Table Recycler Object) pools and reuses table allocations to reduce garbage collection pressure +- **Table pooling**: Animation event tables come from a ClassicAPI object pool (`CreateUnsecuredObjectPool`) and are reused, so 1.12's stop-the-world GC runs less often ## Architecture @@ -97,7 +97,6 @@ SuperWoW, UnitXP SP3 and Nampower integration has been removed in favor of Class |---|---| | `MikSBT` | Main addon: display, animation, profiles, event routing, slash commands | | `MikCEH` | Combat Event Helper: parses combat events into structured data | -| `MikTRO` | Table Recycler Object: memory pool for table reuse | ### Event Processing Pipeline