Replace MikTRO with a ClassicAPI object pool

Animation event tables now come from CreateUnsecuredObjectPool and are
wiped on release, keeping garbage down under 1.12's stop-the-world GC.
Release the tables that were previously dropped for the GC (hidden
events, the power-gain early return), and make /msbt stats report the
number of tables in use as a leak check.
This commit is contained in:
Brues
2026-10-28 16:53:18 -05:00
parent 4b09ebc3f7
commit a36912af47
5 changed files with 32 additions and 198 deletions
@@ -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;
@@ -16,7 +16,6 @@ MikScrollingBattleText-deDE.lua
MikScrollingBattleText-ruRU.lua
localization.lua
MikTableRecyclerObject.lua
MikCombatEventHelper.lua
MikScrollingBattleText.lua
MikCombatEventHelper.xml
@@ -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
+1 -1
View File
@@ -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.",
+2 -3
View File
@@ -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 <filter>` | 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