Apply Patch
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
## Interface: 11200
|
||||
## Title: Gentle GC - Memory
|
||||
## Notes: Measures how much memory each addon keeps, at the loading screen. Results: /ggc addons (needs Gentle GC)
|
||||
## Author: GentleGC contributors
|
||||
## Version: 2.5
|
||||
## SavedVariables: GentleGCMemoryDB
|
||||
|
||||
GentleGC_Memory.lua
|
||||
@@ -0,0 +1,118 @@
|
||||
--[[
|
||||
Gentle GC - Memory: how much memory each addon keeps.
|
||||
|
||||
The 1.12 client has no per-addon memory numbers (GetAddOnMemoryUsage came
|
||||
in 2.x); gcinfo() only gives the size of the whole Lua heap. So this addon
|
||||
loads before the others (the "!!!!" sorts first) and, each time another
|
||||
addon finishes loading, runs a full collection and reads gcinfo(). The rise
|
||||
since the previous addon is what that addon kept: its code, its data and
|
||||
its saved variables. Same approach as AddonMemory by Salahaja.
|
||||
|
||||
It only measures at the loading screen, where the extra collections can't
|
||||
be seen, and only when it's useful:
|
||||
* the first login after it's installed
|
||||
* when the set of enabled addons has changed since the last measurement
|
||||
* the next /reload after /ggc addons measure
|
||||
Any other time it does nothing. Gentle GC reads the results (/ggc addons).
|
||||
--]]
|
||||
|
||||
local ME = "!!!!GentleGC_Memory"
|
||||
|
||||
local M = { measuring = false, justMeasured = false }
|
||||
GentleGCMemory = M
|
||||
|
||||
-- Our own reference, taken before Gentle GC wraps the global, so these
|
||||
-- measuring collections aren't reported as another addon's.
|
||||
local collect = collectgarbage
|
||||
|
||||
local run, last
|
||||
local frame = CreateFrame("Frame")
|
||||
|
||||
local function Live()
|
||||
collect()
|
||||
return (gcinfo())
|
||||
end
|
||||
|
||||
-- The enabled addons, as one string: a change means a new measurement.
|
||||
local function Signature()
|
||||
local names = {}
|
||||
for i = 1, GetNumAddOns() do
|
||||
local name, _, _, enabled = GetAddOnInfo(i)
|
||||
if name and enabled then table.insert(names, name) end
|
||||
end
|
||||
table.sort(names)
|
||||
return table.concat(names, ",")
|
||||
end
|
||||
|
||||
local function Start(db)
|
||||
local sig = Signature()
|
||||
local reason
|
||||
if db.measureNext then
|
||||
reason = "requested"
|
||||
elseif not db.measured then
|
||||
reason = "first login"
|
||||
elseif db.signature ~= sig then
|
||||
reason = "addons changed"
|
||||
end
|
||||
db.measureNext = nil
|
||||
if not reason then return end
|
||||
|
||||
M.measuring = true
|
||||
run = {
|
||||
when = date and date("%Y-%m-%d %H:%M") or "?",
|
||||
reason = reason,
|
||||
base = Live(),
|
||||
before = {},
|
||||
rows = {},
|
||||
stages = {},
|
||||
}
|
||||
last = run.base
|
||||
-- anything that loaded before us is inside the base number; name it
|
||||
for i = 1, GetNumAddOns() do
|
||||
local name = GetAddOnInfo(i)
|
||||
if name and name ~= ME and IsAddOnLoaded(name) then table.insert(run.before, name) end
|
||||
end
|
||||
db.signature = sig
|
||||
end
|
||||
|
||||
local function Record(list, label)
|
||||
local kb = Live()
|
||||
table.insert(list, { label, kb - last })
|
||||
last = kb
|
||||
end
|
||||
|
||||
local function Finish()
|
||||
local total = 0
|
||||
for i = 1, table.getn(run.rows) do total = total + run.rows[i][2] end
|
||||
run.total = total
|
||||
run.after = last
|
||||
GentleGCMemoryDB.measured = run
|
||||
M.measuring = false
|
||||
M.justMeasured = true
|
||||
run = nil
|
||||
end
|
||||
|
||||
frame:RegisterEvent("ADDON_LOADED")
|
||||
frame:RegisterEvent("PLAYER_LOGIN")
|
||||
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
frame:SetScript("OnEvent", function()
|
||||
if event == "ADDON_LOADED" then
|
||||
if arg1 == ME then
|
||||
if type(GentleGCMemoryDB) ~= "table" then GentleGCMemoryDB = {} end
|
||||
Start(GentleGCMemoryDB)
|
||||
elseif M.measuring then
|
||||
-- this addon's files and saved variables; setup it does in its own
|
||||
-- ADDON_LOADED handler runs after ours and lands on the next addon
|
||||
Record(run.rows, arg1)
|
||||
end
|
||||
elseif event == "PLAYER_LOGIN" then
|
||||
if M.measuring then Record(run.stages, "after the last addon loaded (setup, saved variables)") end
|
||||
elseif event == "PLAYER_ENTERING_WORLD" then
|
||||
if M.measuring then
|
||||
Record(run.stages, "addons setting up at login")
|
||||
Finish()
|
||||
end
|
||||
-- load-on-demand addons come later, in play: never measured
|
||||
frame:UnregisterAllEvents()
|
||||
end
|
||||
end)
|
||||
+1015
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
## Interface: 11200
|
||||
## Title: Gentle GC
|
||||
## Notes: Schedules Lua garbage collection for moments you won't notice
|
||||
## Author: Weird Vibes (MarcelineVQ); forks by TheRealFayz, jrc13245
|
||||
## Version: 2.5
|
||||
## SavedVariables: GentleGCDB
|
||||
|
||||
GentleGC.lua
|
||||
@@ -0,0 +1,71 @@
|
||||
-- WoW + ClassicAPI stubs
|
||||
format = string.format
|
||||
local now = 1000
|
||||
function GetTime() return now end
|
||||
local combat, speed, falling = false, 7, false
|
||||
function UnitAffectingCombat() return combat end
|
||||
function UnitIsAFK() return false end
|
||||
function GetUnitSpeed(u) return speed, 7, 0, 4.7 end
|
||||
function IsFalling() return falling end
|
||||
local log = {}
|
||||
DEFAULT_CHAT_FRAME = { AddMessage = function(_, m) table.insert(log, m); print(" " .. m) end }
|
||||
SlashCmdList = {}
|
||||
local tickerFn, tickerPeriod
|
||||
C_Timer = { NewTicker = function(p, f) tickerPeriod, tickerFn = p, f; return {} end }
|
||||
FRAMES = {}
|
||||
function CreateFrame()
|
||||
local F = { scripts = {} }
|
||||
function F:RegisterEvent() end
|
||||
function F:UnregisterEvent() end
|
||||
function F:SetScript(k, f) self.scripts[k] = f end
|
||||
table.insert(FRAMES, F)
|
||||
return F
|
||||
end
|
||||
local function fire(e, a1) event, arg1 = e, a1; F.scripts.OnEvent() end
|
||||
local function seconds(n) for i = 1, n do now = now + 1; tickerFn() end end
|
||||
|
||||
LIVE = {}
|
||||
for i = 1, 300000 do LIVE[i] = { i } end
|
||||
local function makeGarbage(mb) local t = {}; for i = 1, mb * 6000 do t[i] = { i, i } end end
|
||||
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 check(name, c) print((c and "PASS " or "FAIL ") .. name); if not c then FAILED = true end end
|
||||
|
||||
collectgarbage()
|
||||
dofile(arg[1])
|
||||
F = FRAMES[1]
|
||||
HF = FRAMES[2]
|
||||
fire("ADDON_LOADED", "GentleGC")
|
||||
SlashCmdList.GENTLEGC("debug 2")
|
||||
|
||||
check("C_Timer ticker used (1s)", tickerFn ~= nil and tickerPeriod == 1)
|
||||
check("no frame OnUpdate when C_Timer exists", F.scripts.OnUpdate == nil)
|
||||
|
||||
makeGarbage(12)
|
||||
speed = 7; seconds(10)
|
||||
check("no stand-still collection while moving", count("standing still:") == 0)
|
||||
|
||||
speed = 0; seconds(2)
|
||||
check("not yet after 2s still", count("standing still:") == 0)
|
||||
seconds(2)
|
||||
check("collects after 3s standing still", count("standing still:") == 1)
|
||||
|
||||
seconds(20)
|
||||
check("does not repeat while still with no new garbage", count("standing still:") == 1)
|
||||
SlashCmdList.GENTLEGC("status")
|
||||
check("idle retries don't inflate skip count", count("collections, 0 skipped") == 1)
|
||||
|
||||
makeGarbage(12); seconds(15)
|
||||
check("respects minInterval, then collects again", count("standing still:") == 2)
|
||||
|
||||
makeGarbage(12); falling = true; speed = 0; now = now + 60; seconds(5)
|
||||
check("falling is not standing still", count("standing still:") == 2)
|
||||
falling = false
|
||||
|
||||
combat = true; fire("PLAYER_REGEN_DISABLED"); seconds(10)
|
||||
check("never while in combat", count("standing still:") == 2)
|
||||
combat = false; fire("PLAYER_REGEN_ENABLED")
|
||||
|
||||
SlashCmdList.GENTLEGC("still 0"); makeGarbage(12); now = now + 60; seconds(10)
|
||||
check("still 0 disables it", count("standing still:") == 2 or count("standing still:") == 3 and false)
|
||||
|
||||
print(FAILED and "SOME TESTS FAILED" or "ALL TESTS PASSED")
|
||||
@@ -0,0 +1,15 @@
|
||||
format = string.format; local now = 1000; function GetTime() return now end
|
||||
function UnitAffectingCombat() return true end function UnitIsAFK() return false end
|
||||
DEFAULT_CHAT_FRAME = { AddMessage = function(_, m) print(" " .. m) end }; SlashCmdList = {}
|
||||
local F; function CreateFrame() local G = { e = {} }; F = F or G; local F = G; function F:RegisterEvent(x) end; function F:UnregisterEvent() end; function F:SetScript(k, f) self[k] = f end; return F end
|
||||
collectgarbage(); dofile(arg[1]); event, arg1 = "ADDON_LOADED", "GentleGC"; F.OnEvent()
|
||||
SlashCmdList.GENTLEGC("headroom 4"); SlashCmdList.GENTLEGC("debug 2")
|
||||
event = "PLAYER_REGEN_DISABLED"; F.OnEvent()
|
||||
local _, t1 = gcinfo()
|
||||
local keep = {} for i = 1, 60000 do keep[i] = {i} end -- live growth past raised threshold
|
||||
keep = nil; local g = {} for i = 1, 60000 do g[i] = {i} end g = nil
|
||||
arg1 = 1.1; F.OnUpdate()
|
||||
local used, t2 = gcinfo()
|
||||
print("re-raised headroom after in-combat auto GC:", t2 >= used + 4*1024 - 1 and "PASS" or "FAIL", t1, t2, used)
|
||||
event = "PLAYER_REGEN_ENABLED"; F.OnEvent()
|
||||
SlashCmdList.GENTLEGC("status")
|
||||
@@ -0,0 +1,128 @@
|
||||
-- Collector-ratio tests: simulates the 1.12 client's collector, which sets the
|
||||
-- threshold to 1.25x live (at least) instead of stock Lua 5.0's 2x.
|
||||
-- Run: lua5.0 tests/test_factor.lua GentleGC.lua
|
||||
format = string.format
|
||||
local now = 1000
|
||||
function GetTime() return now end
|
||||
function UnitAffectingCombat() return false end
|
||||
function UnitIsAFK() return false end
|
||||
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
|
||||
|
||||
local FACTOR = 1.25
|
||||
local origCG = collectgarbage
|
||||
local function setThreshold() local u = gcinfo(); origCG(math.floor(u * FACTOR)) end
|
||||
-- the modded collector: a full collection leaves the threshold at FACTOR x live
|
||||
collectgarbage = function(n)
|
||||
if n == nil or n == 0 then origCG(); setThreshold() else origCG(n) end
|
||||
end
|
||||
-- the game's own automatic collection under the mod
|
||||
local function autoGC() origCG(); setThreshold() end
|
||||
|
||||
LIVE = {}
|
||||
for i = 1, 300000 do LIVE[i] = { i } end
|
||||
autoGC()
|
||||
|
||||
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 cmd = SlashCmdList.GENTLEGC
|
||||
cmd("debug 2")
|
||||
SF = FRAMES[4]
|
||||
|
||||
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
|
||||
local function tick(sec) local n = sec * 10; for i = 1, n do now = now + 0.1; arg1 = 0.1; this = F; F.scripts.OnUpdate() end end
|
||||
local function liveMB() local _, _, v = string.find(lastMatch("heap .* live ~"), "live ~([%d%.]+)mb"); return tonumber(v) end
|
||||
local function realLiveMB() local u = gcinfo(); return u / 1024 end
|
||||
-- heap creeps toward the threshold over frames (the sampler sees each one),
|
||||
-- then the game's collector fires at the threshold
|
||||
local SF = FRAMES[4]
|
||||
local keepAlive = {}
|
||||
local function growToThresholdThenAutoGC()
|
||||
local _, t = gcinfo()
|
||||
local junk = {}
|
||||
local i = 0
|
||||
while gcinfo() < t * 0.98 do
|
||||
for k = 1, 300 do i = i + 1; junk[i] = { i } end
|
||||
if SF then SF.scripts.OnUpdate() end
|
||||
end
|
||||
junk = nil
|
||||
autoGC()
|
||||
end
|
||||
-- garbage that stays below the 1.25x threshold so Lua never collects on its own
|
||||
local function garbage(kb)
|
||||
local target = gcinfo() + kb
|
||||
local t = {}
|
||||
local i = 0
|
||||
while gcinfo() < target do i = i + 1; t[i] = { i, i } end
|
||||
end
|
||||
|
||||
print("1. automatic GC before GentleGC has measured the ratio")
|
||||
now = now + 60
|
||||
growToThresholdThenAutoGC()
|
||||
tick(1.1)
|
||||
check("automatic GC detected", count("automatic GC detected") == 1)
|
||||
cmd("status")
|
||||
local _, thr = gcinfo()
|
||||
check("live estimate is not threshold/2", liveMB() > (thr / 2) / 1024 + 1)
|
||||
check("status says the ratio isn't measured yet", count("not measured yet") == 1)
|
||||
|
||||
print("2. no useless collection right after it (the bug)")
|
||||
now = now + 60
|
||||
fire("MERCHANT_SHOW")
|
||||
check("skipped: almost no garbage", count("merchant:") == 0)
|
||||
|
||||
print("3. own collection measures the ratio")
|
||||
cmd("now")
|
||||
cmd("status")
|
||||
check("ratio measured as 1.25x", string.find(lastMatch("collector:"), "threshold 1%.25x live after the last full collection, lowest 1%.25x") ~= nil)
|
||||
check("garbage minimum capped at half the 25% room", string.find(lastMatch("collector:"), "collecting from 12%.5%% garbage") ~= nil)
|
||||
|
||||
print("4. automatic GC after measuring: live estimate is right")
|
||||
growToThresholdThenAutoGC()
|
||||
tick(1.1)
|
||||
cmd("status")
|
||||
local real = realLiveMB()
|
||||
check("live estimate within 1mb of real live memory", math.abs(liveMB() - real) < 1)
|
||||
now = now + 60
|
||||
fire("BANKFRAME_OPENED")
|
||||
check("still no useless collection", count("bank:") == 0)
|
||||
|
||||
print("5. enough garbage: normal collections still happen")
|
||||
local live = gcinfo()
|
||||
garbage(math.floor(live * 0.14)) -- 14% of live: above the 12.5% minimum
|
||||
now = now + 60
|
||||
fire("MAIL_SHOW")
|
||||
check("mailbox collection ran", count("mailbox:") == 1)
|
||||
check("it freed real garbage", string.find(lastMatch("mailbox:"), "freed [1-9]") ~= nil)
|
||||
|
||||
print("6. pressure fires before the 1.25x threshold")
|
||||
now = now + 60
|
||||
local l2, t2 = gcinfo()
|
||||
-- pressure point = live + 70% of the room = 1.175x live; stop short of 1.25x
|
||||
garbage(math.floor(l2 * 0.19))
|
||||
local used = gcinfo()
|
||||
check("heap is between pressure point and threshold", used > l2 * 1.175 and used < t2)
|
||||
tick(1.1)
|
||||
check("pressure collection ran", count("pressure:") == 1)
|
||||
check("pressure collection freed garbage", string.find(lastMatch("pressure:"), "freed [1-9]") ~= nil)
|
||||
check("no automatic GC needed", count("automatic GC detected") == 2)
|
||||
|
||||
print("7. pressure can't loop")
|
||||
tick(10)
|
||||
check("one pressure collection only", count("pressure:") == 1)
|
||||
|
||||
print(FAILED and "SOME TESTS FAILED" or "ALL TESTS PASSED")
|
||||
@@ -0,0 +1,232 @@
|
||||
-- Freeze detector tests. Run: lua5.0 tests/test_hitch.lua GentleGC.lua
|
||||
format = string.format
|
||||
local now = 1000
|
||||
function GetTime() return now end
|
||||
local combat = false
|
||||
function UnitAffectingCombat() return combat end
|
||||
function UnitIsAFK() return false end
|
||||
local log = {}
|
||||
DEFAULT_CHAT_FRAME = { AddMessage = function(_, m) table.insert(log, m); print(" " .. m) end }
|
||||
SlashCmdList = {}
|
||||
|
||||
-- a dragged window: an anonymous child of a named frame
|
||||
local auxFrame = { GetName = function() return "AuxFrame" end }
|
||||
local auxButton = { GetName = function() return nil end, GetParent = function() return auxFrame end }
|
||||
local focus = nil
|
||||
function GetMouseFocus() return focus end
|
||||
|
||||
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
|
||||
|
||||
LIVE = {}
|
||||
for i = 1, 300000 do LIVE[i] = { i } end
|
||||
collectgarbage()
|
||||
dofile(arg[1])
|
||||
local F, HF = FRAMES[1], FRAMES[2]
|
||||
local function fire(e, a1) event, arg1 = e, a1; F.scripts.OnEvent() end
|
||||
fire("ADDON_LOADED", "GentleGC")
|
||||
local cmd = SlashCmdList.GENTLEGC
|
||||
|
||||
-- one rendered frame taking `sec` seconds; `during` runs inside it
|
||||
local function frame(sec, during)
|
||||
if during then during() end
|
||||
now = now + sec
|
||||
arg1 = sec
|
||||
if F.scripts.OnUpdate then F.scripts.OnUpdate() end
|
||||
if HF.scripts.OnUpdate then HF.scripts.OnUpdate() end
|
||||
end
|
||||
local function frames(n) for i = 1, n do frame(0.016) end end
|
||||
local function last() return log[table.getn(log)] or "" end
|
||||
local function freezes() local n = 0; for _, m in ipairs(log) do if string.find(m, "freeze %d") then n = n + 1 end end; return n end
|
||||
local function check(name, c) print((c and "PASS " or "FAIL ") .. name); if not c then FAILED = true end end
|
||||
|
||||
check("off by default", HF.scripts.OnUpdate == nil)
|
||||
cmd("hitch")
|
||||
check("/ggc hitch turns it on at 250ms", HF.scripts.OnUpdate ~= nil and GentleGCDB.hitchMs == 250)
|
||||
|
||||
frames(5); frame(0.2); frames(3)
|
||||
check("normal and 200ms frames are not reported", freezes() == 0)
|
||||
|
||||
print("-- freeze with no GC while dragging AuxFrame")
|
||||
focus = auxButton
|
||||
frame(2.0)
|
||||
check("2s freeze reported", freezes() == 1)
|
||||
check("blamed on something other than GC", string.find(last(), "no garbage collection") ~= nil)
|
||||
check("names the window under the mouse", string.find(last(), "mouse over AuxFrame") ~= nil)
|
||||
focus = nil
|
||||
frames(3)
|
||||
|
||||
print("-- freeze containing the game's automatic GC")
|
||||
-- the heap creeps up to just below the threshold over normal frames...
|
||||
local SF = FRAMES[4]
|
||||
do
|
||||
local junk = {}
|
||||
local _, t = gcinfo()
|
||||
while gcinfo() < t * 0.97 do
|
||||
for i = 1, 500 do table.insert(junk, { i }) end
|
||||
SF.scripts.OnUpdate()
|
||||
end
|
||||
junk = nil
|
||||
end
|
||||
-- ...and the collector fires inside a long frame
|
||||
frame(0.6, function()
|
||||
local _, t = gcinfo()
|
||||
local junk = {}
|
||||
while true do -- allocate until Lua collects on its own
|
||||
local used, thr = gcinfo()
|
||||
if thr ~= t then break end
|
||||
table.insert(junk, { 1, 2, 3 })
|
||||
if table.getn(junk) > 5000 then junk = {} end
|
||||
end
|
||||
end)
|
||||
check("automatic GC identified", string.find(last(), "the game's automatic garbage collection") ~= nil)
|
||||
frames(3)
|
||||
|
||||
print("-- freeze containing a GentleGC collection")
|
||||
local g = {}; for i = 1, 60000 do g[i] = { i } end; g = nil
|
||||
frame(0.4, function() cmd("now") end)
|
||||
check("GentleGC collection identified", string.find(last(), "GentleGC collection %(manual%)") ~= nil)
|
||||
frames(3)
|
||||
|
||||
print("-- freeze that allocates a lot without collecting")
|
||||
local keep = {}
|
||||
frame(0.5, function() for i = 1, 40000 do keep[i] = { i, i } end end)
|
||||
check("allocation reported", string.find(last(), "allocated during it") ~= nil)
|
||||
check("still not blamed on GC", string.find(last(), "no garbage collection") ~= nil)
|
||||
keep = nil
|
||||
frames(3)
|
||||
|
||||
print("-- entering combat (threshold raise) inside a freeze is not a GC")
|
||||
combat = true
|
||||
frame(0.5, function() fire("PLAYER_REGEN_DISABLED") end)
|
||||
check("threshold raise not mistaken for GC", string.find(last(), "no garbage collection") ~= nil)
|
||||
check("marked in combat", string.find(last(), "%(in combat%)") ~= nil)
|
||||
combat = false; fire("PLAYER_REGEN_ENABLED"); frames(3)
|
||||
|
||||
print("-- loading screens are ignored")
|
||||
local before = freezes()
|
||||
fire("PLAYER_LEAVING_WORLD"); frame(15); frame(5)
|
||||
fire("PLAYER_ENTERING_WORLD"); frame(1.5); frame(1.0); frame(0.8)
|
||||
check("no reports for loading screen or the frames right after", freezes() == before)
|
||||
frame(1.0)
|
||||
check("reports again afterwards", freezes() == before + 1)
|
||||
|
||||
print("-- history and off")
|
||||
local n0 = table.getn(log)
|
||||
cmd("hitches")
|
||||
check("/ggc hitches lists recorded freezes", table.getn(log) - n0 >= 5)
|
||||
cmd("hitch 0")
|
||||
check("/ggc hitch 0 turns it off", HF.scripts.OnUpdate == nil)
|
||||
local b = freezes(); frame(3); check("no reports when off", freezes() == b)
|
||||
cmd("hitch 500"); frame(0.4); check("custom limit respected (400ms < 500ms)", freezes() == b)
|
||||
|
||||
print(FAILED and "SOME TESTS FAILED" or "ALL TESTS PASSED")
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
print("== event recording (client with RegisterAllEvents)")
|
||||
-- reload the addon on frames that support RegisterAllEvents
|
||||
FRAMES = {}
|
||||
log = {}
|
||||
DEFAULT_CHAT_FRAME = { AddMessage = function(_, m) table.insert(log, m); print(" " .. m) end }
|
||||
local regAll = 0
|
||||
function CreateFrame()
|
||||
local f = { scripts = {}, all = false }
|
||||
function f:RegisterEvent() end
|
||||
function f:UnregisterEvent() end
|
||||
function f:RegisterAllEvents() self.all = true; regAll = regAll + 1 end
|
||||
function f:UnregisterAllEvents() self.all = false end
|
||||
function f:SetScript(k, fn) self.scripts[k] = fn end
|
||||
table.insert(FRAMES, f)
|
||||
return f
|
||||
end
|
||||
GentleGCDB = nil
|
||||
SlashCmdList = {}
|
||||
dofile(arg[1])
|
||||
F, HF = FRAMES[1], FRAMES[2]
|
||||
local EF = FRAMES[3]
|
||||
event, arg1 = "ADDON_LOADED", "GentleGC"; F.scripts.OnEvent()
|
||||
cmd = SlashCmdList.GENTLEGC
|
||||
local function last2() return log[table.getn(log)] or "" end
|
||||
local function ev(name, a1, dt)
|
||||
if dt then now = now + dt end
|
||||
if EF.all and EF.scripts.OnEvent then event, arg1 = name, a1; EF.scripts.OnEvent() end
|
||||
end
|
||||
local function endFrame(dt)
|
||||
now = now + (dt or 0)
|
||||
arg1 = 0.016
|
||||
if HF.scripts.OnUpdate then HF.scripts.OnUpdate() end
|
||||
end
|
||||
local function quiet(n) for i = 1, n do now = now + 0.016; endFrame() end end
|
||||
|
||||
check("event frame not listening while detector off", EF.all == false)
|
||||
cmd("hitch")
|
||||
check("RegisterAllEvents on /ggc hitch", EF.all == true and regAll == 1)
|
||||
quiet(3)
|
||||
|
||||
-- 0.5s spent right after an addon message
|
||||
ev("UNIT_AURA", "raid3", 0.005)
|
||||
ev("UNIT_AURA", "raid7", 0.005)
|
||||
ev("CHAT_MSG_ADDON", "BigWigs", 0.005)
|
||||
ev("UNIT_HEALTH", "raid1", 0.50)
|
||||
endFrame(0.01)
|
||||
local l = last2()
|
||||
check("lists events with counts", string.find(l, "4 events: UNIT_AURA x2") ~= nil)
|
||||
check("finds the longest gap after CHAT_MSG_ADDON (BigWigs)", string.find(l, "longest gap 0%.50s after CHAT_MSG_ADDON %(BigWigs%)") ~= nil)
|
||||
quiet(2)
|
||||
|
||||
-- freeze with no events at all
|
||||
endFrame(0.6)
|
||||
check("no events reported", string.find(last2(), "no game events fired during it") ~= nil)
|
||||
quiet(2)
|
||||
|
||||
-- time lost before the first event
|
||||
ev("BAG_UPDATE", 0, 0.7)
|
||||
endFrame(0.01)
|
||||
check("gap before the first event", string.find(last2(), "longest gap 0%.70s before the first event") ~= nil)
|
||||
quiet(2)
|
||||
|
||||
-- time lost after the last event
|
||||
ev("GUILD_ROSTER_UPDATE", nil, 0.01)
|
||||
endFrame(0.45)
|
||||
check("gap after the last event", string.find(last2(), "after GUILD_ROSTER_UPDATE, the last event") ~= nil)
|
||||
quiet(2)
|
||||
|
||||
-- memory jump attributed to an event
|
||||
local keep
|
||||
ev("PLAYER_TARGET_CHANGED", nil, 0.01)
|
||||
ev("AUCTION_ITEM_LIST_UPDATE", nil, 0.01)
|
||||
keep = {}; for i = 1, 30000 do keep[i] = { i, i } end
|
||||
ev("UNIT_AURA", "target", 0.4)
|
||||
endFrame(0.01)
|
||||
check("allocation attributed to AUCTION_ITEM_LIST_UPDATE", string.find(last2(), "allocated after AUCTION_ITEM_LIST_UPDATE") ~= nil)
|
||||
keep = nil
|
||||
quiet(2)
|
||||
|
||||
-- more events than the ring holds
|
||||
for i = 1, 100 do ev("COMBAT_TEXT_UPDATE", "x", 0.003) end
|
||||
endFrame(0.01)
|
||||
check("ring overflow reported", string.find(last2(), "100 events: COMBAT_TEXT_UPDATE x64 %(last 64 kept%)") ~= nil)
|
||||
quiet(2)
|
||||
|
||||
-- long argument is shortened
|
||||
ev("CHAT_MSG_ADDON", "AVeryLongAddonPrefixThatGoesOnAndOn", 0.01)
|
||||
endFrame(0.5)
|
||||
check("long argument shortened", string.find(last2(), "AVeryLongAddonPrefixThat%.%.%.") ~= nil)
|
||||
quiet(2)
|
||||
|
||||
-- events from an earlier frame aren't blamed
|
||||
ev("SPELLS_CHANGED", nil, 0.01); endFrame(0.01)
|
||||
endFrame(0.5)
|
||||
check("previous frame's events not included", string.find(last2(), "no game events fired during it") ~= nil)
|
||||
|
||||
cmd("hitch 0")
|
||||
check("UnregisterAllEvents on /ggc hitch 0", EF.all == false)
|
||||
|
||||
print(FAILED and "SOME TESTS FAILED" or "ALL TESTS PASSED")
|
||||
@@ -0,0 +1,154 @@
|
||||
-- Per-addon memory: the !!!!GentleGC_Memory companion measuring a simulated
|
||||
-- login, and Gentle GC reporting it.
|
||||
-- Run: lua5.0 tests/test_memory.lua GentleGC.lua "!!!!GentleGC_Memory/GentleGC_Memory.lua"
|
||||
local MAIN, COMPANION = arg[1], arg[2]
|
||||
format = string.format
|
||||
local now = 1000
|
||||
function GetTime() return now end
|
||||
function UnitAffectingCombat() return false end
|
||||
function UnitIsAFK() return false end
|
||||
function date() return "2026-10-21 12:00" end
|
||||
local log = {}
|
||||
DEFAULT_CHAT_FRAME = { AddMessage = function(_, m) table.insert(log, m); print(" " .. m) end }
|
||||
|
||||
local origCG = collectgarbage
|
||||
local ADDONS, LOADED
|
||||
|
||||
-- frames, with events dispatched in registration order like the client
|
||||
local FRAMES
|
||||
function CreateFrame()
|
||||
local f = { scripts = {}, events = {} }
|
||||
function f:RegisterEvent(e) self.events[e] = true end
|
||||
function f:UnregisterEvent(e) self.events[e] = nil end
|
||||
function f:UnregisterAllEvents() self.events = {} end
|
||||
function f:RegisterAllEvents() end
|
||||
function f:SetScript(k, fn) self.scripts[k] = fn end
|
||||
table.insert(FRAMES, f)
|
||||
return f
|
||||
end
|
||||
local function fire(e, a1)
|
||||
for _, f in ipairs(FRAMES) do
|
||||
if f.events[e] and f.scripts.OnEvent then event, arg1 = e, a1; this = f; f.scripts.OnEvent() end
|
||||
end
|
||||
end
|
||||
|
||||
function GetNumAddOns() return table.getn(ADDONS) end
|
||||
function GetAddOnInfo(i)
|
||||
local a = type(i) == "number" and ADDONS[i] or nil
|
||||
if not a then return nil end
|
||||
return a.name, a.name, "", a.enabled and 1 or nil
|
||||
end
|
||||
function IsAddOnLoaded(name) return LOADED[name] and 1 or nil end
|
||||
|
||||
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
|
||||
|
||||
-- an addon's data: tables kept in a global, roughly `mb` megabytes
|
||||
KEEP = {}
|
||||
local function allocate(name, mb)
|
||||
local t = {}
|
||||
for i = 1, mb * 9000 do t[i] = { i } end
|
||||
KEEP[name] = t
|
||||
end
|
||||
|
||||
-- one full login: every enabled addon in order, then login and world events
|
||||
-- sizes: MB per addon; withCompanion: install the companion
|
||||
local function login(sizes, withCompanion)
|
||||
FRAMES, LOADED, KEEP = {}, {}, {}
|
||||
collectgarbage = origCG
|
||||
SlashCmdList = {}
|
||||
GentleGCMemory = nil
|
||||
origCG()
|
||||
for _, a in ipairs(ADDONS) do
|
||||
if a.enabled and (withCompanion or a.name ~= "!!!!GentleGC_Memory") then
|
||||
if a.name == "!!!!GentleGC_Memory" then
|
||||
dofile(COMPANION)
|
||||
elseif a.name == "GentleGC" then
|
||||
dofile(MAIN)
|
||||
else
|
||||
allocate(a.name, sizes[a.name] or 0)
|
||||
end
|
||||
LOADED[a.name] = true
|
||||
fire("ADDON_LOADED", a.name)
|
||||
end
|
||||
end
|
||||
allocate("variables", 0.5)
|
||||
fire("PLAYER_LOGIN")
|
||||
allocate("login setup", 3)
|
||||
fire("PLAYER_ENTERING_WORLD")
|
||||
end
|
||||
|
||||
local SIZES = { ["!!!ClassicAPI"] = 1, Atlas = 2, Bagshui = 5, pfQuest = 20, Wrekkit = 1 }
|
||||
ADDONS = {
|
||||
{ name = "!!!ClassicAPI", enabled = true },
|
||||
{ name = "!!!!GentleGC_Memory", enabled = true },
|
||||
{ name = "Atlas", enabled = true },
|
||||
{ name = "Bagshui", enabled = true },
|
||||
{ name = "GentleGC", enabled = true },
|
||||
{ name = "pfQuest", enabled = true },
|
||||
{ name = "Wrekkit", enabled = true },
|
||||
}
|
||||
-- (the client loads "!!!!" before "!!!", but the test puts ClassicAPI first to
|
||||
-- check the "loaded before" list)
|
||||
|
||||
local function rowMB(name)
|
||||
local m = GentleGCMemoryDB.measured
|
||||
for i = 1, table.getn(m.rows) do if m.rows[i][1] == name then return m.rows[i][2] / 1024 end end
|
||||
end
|
||||
local function near(v, want) return v and math.abs(v - want) <= math.max(0.35, want * 0.15) end
|
||||
|
||||
print("1. first login with the companion: measured")
|
||||
login(SIZES, true)
|
||||
local m = GentleGCMemoryDB and GentleGCMemoryDB.measured
|
||||
check("a measurement was saved", m ~= nil and table.getn(m.rows) >= 5)
|
||||
check("first login is the reason", m and m.reason == "first login")
|
||||
check("Atlas ~2 MB", near(rowMB("Atlas"), 2))
|
||||
check("Bagshui ~5 MB", near(rowMB("Bagshui"), 5))
|
||||
check("pfQuest ~20 MB", near(rowMB("pfQuest"), 20))
|
||||
check("Wrekkit ~1 MB", near(rowMB("Wrekkit"), 1))
|
||||
check("ClassicAPI listed as loaded before", m and m.before[1] == "!!!ClassicAPI")
|
||||
check("summary printed after loading", string.find(lastMatch("measured your addons"), "Biggest: pfQuest") ~= nil)
|
||||
check("companion stopped measuring", GentleGCMemory.measuring == false)
|
||||
|
||||
print("2. /ggc addons")
|
||||
local cmd = SlashCmdList.GENTLEGC
|
||||
cmd("addons")
|
||||
check("header line", count("addon memory at login on 2026%-10%-21") == 1)
|
||||
local first
|
||||
for i = 1, table.getn(log) do if string.find(log[i], "addon memory at login") then first = log[i + 1] end end
|
||||
check("biggest addon on the first row", first and string.find(first, "pfQuest") ~= nil)
|
||||
check("login stage shown", count("then addons setting up at login") == 1)
|
||||
check("loaded-before line", count("counted in the interface number: !!!ClassicAPI") == 1)
|
||||
cmd("")
|
||||
check("status names the biggest addons", string.find(lastMatch("biggest addons"), "pfQuest") ~= nil)
|
||||
check("measuring collections not counted as other addons'", count("by other addons") == 0)
|
||||
check("nor as automatic", string.find(lastMatch("session:"), "0 automatic") ~= nil)
|
||||
|
||||
print("3. next login, same addons: not measured again")
|
||||
local when = m
|
||||
login(SIZES, true)
|
||||
check("same measurement kept", GentleGCMemoryDB.measured == when)
|
||||
check("no summary this time", count("measured your addons") == 1)
|
||||
|
||||
print("4. an addon added: measured again")
|
||||
table.insert(ADDONS, { name = "ShaguTweaks", enabled = true })
|
||||
SIZES.ShaguTweaks = 3
|
||||
login(SIZES, true)
|
||||
check("new measurement", GentleGCMemoryDB.measured ~= when and GentleGCMemoryDB.measured.reason == "addons changed")
|
||||
check("ShaguTweaks ~3 MB", near(rowMB("ShaguTweaks"), 3))
|
||||
|
||||
print("5. /ggc addons measure: measured on the next reload")
|
||||
local before = GentleGCMemoryDB.measured
|
||||
SlashCmdList.GENTLEGC("addons measure")
|
||||
check("asks for a reload", count("type /reload to measure") == 1)
|
||||
login(SIZES, true)
|
||||
check("measured because it was requested", GentleGCMemoryDB.measured ~= before and GentleGCMemoryDB.measured.reason == "requested")
|
||||
|
||||
print("6. without the companion")
|
||||
GentleGCMemoryDB = nil
|
||||
login(SIZES, false)
|
||||
SlashCmdList.GENTLEGC("addons")
|
||||
check("explains the companion folder", count("needs the !!!!GentleGC_Memory folder") == 1)
|
||||
|
||||
print(FAILED and "SOME TESTS FAILED" or "ALL TESTS PASSED")
|
||||
@@ -0,0 +1,242 @@
|
||||
-- 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")
|
||||
@@ -0,0 +1,133 @@
|
||||
-- WoW API stubs ------------------------------------------------------------
|
||||
format = string.format
|
||||
local now = 1000
|
||||
function GetTime() return now end
|
||||
local combat, afk = false, false
|
||||
function UnitAffectingCombat() return combat end
|
||||
function UnitIsAFK() return afk end
|
||||
local log = {}
|
||||
DEFAULT_CHAT_FRAME = { AddMessage = function(self, m) table.insert(log, m); print(" chat: " .. m) end }
|
||||
SlashCmdList = {}
|
||||
FRAMES = {}
|
||||
function CreateFrame()
|
||||
local F = { scripts = {}, events = {} }
|
||||
function F:RegisterEvent(e) self.events[e] = true end
|
||||
function F:UnregisterEvent(e) self.events[e] = nil end
|
||||
function F:SetScript(k, f) self.scripts[k] = f end
|
||||
table.insert(FRAMES, F)
|
||||
return F
|
||||
end
|
||||
local function fire(e, a1)
|
||||
assert(F.events[e], "not registered: " .. e)
|
||||
event, arg1 = e, a1; this = F
|
||||
F.scripts.OnEvent()
|
||||
end
|
||||
local function update(sec) -- advance time in 0.1s frames
|
||||
local n = sec * 10
|
||||
for i = 1, n do now = now + 0.1; arg1 = 0.1; this = F; F.scripts.OnUpdate() end
|
||||
end
|
||||
|
||||
-- live data (~8 MB) and a garbage generator -------------------------------
|
||||
LIVE = {}
|
||||
for i = 1, 60000 do LIVE[i] = { i, "x" .. i } end
|
||||
local function makeGarbage(mb)
|
||||
local t = {}
|
||||
for i = 1, mb * 6000 do t[i] = { i, i } end -- roughly 1 MB per 6000 small tables
|
||||
end
|
||||
|
||||
collectgarbage()
|
||||
dofile(arg[1])
|
||||
F = FRAMES[1]
|
||||
HF = FRAMES[2]
|
||||
fire("ADDON_LOADED", "GentleGC")
|
||||
local cmd = SlashCmdList.GENTLEGC
|
||||
cmd("debug 2")
|
||||
|
||||
local function count() local n = 0; for _, m in ipairs(log) do if string.find(m, "%->") then n = n + 1 end end; return n end
|
||||
local function check(name, cond) print((cond and "PASS " or "FAIL ") .. name); if not cond then FAILED = true end end
|
||||
|
||||
print("1. login loading screen collects")
|
||||
makeGarbage(6); fire("PLAYER_ENTERING_WORLD"); check("collected at login", count() == 1)
|
||||
|
||||
print("2. small garbage is skipped")
|
||||
makeGarbage(1); now = now + 60; fire("MERCHANT_SHOW"); check("skipped (little garbage)", count() == 1)
|
||||
|
||||
print("3. interval respected")
|
||||
makeGarbage(6); fire("BANKFRAME_OPENED"); fire("MAIL_SHOW")
|
||||
check("one collection for two quick events", count() == 2)
|
||||
makeGarbage(6); fire("MAIL_SHOW"); check("within minInterval -> no collect", count() == 2)
|
||||
|
||||
print("4. combat: threshold raised, no automatic GC, deferred collection after")
|
||||
now = now + 100
|
||||
local used0, thr0 = gcinfo()
|
||||
fire("PLAYER_REGEN_DISABLED"); combat = true
|
||||
local _, thr1 = gcinfo()
|
||||
check("threshold raised by headroom", thr1 >= used0 + 32 * 1024 - 1)
|
||||
-- allocate past the ORIGINAL threshold but keep it alive so we can see it
|
||||
local keep = {}
|
||||
local target = thr0 + 1024
|
||||
while gcinfo() < target do table.insert(keep, { 1, 2, 3 }) end
|
||||
local usedMid = gcinfo()
|
||||
check("heap went past old threshold without auto-GC", usedMid > thr0)
|
||||
update(20); check("no collection in combat (fallback timer)", count() == 2)
|
||||
fire("ZONE_CHANGED_NEW_AREA"); check("no collection on safe event in combat", count() == 2)
|
||||
keep = nil
|
||||
fire("PLAYER_REGEN_ENABLED"); combat = false
|
||||
check("collected after combat", count() == 3)
|
||||
local u2, t2 = gcinfo(); check("threshold back to normal (2x live)", t2 <= u2 * 2 + 64)
|
||||
|
||||
print("5. short fight with nothing to collect restores threshold")
|
||||
now = now + 100
|
||||
local _, tb = gcinfo(); fire("PLAYER_REGEN_DISABLED"); fire("PLAYER_REGEN_ENABLED")
|
||||
local _, ta = gcinfo(); check("threshold restored", ta == tb); check("no collection", count() == 3)
|
||||
|
||||
print("6. pressure: collect pre-emptively near threshold, out of combat")
|
||||
now = now + 40
|
||||
local keep2 = {}
|
||||
local _, thr = gcinfo()
|
||||
while gcinfo() < thr * 0.86 do table.insert(keep2, { 1, 2 }) end
|
||||
keep2 = nil
|
||||
update(1.5); check("pressure collection", count() == 4)
|
||||
|
||||
print("7. fallback idle collection after maxInterval")
|
||||
makeGarbage(8); update(310); check("idle collection", count() == 5)
|
||||
|
||||
print("8. AFK")
|
||||
makeGarbage(8); now = now + 60; afk = true; fire("PLAYER_FLAGS_CHANGED"); afk = false
|
||||
check("afk collection", count() == 6)
|
||||
|
||||
print("8b. automatic GC is detected and resynced")
|
||||
cmd("debug 2")
|
||||
local nAuto = 0
|
||||
for _, m in ipairs(log) do if string.find(m, "automatic GC detected") then nAuto = nAuto + 1 end end
|
||||
-- the heap grows frame by frame until Lua collects on its own
|
||||
local SF = FRAMES[4]
|
||||
local _, t0 = gcinfo()
|
||||
local junk = {}
|
||||
local t = t0
|
||||
while t == t0 do -- (a local declared inside repeat isn't visible to until in Lua 5.0)
|
||||
for i = 1, 2000 do table.insert(junk, { i }) end
|
||||
if table.getn(junk) > 20000 then junk = {} end
|
||||
SF.scripts.OnUpdate()
|
||||
_, t = gcinfo()
|
||||
end
|
||||
junk = nil
|
||||
update(1.1)
|
||||
local nAuto2 = 0
|
||||
for _, m in ipairs(log) do if string.find(m, "automatic GC detected") then nAuto2 = nAuto2 + 1 end end
|
||||
check("automatic GC detected", nAuto2 > nAuto)
|
||||
local u, t = gcinfo()
|
||||
check("pressure not falsely triggered after auto GC", u < t * 0.85)
|
||||
|
||||
print("9. slash commands")
|
||||
cmd("interval 45"); check("interval set", GentleGCDB.minInterval == 45)
|
||||
cmd("now"); check("manual collect", count() >= 7)
|
||||
cmd(""); cmd("bogus"); cmd("reset"); check("reset", GentleGCDB.minInterval == 30)
|
||||
|
||||
print("10. summary report")
|
||||
cmd("debug 1"); update(301)
|
||||
local found = false
|
||||
for _, m in ipairs(log) do if string.find(m, "last %d+ min") then found = true end end
|
||||
check("summary printed", found)
|
||||
|
||||
print(FAILED and "SOME TESTS FAILED" or "ALL TESTS PASSED")
|
||||
Reference in New Issue
Block a user