Apply Patch
This commit is contained in:
+103
-10
@@ -27,6 +27,10 @@
|
||||
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
|
||||
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
|
||||
the start of each cycle (the full mark)
|
||||
when collections are slow (default: averaging 100 ms or more):
|
||||
* careful mode: no stand-still or idle collections, only loading screens,
|
||||
death, taxi, NPC windows, AFK, after combat, and pressure
|
||||
@@ -93,6 +97,11 @@ local thrByName, thrCount = {}, 0 -- session: threshold changes by othe
|
||||
local wrappedBy = false -- addon(s) whose Lua wrapper our collectgarbage passes through (found at load)
|
||||
local intercepted, interceptWarned = 0, false -- our collections that didn't run normally
|
||||
local pn, px, py, pxx, pxy, pxMin, pxMax = 0, 0, 0, 0, 0, nil, nil -- pause (ms) vs MB freed, own collections
|
||||
-- weirdperformance.dll: each cycle is one full mark, then the sweep runs in
|
||||
-- chunks; between chunks it sets the threshold to the heap + 128 KB
|
||||
local wp = false -- its collector is loaded (checked on ADDON_LOADED)
|
||||
local wpSweeping = false -- a cycle is in progress (between chunks)
|
||||
local wpChunks = 0 -- session: sweep chunks seen
|
||||
do
|
||||
local used, threshold = gcinfo()
|
||||
-- before any collection we can only assume the stock 2x ratio; the first
|
||||
@@ -218,6 +227,56 @@ local function AfterForeignCollection()
|
||||
if inCombat then EnterCombatThreshold() end
|
||||
end
|
||||
|
||||
-- weirdperformance's chunk boundary: threshold = heap + 128 KB (the heap may
|
||||
-- have grown a little since it was set)
|
||||
local function IsChunkThreshold(threshold, used)
|
||||
return threshold >= used - 64 and threshold <= used + 256
|
||||
end
|
||||
|
||||
-- With weirdperformance: recognize its cycle starts and sweep chunks.
|
||||
-- Returns true when the change was one of them.
|
||||
local function WpSync(used, threshold, oldThreshold)
|
||||
if IsChunkThreshold(threshold, used) then
|
||||
if not wpSweeping then
|
||||
-- a cycle started: its full mark is the pause that matters
|
||||
wpSweeping = true
|
||||
savedThreshold = nil -- a raised combat threshold was used up by it
|
||||
if used < lastUsed and lastUsed < oldThreshold * 0.9 then
|
||||
CountExternal(nil, nil, lastUsed - used)
|
||||
else
|
||||
for _, s in ipairs({ period, total }) do
|
||||
s.auto = s.auto + 1
|
||||
if inCombat then s.autoCombat = s.autoCombat + 1 end
|
||||
end
|
||||
if DebugLevel() >= 2 then
|
||||
Msg(format("automatic GC detected%s (weirdperformance cycle started)", inCombat and " in combat" or ""))
|
||||
end
|
||||
end
|
||||
else
|
||||
wpChunks = wpChunks + 1
|
||||
end
|
||||
if used < lastUsed then lastUsed = used else Account(used) end
|
||||
liveKB = used -- unswept garbage is still in the heap: don't plan on it
|
||||
return true
|
||||
elseif wpSweeping then
|
||||
-- the last chunk: the client's own threshold rule applies again
|
||||
wpSweeping = false
|
||||
wpChunks = wpChunks + 1
|
||||
if used < lastUsed then lastUsed = used else Account(used) end
|
||||
liveKB = used
|
||||
LearnRatio(used, threshold)
|
||||
AfterForeignCollection() -- in combat: delay the next cycle now that this one is done
|
||||
return true
|
||||
elseif used >= lastUsed and threshold >= used * 1.2 then
|
||||
-- a whole cycle fit inside one frame (start, chunks and end): the client's
|
||||
-- own threshold rule, not an addon setting the threshold
|
||||
Account(used)
|
||||
AfterForeignCollection()
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Detect collections we didn't run and thresholds we didn't set. A heap
|
||||
-- drop means a collection; a changed threshold without a drop means someone
|
||||
-- set it with collectgarbage(n). For a collection: the game's
|
||||
@@ -243,6 +302,8 @@ local function Sync()
|
||||
return
|
||||
end
|
||||
|
||||
if wp and WpSync(used, threshold, oldThreshold) then return end
|
||||
|
||||
if used < lastUsed then
|
||||
local est = used
|
||||
-- a threshold left within 5% of the heap wasn't set by the collector's
|
||||
@@ -319,6 +380,8 @@ end
|
||||
|
||||
local function Collect(reason)
|
||||
Sync()
|
||||
-- mid-cycle, collectgarbage() would only advance weirdperformance's sweep
|
||||
if wp and wpSweeping then return end
|
||||
local before, thresholdBefore = gcinfo()
|
||||
local t0 = StartTimer()
|
||||
rawCollect() -- full collection; the client sets a fresh threshold
|
||||
@@ -329,7 +392,8 @@ local function Collect(reason)
|
||||
-- a full collection always leaves the threshold well above the heap (the
|
||||
-- client's rule is at least 1.25x live); within 5% means collectgarbage()
|
||||
-- was turned into something else by an addon that wraps it
|
||||
local abnormal = not blocked and after > 1024 and threshold < after * 1.05
|
||||
-- (weirdperformance leaves heap + 128 KB on purpose)
|
||||
local abnormal = not wp and not blocked and after > 1024 and threshold < after * 1.05
|
||||
if blocked or abnormal then
|
||||
lastGC = GetTime()
|
||||
liveKB = after -- assume nothing to free: don't retry at once
|
||||
@@ -351,6 +415,7 @@ local function Collect(reason)
|
||||
|
||||
liveKB = after
|
||||
lastUsed = after
|
||||
if wp then wpSweeping = IsChunkThreshold(threshold, after) end
|
||||
LearnRatio(after, threshold)
|
||||
UpdateCareful(ms)
|
||||
knownThreshold = threshold
|
||||
@@ -360,10 +425,13 @@ local function Collect(reason)
|
||||
|
||||
local freed = before - after
|
||||
-- pause against garbage freed: on this client freeing may be most of the cost
|
||||
local x = MB(math.max(0, freed))
|
||||
pn, px, py, pxx, pxy = pn + 1, px + x, py + ms, pxx + x * x, pxy + x * ms
|
||||
if not pxMin or x < pxMin then pxMin = x end
|
||||
if not pxMax or x > pxMax then pxMax = x end
|
||||
-- (not with weirdperformance: most of the freeing happens later, in chunks)
|
||||
if not wp then
|
||||
local x = MB(math.max(0, freed))
|
||||
pn, px, py, pxx, pxy = pn + 1, px + x, py + ms, pxx + x * x, pxy + x * ms
|
||||
if not pxMin or x < pxMin then pxMin = x end
|
||||
if not pxMax or x > pxMax then pxMax = x end
|
||||
end
|
||||
for _, s in ipairs({ period, total }) do
|
||||
s.count = s.count + 1
|
||||
s.freed = s.freed + freed
|
||||
@@ -372,8 +440,13 @@ local function Collect(reason)
|
||||
end
|
||||
|
||||
if DebugLevel() >= 2 then
|
||||
Msg(format("%s: %.1fmb -> %.1fmb (freed %.1fmb, next auto-GC at %.1fmb) in %dms",
|
||||
reason, MB(before), MB(after), MB(freed), MB(threshold), ms))
|
||||
if wpSweeping then
|
||||
Msg(format("%s: started a weirdperformance cycle in %dms (%.1fmb -> %.1fmb so far; the rest is freed in chunks)",
|
||||
reason, ms, MB(before), MB(after)))
|
||||
else
|
||||
Msg(format("%s: %.1fmb -> %.1fmb (freed %.1fmb, next auto-GC at %.1fmb) in %dms",
|
||||
reason, MB(before), MB(after), MB(freed), MB(threshold), ms))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -399,6 +472,9 @@ end
|
||||
|
||||
EnterCombatThreshold = function()
|
||||
if db.combatHeadroomMB <= 0 then return end
|
||||
-- mid-cycle, let weirdperformance's chunks run; the headroom is added when
|
||||
-- the cycle ends, to keep the next cycle's full mark out of the fight
|
||||
if wp and wpSweeping then return end
|
||||
local used, threshold = gcinfo()
|
||||
local want = used + db.combatHeadroomMB * 1024
|
||||
if want > threshold then
|
||||
@@ -426,7 +502,7 @@ local function LeaveCombat()
|
||||
return
|
||||
end
|
||||
TryCollect("post-combat")
|
||||
if savedThreshold then
|
||||
if savedThreshold and not (wp and wpSweeping) then
|
||||
rawCollect(savedThreshold)
|
||||
savedThreshold = nil
|
||||
local _, t = gcinfo()
|
||||
@@ -464,6 +540,7 @@ local function WrappedCollect(a, b)
|
||||
local ms = (GetTime() - t0) * 1000
|
||||
local after, threshold = gcinfo()
|
||||
local full = a == nil or a == 0 or a == "collect"
|
||||
if wp and full then wpSweeping = IsChunkThreshold(threshold, after) end
|
||||
if after < before - 256 or (full and threshold ~= knownThreshold) then
|
||||
-- a full collection (or a threshold set below the heap, which collects)
|
||||
liveKB = after
|
||||
@@ -599,7 +676,7 @@ local function Check(dt)
|
||||
|
||||
local used, threshold = gcinfo()
|
||||
if used >= PressurePointKB(threshold) and GetTime() - lastGC >= 5
|
||||
and GarbageKB() >= math.max(1024, liveKB / 100) then
|
||||
and GarbageKB() >= math.max(1024, liveKB / 100) and not (wp and wpSweeping) then
|
||||
-- The automatic collector is about to fire anyway and would cost the
|
||||
-- same pause, so collect now, out of combat, without the garbage minimum.
|
||||
-- But not when there's next to nothing to free: that happens when an
|
||||
@@ -649,6 +726,17 @@ end
|
||||
-- C_Timer callbacks run once per frame from the world tick, outside the
|
||||
-- frame OnUpdate walk. Without ClassicAPI, fall back to a frame OnUpdate.
|
||||
StartScheduler = function()
|
||||
if GetWeirdUtilsVersion then
|
||||
local ok, v = pcall(GetWeirdUtilsVersion, "weirdperformance")
|
||||
wp = ok and v and true or false
|
||||
end
|
||||
if wp then
|
||||
local used, threshold = gcinfo()
|
||||
wpSweeping = IsChunkThreshold(threshold, used)
|
||||
if DebugLevel() >= 1 then
|
||||
Msg("weirdperformance detected: its collector frees memory in chunks; GentleGC schedules only the start of each cycle.")
|
||||
end
|
||||
end
|
||||
sampleFrame = sampleFrame or CreateFrame("Frame")
|
||||
sampleFrame:SetScript("OnUpdate", SampleOnUpdate)
|
||||
hasSpeed = type(GetUnitSpeed) == "function"
|
||||
@@ -837,7 +925,8 @@ local function HitchOnUpdate()
|
||||
cause = format("garbage collection run by %s%s", extName or "another addon",
|
||||
extMs and format(" took %dms", extMs) or "")
|
||||
elseif used < prevUsed - 256 or (threshold ~= prevThreshold and not weMovedThreshold) then
|
||||
cause = format("the game's automatic garbage collection: heap %.1fmb -> %.1fmb",
|
||||
cause = format("%s: heap %.1fmb -> %.1fmb",
|
||||
wp and "weirdperformance's collector (a cycle's full mark, or a sweep chunk)" or "the game's automatic garbage collection",
|
||||
MB(prevUsed), MB(used))
|
||||
else
|
||||
cause = "no garbage collection - something else caused it"
|
||||
@@ -991,6 +1080,10 @@ local OPTIONS = {
|
||||
-- Other addons' collections and thresholds, wrappers, and the pause model
|
||||
-- (split from PrintStatus: Lua 5.0 allows a function only 32 upvalues).
|
||||
local function PrintDiagnostics()
|
||||
if wp then
|
||||
Msg(format("weirdperformance: %d cycles started by the game (%d in combat), %d by GentleGC, %d sweep chunks; %s",
|
||||
total.auto, total.autoCombat, total.count, wpChunks, wpSweeping and "a cycle is running now" or "no cycle running"))
|
||||
end
|
||||
if total.ext > 0 then
|
||||
local parts = {}
|
||||
for name, n in pairs(extByName) do table.insert(parts, name .. " " .. n) end
|
||||
|
||||
+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.6
|
||||
## Version: 2.7
|
||||
## SavedVariables: GentleGCDB
|
||||
|
||||
GentleGC.lua
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
-- weirdperformance.dll mode: its collector runs one full mark per cycle and
|
||||
-- then sweeps in chunks, setting the threshold to heap + 128 KB in between.
|
||||
-- Emulated here as the game would show it through gcinfo()/collectgarbage().
|
||||
-- Run: lua5.0 tests/test_weirdperf.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 = {}
|
||||
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
|
||||
function GetWeirdUtilsVersion(m)
|
||||
if m == "weirdperformance" then return "1.0" end
|
||||
return { weirdperformance = "1.0" }
|
||||
end
|
||||
|
||||
-- the emulated collector (the real Lua threshold is kept out of the way)
|
||||
local realGC, realInfo = collectgarbage, gcinfo
|
||||
local W = { thr = 0, sweeping = false, left = 0, cycles = 0, chunks = 0 }
|
||||
local CHUNKS = 3
|
||||
local function heap() return (realInfo()) end
|
||||
local function wpRun() -- the hooked luaC_collectgarbage
|
||||
if not W.sweeping then
|
||||
W.cycles = W.cycles + 1
|
||||
realGC(); realGC(4000000) -- the mark (the emulation frees everything here)
|
||||
W.sweeping, W.left = true, CHUNKS
|
||||
W.thr = heap() + 128
|
||||
else
|
||||
W.chunks = W.chunks + 1
|
||||
W.left = W.left - 1
|
||||
if W.left <= 0 then
|
||||
W.sweeping = false
|
||||
W.thr = math.floor(heap() * 1.25)
|
||||
else
|
||||
W.thr = heap() + 128
|
||||
end
|
||||
end
|
||||
end
|
||||
gcinfo = function() return heap(), W.thr end
|
||||
collectgarbage = function(n) -- lua_setgcthreshold + luaC_checkGC
|
||||
if type(n) == "table" then return realGC(n) end -- (invalid argument: errors, like the game)
|
||||
W.thr = n or 0
|
||||
if heap() >= W.thr then wpRun() end
|
||||
end
|
||||
local function engine() if heap() >= W.thr then wpRun() end end
|
||||
|
||||
LIVE = {}
|
||||
for i = 1, 300000 do LIVE[i] = { i } end
|
||||
realGC(); realGC(4000000)
|
||||
W.thr = math.floor(heap() * 1.25)
|
||||
|
||||
dofile(arg[1])
|
||||
local F = FRAMES[1]
|
||||
local HF = FRAMES[2]
|
||||
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 junk, ji = {}, 0
|
||||
-- one frame: allocate `kb` of garbage, let the collector react, then GentleGC's OnUpdates
|
||||
-- (allocation is counted in objects, ~9 per KB, so a collection in the middle
|
||||
-- of the frame doesn't change how much the frame allocates)
|
||||
local function frame(dt, kb)
|
||||
local n = math.floor((kb or 0) * 9)
|
||||
for k = 1, n do
|
||||
ji = ji + 1; junk[ji] = { ji }
|
||||
if ji > 20000 then junk, ji = {}, 0 end
|
||||
if math.mod(k, 50) == 0 then engine() end
|
||||
end
|
||||
engine()
|
||||
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 play(seconds, kbPerSec)
|
||||
for i = 1, seconds * 10 do frame(0.1, (kbPerSec or 0) / 10) end
|
||||
end
|
||||
|
||||
print("1. detected")
|
||||
cmd("status")
|
||||
check("status names weirdperformance", count("weirdperformance: ") >= 1)
|
||||
|
||||
print("2. the game's cycles and chunks while playing")
|
||||
cmd("pressure 100") -- GentleGC holds off: the game starts every cycle here
|
||||
play(40, 400) -- 400 KB/s of garbage
|
||||
local gameCycles = W.cycles
|
||||
check("the emulated collector ran cycles and chunks", W.cycles >= 1 and W.chunks >= 2)
|
||||
check("chunks aren't reported as threshold changes", count("threshold set to") == 0)
|
||||
check("chunks aren't reported as collections", count("automatic GC detected") <= W.cycles)
|
||||
check("each game-started cycle reported once", count("weirdperformance cycle started") == W.cycles)
|
||||
check("no 'didn't run a normal collection' warnings", count("didn't run a normal collection") == 0)
|
||||
check("no 'did not run' warnings", count("did not run") == 0)
|
||||
|
||||
print("3. GentleGC starts a cycle only when none is running")
|
||||
while W.sweeping do frame(0.1, 200) end
|
||||
play(2)
|
||||
local c0 = W.cycles
|
||||
cmd("now")
|
||||
check("GentleGC's collection started a cycle", W.cycles == c0 + 1 and W.sweeping)
|
||||
check("logged as a cycle start", string.find(lastMatch("manual:"), "started a weirdperformance cycle") ~= nil)
|
||||
local c1, k1 = W.cycles, W.chunks
|
||||
cmd("now")
|
||||
check("mid-cycle, GentleGC does nothing", W.cycles == c1 and W.chunks == k1)
|
||||
|
||||
print("4. no pressure attempts in the middle of a cycle")
|
||||
cmd("pressure 70")
|
||||
local before = count("pressure:")
|
||||
for i = 1, 30 do frame(0.1, 3) end -- 90 KB in all: stays inside one chunk
|
||||
check("still mid-cycle", W.sweeping)
|
||||
check("no pressure collection mid-cycle", count("pressure:") == before)
|
||||
|
||||
print("5. the end of a cycle teaches the ratio and the live estimate")
|
||||
while W.sweeping do frame(0.1, 200) end
|
||||
play(1)
|
||||
cmd("status")
|
||||
check("ratio measured as ~1.25x (read a frame late, so a little lower)", string.find(lastMatch("collector:"), "lowest 1%.2[0-5]x") ~= nil)
|
||||
local _, _, live = string.find(lastMatch("heap .* live ~"), "live ~([%d%.]+)mb")
|
||||
check("live estimate matches the heap after the cycle", live and math.abs(tonumber(live) - heap() / 1024) < 1)
|
||||
|
||||
print("6. combat: chunks keep running, the next cycle waits for the fight to end")
|
||||
-- start a cycle, then pull while it runs
|
||||
cmd("now")
|
||||
check("a cycle is running at the pull", W.sweeping)
|
||||
combat = true
|
||||
event = "PLAYER_REGEN_DISABLED"; this = F; F.scripts.OnEvent()
|
||||
check("threshold not raised mid-cycle (chunks continue)", W.thr <= heap() + 256)
|
||||
while W.sweeping do frame(0.1, 200) end
|
||||
play(1)
|
||||
local used = heap()
|
||||
check("after the cycle ends in combat, the headroom is added", W.thr >= used + 32 * 1024 - 256)
|
||||
local cyclesInCombat = W.cycles
|
||||
play(20, 1000) -- 20 MB of garbage in the fight
|
||||
check("no new cycle during the fight", W.cycles == cyclesInCombat)
|
||||
combat = false
|
||||
event = "PLAYER_REGEN_ENABLED"; this = F; F.scripts.OnEvent()
|
||||
play(2)
|
||||
cmd("status")
|
||||
check("status counts cycles and chunks", string.find(lastMatch("weirdperformance: "), "sweep chunks") ~= nil)
|
||||
|
||||
print("6b. a cycle starting in the fight, and the fight ending mid-cycle")
|
||||
while W.sweeping do frame(0.1, 200) end
|
||||
play(1)
|
||||
combat = true
|
||||
event = "PLAYER_REGEN_DISABLED"; this = F; F.scripts.OnEvent()
|
||||
local c6 = W.cycles
|
||||
while W.cycles == c6 do frame(0.1, 100) end -- the fight fills the raised headroom
|
||||
check("a cycle started in the fight", W.sweeping)
|
||||
combat = false
|
||||
event = "PLAYER_REGEN_ENABLED"; this = F; F.scripts.OnEvent()
|
||||
check("leaving combat mid-cycle keeps the chunk threshold", W.thr <= heap() + 256)
|
||||
local k6 = W.chunks
|
||||
play(2, 400)
|
||||
check("chunks keep running after the fight", W.chunks > k6)
|
||||
|
||||
print("7. freeze detector names weirdperformance's collector")
|
||||
cmd("pressure 100")
|
||||
cmd("hitch")
|
||||
while W.sweeping do frame(0.1, 200) end
|
||||
play(1)
|
||||
while heap() < W.thr - 96 and not W.sweeping do frame(0.1, 16) end -- creep up to the threshold...
|
||||
now = now + 0.4 -- ...and cross it inside a long frame
|
||||
frame(0.05, 200)
|
||||
local f = lastMatch("freeze")
|
||||
check("freeze names weirdperformance", string.find(f, "weirdperformance") ~= nil or string.find(f, "GentleGC collection") ~= nil)
|
||||
cmd("hitch 0")
|
||||
|
||||
print(FAILED and "SOME TESTS FAILED" or "ALL TESTS PASSED")
|
||||
Reference in New Issue
Block a user