mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
fix(addons): memory usage walks the live objects, as luaC_sizeall does
UpdateAddOnMemoryUsage summed Gc::Tag::SizeOf over the keys of the tag map. That is only safe while every free reaches luaC_freeobj (0x006F7260), whose co-hook erases the key. wow_turbo replaces luaC_collectgarbage (0x006F7340) with a parallel sweep that frees strings, tables, closures, userdata and upvalues itself and sends only threads and protos through freeobj. Dead keys stayed in the map, and the recount read freed blocks: crash 132. The recount now walks the lists the engine sweeps: rootgc (G+0x10), rootudata (G+0x14) and every string-table bucket (G+0x04, size G+0x0C), and looks up the tag of each live object. 3.3.5's luaC_sizeall walks the same lists. A create with no context word now erases its address's entry, so a reused address never carries a dead object's tag, whoever freed it. Dead entries a foreign sweep leaves are never read, and the pool's address reuse bounds them. _classicapi_LuaHeapAudit uses the same walk. Fixes #120
This commit is contained in:
@@ -89,8 +89,8 @@ void PatchLoadWindow() {
|
||||
reinterpret_cast<const void *>(&LoadWindowCloseThunk));
|
||||
}
|
||||
|
||||
int __fastcall Script_UpdateAddOnMemoryUsage(void * /*L*/) {
|
||||
Gc::Tag::UpdateUsage();
|
||||
int __fastcall Script_UpdateAddOnMemoryUsage(void *L) {
|
||||
Gc::Tag::UpdateUsage(L);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -107,30 +107,15 @@ int __fastcall Script_GetAddOnMemoryUsage(void *L) {
|
||||
}
|
||||
|
||||
// `_classicapi_LuaHeapAudit()` → objectBytes, nblocks, taggedObjects.
|
||||
// Walks every collectable object the engine can reach — the main list,
|
||||
// the userdata list and the string table, the three lists `luaC_sweep`
|
||||
// sweeps — summing `Gc::Tag::SizeOf`. `objectBytes` should sit just under
|
||||
// `nblocks` (the engine's own byte count): the gap is what is not a GC
|
||||
// object (string-table buckets, the main thread and its stack, buffers).
|
||||
// A gap that grows with the heap means a size formula is wrong.
|
||||
uint64_t SumList(const uint8_t *object) {
|
||||
uint64_t total = 0;
|
||||
while (object != nullptr) {
|
||||
total += Gc::Tag::SizeOf(object);
|
||||
object = Game::Read<const uint8_t *>(object, Offsets::OFF_LUA_GCHEADER_NEXT);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// Sums `Gc::Tag::SizeOf` over every collectable object the engine can
|
||||
// reach. `objectBytes` should sit just under `nblocks` (the engine's own
|
||||
// byte count): the gap is what is not a GC object (string-table buckets,
|
||||
// the main thread and its stack, buffers). A gap that grows with the heap
|
||||
// means a size formula is wrong.
|
||||
int __fastcall Script_LuaHeapAudit(void *L) {
|
||||
uint64_t bytes = 0;
|
||||
Gc::Tag::ForEachObject(L, [&bytes](const void *object) { bytes += Gc::Tag::SizeOf(object); });
|
||||
const uint8_t *g = Game::Read<const uint8_t *>(L, Offsets::OFF_LUA_STATE_G);
|
||||
uint64_t bytes = SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTGC)) +
|
||||
SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTUDATA));
|
||||
const auto buckets =
|
||||
Game::Read<const uint8_t *const *>(g, Offsets::OFF_LUA_G_STRT_HASH);
|
||||
const int size = Game::Read<int>(g, Offsets::OFF_LUA_G_STRT_SIZE);
|
||||
for (int i = 0; i < size; ++i)
|
||||
bytes += SumList(buckets[i]);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(bytes));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(Game::Read<uint32_t>(g, Offsets::OFF_LUA_G_NBLOCKS)));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(Gc::Tag::TaggedCount()));
|
||||
|
||||
+11
-6
@@ -25,8 +25,9 @@ namespace Gc::Tag {
|
||||
|
||||
namespace {
|
||||
|
||||
// Live tagged objects → tag. Untagged objects are never stored, and every
|
||||
// free erases, so a reused address can't inherit a dead object's tag.
|
||||
// Tagged objects → tag. Untagged objects are never stored. Every create
|
||||
// writes or erases its address's entry, so a reused address can't inherit a
|
||||
// dead object's tag even when its free bypassed `FUN_LUA_C_FREEOBJ`.
|
||||
ankerl::unordered_dense::map<const void *, const char *> g_tags;
|
||||
|
||||
// `GCUsage` list: tag → bytes at the last UpdateUsage. Like 3.3.5's list, a
|
||||
@@ -38,8 +39,10 @@ std::vector<const char *> g_pins;
|
||||
|
||||
void OnCreate(const void *object) {
|
||||
const char *ctx = Game::Read<const char *>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT);
|
||||
if (ctx == nullptr)
|
||||
if (ctx == nullptr) {
|
||||
g_tags.erase(object);
|
||||
return;
|
||||
}
|
||||
const char *pin = g_pins.empty() ? nullptr : g_pins.back();
|
||||
g_tags[object] = pin != nullptr ? pin : ctx;
|
||||
}
|
||||
@@ -103,11 +106,13 @@ void PopPin() {
|
||||
g_pins.pop_back();
|
||||
}
|
||||
|
||||
void UpdateUsage() {
|
||||
void UpdateUsage(const void *L) {
|
||||
for (auto &entry : g_usage)
|
||||
entry.second = 0;
|
||||
for (const auto &[object, tag] : g_tags)
|
||||
g_usage[tag] += SizeOf(object);
|
||||
ForEachObject(L, [](const void *object) {
|
||||
if (const char *tag = Of(object))
|
||||
g_usage[tag] += SizeOf(object);
|
||||
});
|
||||
}
|
||||
|
||||
uint64_t Usage(const char *tag) {
|
||||
|
||||
+28
-2
@@ -13,6 +13,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
@@ -26,9 +29,14 @@
|
||||
// addon loader sets to the addon's name pointer and script slots, constants
|
||||
// and value copies carry — but its GC header has no room for a tag (a
|
||||
// table's spare header bytes hold flags/lsizenode, a closure's isC/nupvalues),
|
||||
// so the tag lives here, keyed by object, added on create and dropped on free
|
||||
// so the tag lives here, keyed by object, set on create and dropped on free
|
||||
// through `Gc::Hooks`. Tags are compared by pointer and never dereferenced.
|
||||
//
|
||||
// The map is only ever asked about live objects: `UpdateUsage` walks the
|
||||
// engine's lists as `luaC_sizeall` does, never the map's own keys. A sweep
|
||||
// that frees without `FUN_LUA_C_FREEOBJ` (wow_turbo's frees inline) leaves
|
||||
// dead entries behind, and they are never read.
|
||||
//
|
||||
// `lua_gctag` is this module's pin stack: `PinScope` sets it for exactly the
|
||||
// windows 3.3.5 sets it (an addon's load, a template's application).
|
||||
|
||||
@@ -55,8 +63,26 @@ struct PinScope {
|
||||
PinScope &operator=(const PinScope &) = delete;
|
||||
};
|
||||
|
||||
// Calls `fn(object)` on every collectable object the engine can reach: the
|
||||
// main list, the userdata list and the string table, the three lists
|
||||
// `luaC_sweep` sweeps.
|
||||
template <typename Fn> void ForEachObject(const void *L, Fn fn) {
|
||||
const auto walk = [&fn](const uint8_t *object) {
|
||||
for (; object != nullptr;
|
||||
object = Game::Read<const uint8_t *>(object, Offsets::OFF_LUA_GCHEADER_NEXT))
|
||||
fn(object);
|
||||
};
|
||||
const uint8_t *g = Game::Read<const uint8_t *>(L, Offsets::OFF_LUA_STATE_G);
|
||||
walk(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTGC));
|
||||
walk(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTUDATA));
|
||||
const auto buckets = Game::Read<const uint8_t *const *>(g, Offsets::OFF_LUA_G_STRT_HASH);
|
||||
const int size = Game::Read<int>(g, Offsets::OFF_LUA_G_STRT_SIZE);
|
||||
for (int i = 0; i < size; ++i)
|
||||
walk(buckets[i]);
|
||||
}
|
||||
|
||||
// `luaC_sizeall`: recount every tag's bytes from the live objects.
|
||||
void UpdateUsage();
|
||||
void UpdateUsage(const void *L);
|
||||
|
||||
// `luaC_getsize`: a tag's bytes as of the last `UpdateUsage`; 0 if unseen.
|
||||
uint64_t Usage(const char *tag);
|
||||
|
||||
Reference in New Issue
Block a user