Files
OEVGandClaude Sonnet 5.5 00c7860f31 GentleGC v2.7 (unified edition)
Squashed history of the earlier README and patch commits.
Original history kept locally on branch backup/pre-squash.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwtugSE5NbbiWQCbbKX
2026-10-31 21:33:36 +00:00

243 lines
8.4 KiB
Lua

-- Raid-scale behaviour: careful mode when collections are slow, collections
-- run by other addons, and the garbage creation rate.
-- Run: lua5.0 tests/test_raid.lua GentleGC.lua
format = string.format
local now = 1000
function GetTime() return now end
local combat, speed = false, 7
function UnitAffectingCombat() return combat end
function UnitIsAFK() return false end
function GetUnitSpeed() return speed, 7, 0, 4.7 end -- ClassicAPI: enables stand-still
local log = {}
DEFAULT_CHAT_FRAME = { AddMessage = function(_, m) table.insert(log, m); print(" " .. m) end }
SlashCmdList = {}
FRAMES = {}
function CreateFrame()
local f = { scripts = {} }
function f:RegisterEvent() end
function f:UnregisterEvent() end
function f:SetScript(k, fn) self.scripts[k] = fn end
table.insert(FRAMES, f)
return f
end
-- the call stack another addon's collectgarbage() call is made from
local caller = nil
function debugstack()
local s = "Interface\\AddOns\\GentleGC\\GentleGC.lua:400: in function `WrappedCollect'\n"
if caller then s = s .. "Interface\\AddOns\\" .. caller .. "\\core.lua:12: in function `FreeMemory'\n" end
return s .. "[C]: ?\n"
end
-- a slow collector: every full collection advances the clock by `gcCost`
local gcCost = 0.3
local origCG = collectgarbage
collectgarbage = function(n)
if n == nil or n == 0 then now = now + gcCost end
return origCG(n)
end
local unwrappedCG = collectgarbage -- what an addon that localized it before us holds
LIVE = {}
for i = 1, 300000 do LIVE[i] = { i } end
origCG()
dofile(arg[1])
local F = FRAMES[1]
local function fire(e, a1) event, arg1 = e, a1; this = F; F.scripts.OnEvent() end
fire("ADDON_LOADED", "GentleGC")
local SF = FRAMES[4]
local cmd = SlashCmdList.GENTLEGC
cmd("debug 2")
local function count(pat) local n = 0; for _, m in ipairs(log) do if string.find(m, pat) then n = n + 1 end end; return n end
local function lastMatch(pat) for i = table.getn(log), 1, -1 do if string.find(log[i], pat) then return log[i] end end; return "" end
local function check(name, c) print((c and "PASS " or "FAIL ") .. name); if not c then FAILED = true end end
-- one rendered frame: per-frame sampler, then the main frame's OnUpdate
local HF = FRAMES[2]
local function frame(dt)
now = now + dt
SF.scripts.OnUpdate()
arg1 = dt; this = F
F.scripts.OnUpdate()
if HF.scripts.OnUpdate then HF.scripts.OnUpdate() end
end
local function seconds(n) for i = 1, n * 10 do frame(0.1) end end
local function garbageMB(mb)
local t = {}
for i = 1, mb * 7000 do t[i] = { i, i } end
end
local function liveMB() local _, _, v = string.find(lastMatch("heap .* live ~"), "live ~([%d%.]+)mb"); return tonumber(v) end
---------------------------------------------------------------------------
print("1. slow collections switch to careful mode")
garbageMB(8)
fire("PLAYER_LEAVING_WORLD")
fire("PLAYER_ENTERING_WORLD")
check("loading-screen collection ran", count("loading screen:") >= 1)
cmd("status")
check("status shows careful mode", string.find(lastMatch("collections take"), "~[23]%d%dms: careful mode") ~= nil)
print("2. careful mode: no stand-still or idle collections")
garbageMB(8)
speed = 0
seconds(10)
check("no stand-still collection", count("standing still:") == 0)
now = now + 400
seconds(3)
check("no idle collection", count("idle:") == 0)
speed = 7
print("3. careful mode: NPC windows and pressure still collect")
fire("MERCHANT_SHOW")
check("merchant collection ran", count("merchant:") == 1)
local _, thr = gcinfo()
do
local junk = {}
local i = 0
while gcinfo() < thr * 0.9 do
for k = 1, 500 do i = i + 1; junk[i] = { i } end
if i > 30000 then junk = {}; i = 0 end
SF.scripts.OnUpdate()
end
end
seconds(6)
check("pressure collection ran", count("pressure:") == 1)
print("4. fast collections switch back to normal")
gcCost = 0.01
for k = 1, 6 do now = now + 40; garbageMB(8); fire("MAIL_SHOW") end
cmd("status")
check("back to normal mode", string.find(lastMatch("collections take"), "normal mode") ~= nil)
garbageMB(8)
now = now + 40
speed = 0
seconds(5)
check("stand-still collection runs again", count("standing still:") == 1)
speed = 7
print("5. /ggc slow 0 turns careful mode off")
gcCost = 0.3
cmd("slow 0")
for k = 1, 3 do now = now + 40; garbageMB(8); fire("BANKFRAME_OPENED") end
cmd("status")
check("normal mode despite slow collections", string.find(lastMatch("collections take"), "normal mode") ~= nil)
cmd("slow 100")
gcCost = 0.01
---------------------------------------------------------------------------
print("6. another addon's collectgarbage() is attributed, not 'automatic'")
local autoBefore = count("automatic GC detected")
garbageMB(10)
seconds(1)
caller = "Wrekkit"
collectgarbage()
caller = nil
seconds(2)
check("logged as Wrekkit's collection", string.find(lastMatch("collection by"), "collection by Wrekkit: freed") ~= nil)
check("not counted as automatic", count("automatic GC detected") == autoBefore)
cmd("status")
check("status lists it by name", string.find(lastMatch("by other addons"), "Wrekkit 1") ~= nil)
local real = gcinfo() / 1024
check("live estimate matches real live memory", math.abs(liveMB() - real) < 1)
print("7. an addon setting the threshold (no collection) changes nothing")
local liveBefore = liveMB()
caller = "FpsWatch"
local u = gcinfo()
collectgarbage(u * 3)
caller = nil
seconds(2)
cmd("status")
check("no collection counted", string.find(lastMatch("by other addons"), "FpsWatch") == nil)
check("no automatic GC counted", count("automatic GC detected") == autoBefore)
check("live estimate unchanged", math.abs(liveMB() - liveBefore) < 0.5)
origCG(math.floor(u * 1.5) + 2048) -- (back to a normal threshold for the next steps)
seconds(1)
print("8. an addon we couldn't wrap: detected from the heap dropping below the threshold")
garbageMB(10)
seconds(1)
unwrappedCG()
seconds(2)
check("counted as another addon's (unknown)", string.find(lastMatch("collection by"), "collection by unknown addon") ~= nil)
check("still not counted as automatic", count("automatic GC detected") == autoBefore)
print("9. the game's collector firing at the threshold is still 'automatic'")
do
local _, t = gcinfo()
local junk = {}
local i = 0
local _, cur = gcinfo()
while cur == t do
for k = 1, 500 do i = i + 1; junk[i] = { i } end
if i > 30000 then junk = {}; i = 0 end
SF.scripts.OnUpdate()
_, cur = gcinfo()
end
end
seconds(1)
check("counted as automatic", count("automatic GC detected") == autoBefore + 1)
print("10. another addon collecting in combat: combat headroom is restored")
combat = true; fire("PLAYER_REGEN_DISABLED")
garbageMB(6)
caller = "Wrekkit"
collectgarbage()
caller = nil
local used, t = gcinfo()
check("threshold raised again after it", t >= used + 32 * 1024 - 64)
combat = false; fire("PLAYER_REGEN_ENABLED")
seconds(1)
print("11. freeze detector names the addon")
cmd("hitch")
seconds(1)
garbageMB(10)
caller = "Wrekkit"
now = now + 0.4
collectgarbage()
caller = nil
frame(0.05)
check("freeze blamed on Wrekkit", string.find(lastMatch("freeze"), "run by Wrekkit") ~= nil)
cmd("hitch 0")
---------------------------------------------------------------------------
print("12. garbage creation rate")
cmd("allocwarn 1")
now = now + 120
for s = 1, 40 do
for f = 1, 10 do
garbageMB(0.2) -- ~2 MB/s
frame(0.1)
end
end
cmd("status")
local _, _, rate = string.find(lastMatch("garbage created"), "([%d%.]+) MB/s now")
rate = tonumber(rate)
check("rate is measured (about 2 MB/s)", rate and rate > 1.2 and rate < 3.5)
check("warning shown above the limit", count("garbage is being created at") == 1)
for s = 1, 20 do for f = 1, 10 do garbageMB(0.2); frame(0.1) end end
check("warning not repeated within 10 minutes", count("garbage is being created at") == 1)
---------------------------------------------------------------------------
print("13. a wrapper that blocks collectgarbage is reported, not counted")
FRAMES = {}
log = {}
DEFAULT_CHAT_FRAME = { AddMessage = function(_, m) table.insert(log, m); print(" " .. m) end }
GentleGCDB = nil
SlashCmdList = {}
collectgarbage = function(n) end -- an addon loaded before us that swallows every call
dofile(arg[1])
F = FRAMES[1]
fire("ADDON_LOADED", "GentleGC")
SlashCmdList.GENTLEGC("debug 1")
garbageMB(8)
SlashCmdList.GENTLEGC("now")
check("reported as blocked", count("did not run") == 1)
SlashCmdList.GENTLEGC("status")
check("not counted as a collection", string.find(lastMatch("session:"), "0 collections") ~= nil)
check("ratio not learned from it", count("not measured yet") == 1)
print(FAILED and "SOME TESTS FAILED" or "ALL TESTS PASSED")