Release 2.8.0: live per-addon memory from ClassicAPI
/ggc addons reads GetAddOnMemoryUsage when ClassicAPI provides it, so the !!!!GentleGC_Memory companion is only needed without it. /ggc also shows the client's scriptMemory limit and warns when scriptProfile is on. Adds tests/test_native_memory.lua. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019W3rwtugSE5NbbiWQCbbKX
This commit is contained in:
+12
-1
@@ -3,7 +3,18 @@
|
||||
All notable changes to this OctoWoW edition of Gentle GC. Versions follow
|
||||
`GentleGC.toc`. Dates are the commit dates (UTC).
|
||||
|
||||
## Unreleased
|
||||
## 2.8.0 - 2026-11-02
|
||||
|
||||
### Added
|
||||
- With ClassicAPI, `/ggc addons` reads each addon's memory live from the client
|
||||
(`UpdateAddOnMemoryUsage` / `GetAddOnMemoryUsage`): no companion, no extra
|
||||
collections at the loading screen, current numbers at any time, and a note on
|
||||
addons that grew by 100 KB or more since the last check. The
|
||||
`!!!!GentleGC_Memory` companion is now only needed without ClassicAPI, and is
|
||||
still used when the client returns nothing.
|
||||
- `/ggc` shows the client's `scriptMemory` limit and warns when `scriptProfile`
|
||||
(CPU profiling) is on, since profiling slows every Lua call.
|
||||
- `tests/test_native_memory.lua`.
|
||||
|
||||
### Repository
|
||||
- README shows client-mod badges (ClassicAPI and weirdperformance, both optional).
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
optional, used only when the ClassicAPI client mod is installed:
|
||||
* collect when you've been standing still for a few seconds (GetUnitSpeed)
|
||||
* run the once-a-second check from C_Timer instead of a frame OnUpdate
|
||||
* /ggc addons reads each addon's memory live (GetAddOnMemoryUsage), so the
|
||||
!!!!GentleGC_Memory companion is only needed without ClassicAPI
|
||||
with weirdperformance.dll (WeirdUtils), whose collector does one full mark
|
||||
per cycle and then frees memory in small chunks:
|
||||
* recognize the chunks, stay out of a running cycle, and schedule only
|
||||
@@ -1031,7 +1033,57 @@ ShowAddonSummary = function()
|
||||
Size(m.total or 0), table.getn(m.rows), Biggest(m, 3)))
|
||||
end
|
||||
|
||||
-- With ClassicAPI the client reports each addon's Lua memory itself
|
||||
-- (UpdateAddOnMemoryUsage / GetAddOnMemoryUsage), at any time and without the
|
||||
-- companion's collections at the loading screen. It recounts everything on each
|
||||
-- update, so it only runs when asked for, never per frame.
|
||||
local nativeLast = nil -- session: addon memory (KB) at the last native check
|
||||
|
||||
local function HasNativeMem()
|
||||
return type(UpdateAddOnMemoryUsage) == "function" and type(GetAddOnMemoryUsage) == "function"
|
||||
and type(GetNumAddOns) == "function" and type(GetAddOnInfo) == "function"
|
||||
end
|
||||
|
||||
-- Prints the live per-addon list; false when it can't (then the companion's
|
||||
-- login measurement is used instead).
|
||||
local function PrintNativeAddons(all)
|
||||
if not HasNativeMem() or not pcall(UpdateAddOnMemoryUsage) then return false end
|
||||
local rows, sum, now = {}, 0, {}
|
||||
for i = 1, GetNumAddOns() do
|
||||
local name = GetAddOnInfo(i)
|
||||
if name then
|
||||
local ok, kb = pcall(GetAddOnMemoryUsage, i)
|
||||
if ok and type(kb) == "number" and kb > 0 then
|
||||
table.insert(rows, { name, kb })
|
||||
sum = sum + kb
|
||||
now[name] = kb
|
||||
end
|
||||
end
|
||||
end
|
||||
if table.getn(rows) == 0 then return false end
|
||||
table.sort(rows, function(a, b) return a[2] > b[2] end)
|
||||
local used = gcinfo()
|
||||
Msg(format("addon memory now (from ClassicAPI): %s in %d addons; Lua heap %s.",
|
||||
Size(sum), table.getn(rows), Size(used)))
|
||||
local shown = table.getn(rows)
|
||||
if not all and shown > ADDONS_TOP then shown = ADDONS_TOP end
|
||||
for i = 1, shown do
|
||||
local name, kb = rows[i][1], rows[i][2]
|
||||
local extra = ""
|
||||
if nativeLast and nativeLast[name] and kb - nativeLast[name] >= 100 then
|
||||
extra = format(" (+%s since the last check)", Size(kb - nativeLast[name]))
|
||||
end
|
||||
DEFAULT_CHAT_FRAME:AddMessage(format(" %9s %s%s", Size(kb), name, extra))
|
||||
end
|
||||
if shown < table.getn(rows) then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(format(" ...and %d more (/ggc addons all)", table.getn(rows) - shown))
|
||||
end
|
||||
nativeLast = now
|
||||
return true
|
||||
end
|
||||
|
||||
local function PrintAddons(all)
|
||||
if PrintNativeAddons(all) then return end
|
||||
if not GentleGCMemory then
|
||||
Msg("per-addon memory needs the " .. COMPANION .. " folder from the Gentle GC zip in Interface/AddOns. "
|
||||
.. "It has to load before your other addons, which is what the !!!! is for.")
|
||||
@@ -1069,6 +1121,10 @@ local function PrintAddons(all)
|
||||
end
|
||||
|
||||
local function MeasureNext()
|
||||
if HasNativeMem() then
|
||||
Msg("no measuring needed: ClassicAPI reports each addon's memory live. Just /ggc addons.")
|
||||
return
|
||||
end
|
||||
if not GentleGCMemory or type(GentleGCMemoryDB) ~= "table" then
|
||||
PrintAddons(false) -- explains the missing companion
|
||||
return
|
||||
@@ -1175,12 +1231,34 @@ local function PrintStatus()
|
||||
", C_Timer " .. (hasTicker and "on" or "not available (using OnUpdate)"))
|
||||
end
|
||||
|
||||
-- The client's interface memory limit and CPU profiling, which change how the
|
||||
-- client frees memory and how fast Lua runs.
|
||||
local function PrintClientInfo()
|
||||
if type(GetCVar) ~= "function" then return end
|
||||
local ok, limit = pcall(GetCVar, "scriptMemory")
|
||||
local ok2, prof = pcall(GetCVar, "scriptProfile")
|
||||
limit = ok and tonumber(limit) or nil
|
||||
if limit then
|
||||
if limit == 0 then
|
||||
Msg("client: interface memory limit off")
|
||||
else
|
||||
Msg(format("client: interface memory limit %s - the client frees what it can when the interface passes it, "
|
||||
.. "which shows up as collections nobody ran", Size(limit)))
|
||||
end
|
||||
end
|
||||
if ok2 and prof == "1" then
|
||||
Msg("client: CPU profiling is ON (scriptProfile 1): it slows every Lua function call. "
|
||||
.. "Turn it off (/console scriptProfile 0, then /reload) unless you are measuring.")
|
||||
end
|
||||
end
|
||||
|
||||
SLASH_GENTLEGC1 = "/ggc"
|
||||
SLASH_GENTLEGC2 = "/gentlegc"
|
||||
SlashCmdList["GENTLEGC"] = function(msg)
|
||||
local _, _, cmd, val = string.find(string.lower(msg or ""), "^%s*(%S*)%s*(.-)%s*$")
|
||||
if cmd == "" or cmd == "status" then
|
||||
PrintStatus()
|
||||
PrintClientInfo()
|
||||
elseif cmd == "now" then
|
||||
if inCombat then Msg("in combat - not collecting") else Collect("manual") end
|
||||
elseif cmd == "debug" then
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
## Title: Gentle GC
|
||||
## Notes: Schedules Lua garbage collection for moments you won't notice
|
||||
## Author: Weird Vibes (MarcelineVQ); forks by TheRealFayz, jrc13245
|
||||
## Version: 2.7.1
|
||||
## Version: 2.8.0
|
||||
## SavedVariables: GentleGCDB
|
||||
|
||||
GentleGC.lua
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://github.com/brues-code/ClassicAPI) [](https://codeberg.org/MarcelineVQ/WeirdUtils)
|
||||
|
||||
Gentle Garbage Collection, unified edition (v2.7.1), maintained for OctoWoW.
|
||||
Gentle Garbage Collection, unified edition (v2.8.0), maintained for OctoWoW.
|
||||
|
||||
For WoW 1.18.1 clients (Lua 5.0).
|
||||
|
||||
@@ -12,7 +12,7 @@ See [CHANGELOG.md](CHANGELOG.md) for the version history.
|
||||
|
||||
Unzip into `Interface/AddOns`. The zip has two folders:
|
||||
`GentleGC` (the addon) and `!!!!GentleGC_Memory` (optional, per-addon memory;
|
||||
see below). Keep both folder names exactly as they are. If you download the
|
||||
see below; not needed with ClassicAPI). Keep both folder names exactly as they are. If you download the
|
||||
repository instead, move `!!!!GentleGC_Memory` out of the `GentleGC` folder
|
||||
so it sits next to it in `Interface/AddOns`.
|
||||
|
||||
@@ -68,8 +68,10 @@ under 75 ms. `/ggc slow <ms>` sets the limit; `/ggc slow 0` turns it off.
|
||||
and whether careful mode is on; and how fast garbage is being created.
|
||||
- `/ggc debug 1`: chat summary every 5 minutes. `/ggc debug 2` also prints each collection. `/ggc debug 0` turns it off.
|
||||
- `/ggc now`: collect immediately.
|
||||
- `/ggc addons`: memory per addon (needs `!!!!GentleGC_Memory`). `/ggc addons all`
|
||||
lists every addon; `/ggc addons measure` measures again on the next `/reload`.
|
||||
- `/ggc addons`: memory per addon. With ClassicAPI it is read live from the
|
||||
client; without it, it needs `!!!!GentleGC_Memory`. `/ggc addons all` lists
|
||||
every addon; `/ggc addons measure` measures again on the next `/reload`
|
||||
(companion only).
|
||||
- `/ggc interval|maxinterval|report <sec>`, `/ggc garbage|headroom <mb>`,
|
||||
`/ggc garbagepct <%>`, `/ggc pressure <%>` (share of the room, default 70),
|
||||
`/ggc slow <ms>` (careful mode, default 100), `/ggc allocwarn <MB/s>`
|
||||
@@ -100,7 +102,14 @@ Off by default.
|
||||
|
||||
## Per-addon memory
|
||||
|
||||
`/ggc addons`: the 1.12 client has no per-addon
|
||||
**With ClassicAPI** the client reports each addon's Lua memory itself
|
||||
(`GetAddOnMemoryUsage`), so `/ggc addons` shows a live list at any time, with
|
||||
no companion and no extra collections at the loading screen. Each call recounts
|
||||
all memory, so it only runs when you type the command. Addons that have grown by
|
||||
100 KB or more since your last `/ggc addons` are marked. Blizzard addons and
|
||||
addons that haven't loaded report 0 and are left out.
|
||||
|
||||
**Without ClassicAPI** (or if it returns nothing) the old method is used. The 1.12 client has no per-addon
|
||||
memory numbers, only the size of the whole Lua heap. The optional
|
||||
`!!!!GentleGC_Memory` folder loads before every other addon (that's what the
|
||||
`!!!!` is for) and, each time another addon finishes loading, collects garbage
|
||||
@@ -167,7 +176,11 @@ pfQuest, for example, can hold 100 MB while idle.
|
||||
## Client mods
|
||||
|
||||
- ClassicAPI: supported, as above. Its once-per-frame timer also runs
|
||||
GentleGC's background check, in place of a frame OnUpdate.
|
||||
GentleGC's background check, in place of a frame OnUpdate, and its
|
||||
`GetAddOnMemoryUsage` gives `/ggc addons` live per-addon memory. `/ggc` also
|
||||
reports the client's `scriptMemory` limit (the client frees what it can when
|
||||
the interface passes it) and warns if `scriptProfile` (CPU profiling, which
|
||||
slows every Lua call) is on.
|
||||
- weirdperformance (WeirdUtils): supported. It replaces the game's collector:
|
||||
each cycle does one full mark, then frees memory in small chunks, setting
|
||||
the threshold to 128 KB above the memory in use between chunks. Gentle GC
|
||||
|
||||
@@ -19,4 +19,5 @@ Single test: `lua5.0 tests/test_stock.lua GentleGC.lua`
|
||||
| test_classicapi.lua | ClassicAPI client mod |
|
||||
| test_weirdperf.lua | weirdperformance.dll collector |
|
||||
| test_unknown_context.lua | event context shown for unattributed collections and threshold changes |
|
||||
| test_native_memory.lua | ClassicAPI per-addon memory, companion fallback, client info lines |
|
||||
| test_memory.lua | per-addon memory (also needs `!!!!GentleGC_Memory/GentleGC_Memory.lua` as the second argument) |
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
-- Native per-addon memory (ClassicAPI), the companion fallback, and the client info lines.
|
||||
-- Run: lua5.0 tests/test_native_memory.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 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 `x'\n" end
|
||||
return s
|
||||
end
|
||||
|
||||
-- the client's collector: full collections leave the threshold at 1.25x live,
|
||||
-- and take 10 ms + 8 ms per MB freed (simulated on the clock)
|
||||
local origCG = collectgarbage
|
||||
local function setThreshold() local u = gcinfo(); origCG(math.floor(u * 1.25)) end
|
||||
local timed = false
|
||||
collectgarbage = function(n)
|
||||
if n == nil or n == 0 then
|
||||
local before = gcinfo()
|
||||
origCG(); setThreshold()
|
||||
if timed then now = now + 0.010 + 0.008 * (before - gcinfo()) / 1024 end
|
||||
else
|
||||
origCG(n)
|
||||
end
|
||||
end
|
||||
local unwrapped = collectgarbage -- what an addon holds if it loaded before us
|
||||
|
||||
LIVE = {}
|
||||
for i = 1, 300000 do LIVE[i] = { i } end
|
||||
origCG(); setThreshold()
|
||||
|
||||
dofile(arg[1])
|
||||
local F = FRAMES[1]
|
||||
event, arg1 = "ADDON_LOADED", "GentleGC"; this = F; F.scripts.OnEvent()
|
||||
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
|
||||
local function clear() log = {} end
|
||||
local function has(pat) return count(pat) > 0 end
|
||||
|
||||
print("1. without ClassicAPI's functions: the companion is asked for")
|
||||
clear()
|
||||
cmd("addons")
|
||||
check("explains the companion", has("per%-addon memory needs"))
|
||||
cmd("addons measure")
|
||||
check("measure falls through to the companion text", has("per%-addon memory needs"))
|
||||
|
||||
-- a stand-in for ClassicAPI's profiling functions
|
||||
local names = {}
|
||||
local kbs = {}
|
||||
local ADDONS = 20
|
||||
for i = 1, ADDONS do names[i] = "Addon" .. i; kbs[i] = i * 100 end -- 100 KB .. 2000 KB
|
||||
names[21] = "Blizzard_Thing"; kbs[21] = 0 -- Blizzard / not loaded: 0
|
||||
local updates = 0
|
||||
function GetNumAddOns() return table.getn(names) end
|
||||
function GetAddOnInfo(i) return names[i] end
|
||||
function UpdateAddOnMemoryUsage() updates = updates + 1 end
|
||||
function GetAddOnMemoryUsage(i)
|
||||
if i < 1 or i > table.getn(names) then error("bad index") end
|
||||
return kbs[i]
|
||||
end
|
||||
|
||||
print("2. with them: a live list, biggest first, top 15")
|
||||
clear()
|
||||
cmd("addons")
|
||||
check("one update per listing", updates == 1)
|
||||
check("header says where it comes from", has("addon memory now %(from ClassicAPI%)"))
|
||||
check("total counts the 20 loaded addons", has("in 20 addons"))
|
||||
check("zero rows (not loaded) are left out", not has("Blizzard_Thing"))
|
||||
check("biggest is first", string.find(log[2], "Addon20") ~= nil)
|
||||
check("15 rows shown", count("Addon%d+") == 15)
|
||||
check("says how many more", has("and 5 more"))
|
||||
check("no growth on the first check", not has("since the last check"))
|
||||
|
||||
print("3. 'all' lists every addon")
|
||||
clear()
|
||||
cmd("addons all")
|
||||
check("20 rows", count("Addon%d+") == 20)
|
||||
|
||||
print("4. growth since the last check is shown")
|
||||
kbs[3] = kbs[3] + 3000
|
||||
kbs[4] = kbs[4] + 50 -- under 100 KB: not worth a note
|
||||
clear()
|
||||
cmd("addons all")
|
||||
check("Addon3 grew", string.find(lastMatch("Addon3 "), "%+2%.9 MB since the last check") ~= nil or string.find(lastMatch("Addon3"), "since the last check") ~= nil)
|
||||
check("Addon4's small change is not flagged", string.find(lastMatch("Addon4 "), "since the last check") == nil)
|
||||
|
||||
print("5. measure is not needed")
|
||||
clear()
|
||||
cmd("addons measure")
|
||||
check("says so", has("no measuring needed"))
|
||||
|
||||
print("6. a client where the update call errors falls back")
|
||||
local good = UpdateAddOnMemoryUsage
|
||||
function UpdateAddOnMemoryUsage() error("nope") end
|
||||
clear()
|
||||
cmd("addons")
|
||||
check("falls back to the companion text", has("per%-addon memory needs"))
|
||||
UpdateAddOnMemoryUsage = good
|
||||
|
||||
print("7. everything reporting 0 falls back")
|
||||
local saved = {}
|
||||
for i = 1, table.getn(kbs) do saved[i] = kbs[i]; kbs[i] = 0 end
|
||||
clear()
|
||||
cmd("addons")
|
||||
check("falls back", has("per%-addon memory needs"))
|
||||
for i = 1, table.getn(kbs) do kbs[i] = saved[i] end
|
||||
|
||||
print("8. client info lines in /ggc")
|
||||
clear()
|
||||
cmd("status")
|
||||
check("no cvar function: no client lines", not has("client:"))
|
||||
local cvars = { scriptMemory = "0", scriptProfile = "0" }
|
||||
function GetCVar(k) return cvars[k] end
|
||||
-- the slash command prints them after the status block
|
||||
clear()
|
||||
cmd("status")
|
||||
check("limit off", has("client: interface memory limit off"))
|
||||
check("profiling off prints no warning", not has("CPU profiling is ON"))
|
||||
cvars.scriptMemory = "65536"; cvars.scriptProfile = "1"
|
||||
clear()
|
||||
cmd("status")
|
||||
check("limit shown", has("interface memory limit 64.0 MB"))
|
||||
check("profiling warning", has("CPU profiling is ON"))
|
||||
|
||||
print(FAILED and "SOME TESTS FAILED" or "ALL TESTS PASSED")
|
||||
Reference in New Issue
Block a user