From 6c5269e3e68a880589aa9520d3327e569e8bb12f Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:31:39 -0500 Subject: [PATCH] feat(profiling): add per-addon memory and CPU usage Backport UpdateAddOnMemoryUsage / GetAddOnMemoryUsage and the script profiling family (scriptProfile, GetFunctionCPUUsage, UpdateAddOnCPUUsage, GetAddOnCPUUsage, GetFrameCPUUsage, GetEventCPUUsage, GetScriptCPUUsage, ResetCPUUsage), mirroring 3.3.5's engine mechanism. - Gc::Hooks owns the GC lifecycle hooks (luaC_link, newlstr, luaS_newudata, freeobj); Gc::Tag stamps each new object with the engine's context word (3.3.5's lua_tainted rule) and sums sizes with the engine's own free sizes. - lua_gctag pins: the addon load window (two call patches in FUN_ADDON_LOAD_ONE) and template application (engine single-name sites + Xml::Inherits lists). - Profile::Cpu keeps 3.3.5's per-closure Profile records through Lua's own call/return hook, installed only when scriptProfile is 1; tail calls, error unwinds and coroutines handled. - ScriptArgs observers time every handler run; signals counted at each dispatcher (call-site redirect inside FUN_FIRE_EVENT, not a MinHook). - Frame::ScriptSlots enumerates a frame's script slots without creating them, for GetFrameCPUUsage. - Add the unordered_dense submodule (v5.3.2); share ResolveRecord via addons/Registry; fix the AddOnEntry inline-name comments. --- .gitmodules | 3 + CMakeLists.txt | 3 +- README.md | 3 + docs/API.md | 137 ++++++++++++ external/unordered_dense | 1 + src/Offsets.h | 189 +++++++++++++++-- src/addons/Info.cpp | 63 +----- src/addons/MemoryUsage.cpp | 199 ++++++++++++++++++ src/addons/Registry.cpp | 63 ++++++ src/addons/Registry.h | 9 + src/coroutine/Library.cpp | 6 + src/event/Custom.cpp | 3 + src/frame/ClickEvents.cpp | 11 + src/frame/ExtraScripts.cpp | 9 + src/frame/ScriptArgs.cpp | 30 ++- src/frame/ScriptArgs.h | 14 ++ src/frame/ScriptSlots.cpp | 64 ++++++ src/frame/ScriptSlots.h | 45 ++++ src/gc/Hooks.cpp | 92 +++++++++ src/gc/Hooks.h | 47 +++++ src/gc/Tag.cpp | 118 +++++++++++ src/gc/Tag.h | 64 ++++++ src/profile/Cpu.cpp | 412 +++++++++++++++++++++++++++++++++++++ src/profile/Cpu.h | 44 ++++ src/profile/FrameTime.cpp | 104 ++++++++++ src/profile/ScriptTime.cpp | 146 +++++++++++++ src/profile/ScriptTime.h | 28 +++ src/tooltip/SetEvents.cpp | 9 + src/xml/FrameLoad.cpp | 23 +++ src/xml/FrameLoad.h | 4 + src/xml/Inherits.cpp | 14 +- src/xml/TemplateTag.cpp | 108 ++++++++++ 32 files changed, 1989 insertions(+), 76 deletions(-) create mode 160000 external/unordered_dense create mode 100644 src/addons/MemoryUsage.cpp create mode 100644 src/addons/Registry.cpp create mode 100644 src/frame/ScriptSlots.cpp create mode 100644 src/frame/ScriptSlots.h create mode 100644 src/gc/Hooks.cpp create mode 100644 src/gc/Hooks.h create mode 100644 src/gc/Tag.cpp create mode 100644 src/gc/Tag.h create mode 100644 src/profile/Cpu.cpp create mode 100644 src/profile/Cpu.h create mode 100644 src/profile/FrameTime.cpp create mode 100644 src/profile/ScriptTime.cpp create mode 100644 src/profile/ScriptTime.h create mode 100644 src/xml/TemplateTag.cpp diff --git a/.gitmodules b/.gitmodules index 28402769..bbdd82d8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "external/tinycbor"] path = external/tinycbor url = https://github.com/intel/tinycbor.git +[submodule "external/unordered_dense"] + path = external/unordered_dense + url = https://github.com/martinus/unordered_dense.git diff --git a/CMakeLists.txt b/CMakeLists.txt index cd3b63af..c3de773c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,7 +133,8 @@ target_include_directories( ${CMAKE_BINARY_DIR}/generated external/minhook/include external - external/tinycbor/src) + external/tinycbor/src + external/unordered_dense/include) set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "ClassicAPI") diff --git a/README.md b/README.md index 103925f5..7710262a 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ The flagship feature: run modern Lua 5.1 addon code on 1.12's Lua 5.0 VM. | [Retail-like `/reload`](docs/API.md#reload-picks-up-new-addons-and-new-files) | `/reload` picks up addon changes made while the game runs. A new folder under `Interface\AddOns\` registers and loads as a normal addon, new files added to an existing addon's TOC load, a newly installed addon's first SavedVariables save survives `/reload`, `##` metadata edits (`## SavedVariables:`, `## Dependencies:`, `## Title:`, …) take effect, and a deleted addon folder drops from the addon list. | | [Multi-flavor & conditional TOC](docs/API.md#conditional-and-multi-flavor-toc-loading) | Loads modern multi-flavor addons that ship one folder. Selects a version-specific TOC (`_ClassicAPI.toc` or `_Turtle.toc`) and the matching keybinding file (`Bindings_ClassicAPI.xml` / `Bindings_Turtle.xml`), accepts a comma-separated `## Interface:` version list (compatible when it includes the client version `11200`), and honors per-line `[AllowLoadGameType]` / `[AllowLoadTextLocale]` conditions and `[Family]` / `[Game]` / `[TextLocale]` path variables inside a TOC. | | [Launch switches](docs/API.md#launch) | Options you add to the shortcut that starts the game. `-config ` reads and writes `WTF\` in place of `WTF\Config.wtf`, so one install can hold several settings profiles. `-gluescript` and `-gluescriptFile` run Lua at the login and character-select screens, every time those screens appear. `-gamescript` and `-gamescriptFile` run it one time, after you enter the world. Each script runs after the interface has loaded, so it can call interface functions and use frames. | +| [Addon memory and CPU usage](docs/API.md#profiling) | Addon managers and performance tools can show how much Lua memory and CPU time each addon uses. `GetAddOnMemoryUsage` gives the memory of each addon. When `scriptProfile` is `1`, the CPU functions give the time of each addon, function, frame, and event. With profiling off, the CPU functions cost nothing. | | [Any texture size](docs/API.md#texture-size-and-shape) | Textures load at any size and any shape. Width and height do not need to be powers of two, and there is no fixed upper limit. The only limit is the maximum texture size of your graphics card. This applies to every texture path: `SetTexture`, sprite sheets, inline textures, tooltips, and masks. The client allocates memory for a large texture only when an addon loads one, so small textures cost nothing extra. | Smaller behavior fixes, such as frame alpha and `SetParent` frame levels, are @@ -125,6 +126,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th | [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` | | [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` | | [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GetClass`, `C_PlayerInfo.GetName`, `C_PlayerInfo.GetRace`, `C_PlayerInfo.GetSex`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `C_PlayerInfo.IsConnected` | +| [Profiling](docs/API.md#profiling) | `GetAddOnCPUUsage`, `GetAddOnMemoryUsage`, `GetEventCPUUsage`, `GetFrameCPUUsage`, `GetFunctionCPUUsage`, `GetScriptCPUUsage`, `ResetCPUUsage`, `UpdateAddOnCPUUsage`, `UpdateAddOnMemoryUsage` | | [Quest](docs/API.md#quest) | `C_QuestLog.GetInfo`, `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestLogTitle`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` | | [Sound](docs/API.md#sound) | `C_Sound.GetRecentSoundFiles`, `C_Sound.GetSoundScaledVolume`, `C_Sound.IsPlaying`, `C_Sound.PlayItemSound`, `C_Sound.PlaySound`, `C_Sound.PlaySoundWithOptions`, `C_Sound.PlayVocalErrorSound`, `MuteSoundFile`, `PlaySound`, `PlaySoundFile`, `StopSound`, `UnmuteSoundFile` | | [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCastCount`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectInfo`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellLossOfControlCooldown`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` | @@ -224,6 +226,7 @@ menu and click AddOn Options, then ClassicAPI. | [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. | | | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. | | | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. | +| [Profiling](docs/API.md#scriptprofile) | `scriptProfile` | `0` | `1` turns on CPU profiling for the `*CPUUsage` functions. The change starts after `/reload`. | | [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract) | `SoftTargetInteract` | `3` | Turns `softinteract` on: `2` keyboard and mouse, `3` always, `0` / `1` off. | | | `SoftTargetInteractArc` | `0` | Where the object can be: `0` straight ahead of the camera, `1` within 30° of it, `2` any direction. | | | `SoftTargetInteractRange` | `10` | Maximum distance in yards. | diff --git a/docs/API.md b/docs/API.md index 203291a1..8da27dce 100644 --- a/docs/API.md +++ b/docs/API.md @@ -672,6 +672,18 @@ build instructions. - [`C_PlayerInfo.CanUseItem(itemID)`](#c_playerinfocanuseitemitemid) - [`C_PlayerInfo.GUIDIsPlayer(guid)` / `GUIDIsCreature` / `GUIDIsPet` / `GUIDIsGameObject`](#c_playerinfoguidisplayerguid--guidiscreature--guidispet--guidisgameobject) - [`C_PlayerInfo.GetName / GetClass / GetRace / GetSex / IsConnected(playerLocation)`](#c_playerinfogetname--getclass--getrace--getsex--isconnectedplayerlocation) + +- [Profiling](#profiling) + - [`UpdateAddOnMemoryUsage()` / `GetAddOnMemoryUsage(index or "name")`](#updateaddonmemoryusage--getaddonmemoryusageindex-or-name) + - [`scriptProfile`](#scriptprofile) + - [`GetFunctionCPUUsage(func [, includeSubroutines])`](#getfunctioncpuusagefunc--includesubroutines) + - [`UpdateAddOnCPUUsage()` / `GetAddOnCPUUsage(index or "name")`](#updateaddoncpuusage--getaddoncpuusageindex-or-name) + - [`GetFrameCPUUsage(frame [, includeChildren])`](#getframecpuusageframe--includechildren) + - [`GetEventCPUUsage([event])`](#geteventcpuusageevent) + - [`GetScriptCPUUsage()`](#getscriptcpuusage) + - [`ResetCPUUsage()`](#resetcpuusage) + - [`_classicapi_LuaHeapAudit()` / `_classicapi_GcTagOf(value)`](#_classicapi_luaheapaudit--_classicapi_gctagofvalue) + - [Quest](#quest) - [`C_QuestLog.GetInfo(questLogIndex)`](#c_questloggetinfoquestlogindex) - [`C_QuestLog.GetQuestLogTitle(questLogIndex)`](#c_questloggetquestlogtitlequestlogindex) @@ -15889,6 +15901,131 @@ local sex = C_PlayerInfo.GetSex(loc) raises (the engine's token→GUID resolver rejects it), same as passing it to `UnitName`/`UnitClass` directly. +## Profiling + +These functions measure how much Lua memory and CPU time each addon uses. +Addon managers and performance tools read them. + +### `UpdateAddOnMemoryUsage()` / `GetAddOnMemoryUsage(index or "name")` + +`GetAddOnMemoryUsage` returns the Lua memory that an addon uses, in +kilobytes. The value comes from the last call to `UpdateAddOnMemoryUsage`. +Call `UpdateAddOnMemoryUsage` first. Each call counts all memory again, so +do not call it every frame. + +```lua +UpdateAddOnMemoryUsage() +print(GetAddOnMemoryUsage("MyAddon")) -- 854.04 +print(GetAddOnMemoryUsage(3)) -- the third addon in the list +``` + +- The memory of an addon is all Lua memory that the addon's code makes. This + includes the memory that its file load, its SavedVariables, and its + scripts make. +- The objects that an XML template makes belong to the addon that defines + the template. This is true when a different addon uses the template. +- Blizzard addons, unknown names, and addons that have not loaded return `0`. +- An index that is not in the range `1..GetNumAddOns()` raises an error. + +### `scriptProfile` + +A setting that turns CPU profiling on (`1`) or off (`0`, the default). The +CPU functions in this section return `0` while profiling is off. The game +reads the setting when the UI loads, so a change starts after `/reload`. + +``` +/console scriptProfile 1 +/reload +``` + +Profiling makes each Lua function call slower. Turn it on only when you +measure. + +### `GetFunctionCPUUsage(func [, includeSubroutines])` + +Returns two values: the time in milliseconds that the function ran, and the +number of calls to it. If `includeSubroutines` is `true` (the default), the +time includes the functions that it calls. If `includeSubroutines` is +`false`, the time is only the function's own code. + +```lua +ResetCPUUsage() +MyAddon_Update() +print(GetFunctionCPUUsage(MyAddon_Update)) -- 11.81 1 +print(GetFunctionCPUUsage(MyAddon_Update, false)) -- 0.004 1 +``` + +- A recursive call counts in the call count. Its time counts only once. +- A coroutine that waits in `coroutine.yield` does not add time. +- An argument that is not a function raises an error. + +### `UpdateAddOnCPUUsage()` / `GetAddOnCPUUsage(index or "name")` + +`GetAddOnCPUUsage` returns the time in milliseconds that an addon's +functions ran. The value comes from the last call to `UpdateAddOnCPUUsage`. +The time of each function counts once, for the addon that made the +function. + +```lua +UpdateAddOnCPUUsage() +print(GetAddOnCPUUsage("MyAddon")) -- 23.96 +``` + +### `GetFrameCPUUsage(frame [, includeChildren])` + +Returns two values: the time in milliseconds that the frame's scripts ran, +and the number of calls to them. Each script counts, `OnUpdate` included. +If `includeChildren` is `true` (the default), the values include the child +frames of the frame. + +```lua +print(GetFrameCPUUsage(MyFrame)) -- the frame and its children +print(GetFrameCPUUsage(MyFrame, false)) -- the frame only +``` + +A script that you remove with `SetScript(name, nil)` does not count. + +### `GetEventCPUUsage([event])` + +Returns two values: the time in milliseconds that event handlers ran, and +the number of events that fired. The values are for all events. The +`event` argument has no effect. + +```lua +print(GetEventCPUUsage()) -- 39.06 27 +``` + +The event count increases also when profiling is off. The time stays `0` +when profiling is off. + +### `GetScriptCPUUsage()` + +Returns the time in milliseconds that all frame scripts ran. When a script +runs inside a different script, the time counts for both scripts. + +### `ResetCPUUsage()` + +Sets all CPU times and call counts to `0`, and the event count too. Memory +values do not change. + +### `_classicapi_LuaHeapAudit()` / `_classicapi_GcTagOf(value)` + +Two aids for developers who test the memory values. + +`_classicapi_LuaHeapAudit()` returns three values: the bytes of all Lua +objects, the total bytes of Lua memory, and the number of objects that +belong to an addon. The first value is a little less than the second. + +`_classicapi_GcTagOf(value)` returns the name of the addon that a table, +function, userdata, or thread belongs to. It returns `nil` for an object +that the game's own code made. For an object that a macro or `/run` made, +it returns a hexadecimal string such as `"0x0083DE24"`. + +```lua +print(_classicapi_GcTagOf(MyAddon_Update)) -- "MyAddon" +print(_classicapi_GcTagOf(UIParent)) -- nil +``` + ## Quest ### `C_QuestLog.GetInfo(questLogIndex)` diff --git a/external/unordered_dense b/external/unordered_dense new file mode 160000 index 00000000..ddd49116 --- /dev/null +++ b/external/unordered_dense @@ -0,0 +1 @@ +Subproject commit ddd49116e63be7345b448351cad153653a712fdb diff --git a/src/Offsets.h b/src/Offsets.h index ed8a15b9..2c3dbced 100644 --- a/src/Offsets.h +++ b/src/Offsets.h @@ -7722,6 +7722,110 @@ enum Offsets { // `CMP EDX,[ECX+0x1C]` at 0x006FA70E (array pointer at +0x0C). OFF_LUA_TABLE_SIZEARRAY = 0x1C, + // --- Lua 5.0 GC object lifecycle (Lua::GcHooks) --------------------------- + // Every collectable object is created through one of three functions and + // freed through one. Proven by xrefs: luaC_link's six callers are luaH_new, + // luaF_newCclosure, luaF_newLclosure, luaF_newproto, luaF_close (a closed + // upvalue, tt 10) and luaE_newthread (tt 8); newlstr's only caller is + // luaS_newlstr; luaS_newudata's only caller is lua_newuserdata. freeobj's + // only caller is sweeplist (0x006F7210, from luaC_sweep twice and + // sweepstrings), and each per-type free function's only caller is freeobj. + // `__fastcall(L, o)` + `tt` on the stack, RET 4: next = rootgc, rootgc = o, + // marked = 0, tt = arg. + FUN_LUA_C_LINK = 0x006F7B20, + // `__fastcall(L, str)` + `len, hash` on the stack, RET 8 → TString*. + // Allocates len+0x11, links into strt.hash[hash & (size-1)]. + FUN_LUA_S_NEWLSTR = 0x006F9D90, + // `__fastcall(L, size)` → Udata*. Allocates size+0x10, tt 7, metatable + // [G+0x48], links into rootudata [G+0x14]. + FUN_LUA_S_NEWUDATA = 0x006F9E30, + // `__fastcall(L, o)`. Switch on tt-4 through the jump table at 0x006F72D0. + FUN_LUA_C_FREEOBJ = 0x006F7260, + // lua_State: the global state (luaC_link reads `[ecx+0x10]`). + OFF_LUA_STATE_G = 0x10, + // global_State: string table {hash@+4, nuse@+8, size@+0xC} (newlstr's + // bucket insert), rootgc / rootudata (luaC_link, luaS_newudata, + // luaC_sweep), nblocks = bytes in use (lua_newtable's GC threshold test + // `cmp [G+0x28],[G+0x24]`; what gcinfo reports). + OFF_LUA_G_STRT_HASH = 0x04, + OFF_LUA_G_STRT_SIZE = 0x0C, + OFF_LUA_G_ROOTGC = 0x10, + OFF_LUA_G_ROOTUDATA = 0x14, + OFF_LUA_G_NBLOCKS = 0x28, + // GCheader: next@+0, tt@+4 (luaC_link writes both; freeobj switches on tt). + OFF_LUA_GCHEADER_NEXT = 0x00, + OFF_LUA_GCHEADER_TT = 0x04, + // GC type tags past TYPE_THREAD, from freeobj's jump table. + LUA_TT_PROTO = 9, + LUA_TT_UPVAL = 10, + // Object sizes, read from the sizes freeobj and its per-type free + // functions pass to luaM_irealloc (so they match the engine's own + // accounting exactly): + // string len@+0xC + 0x11 (freeobj 0x006F729B) + // udata len@+0xC + 0x10 (freeobj 0x006F72B4) + // upval 0x20 (freeobj 0x006F727D) + // table 0x20 + 16*sizearray@+0x1C + (lsizenode@+7 ? 40 << lsizenode : 0) + // (luaH_free 0x006FA690; a 0 lsizenode is the shared dummy node) + // closure isC@+6 ? (nupvalues@+7 + 1)*16 : nupvalues*4 + 0x24 + // (luaF_freeclosure 0x006FA110) + // proto 0x48 + 4*sizecode@+0x2C + 4*sizep@+0x34 + 16*sizek@+0x28 + + // 4*sizelineinfo@+0x30 + 12*sizelocvars@+0x38 + 4*sizeupvalues@+0x24 + // (luaF_freeproto 0x006FA050) + // thread 0x68 + 16*stacksize@+0x20 + 24*size_ci(u16)@+0x2C + // (luaE_freethread 0x006F6C90 → 0x006F6CE0 stack/ci, 0x006F6CC0 state) + OFF_LUA_TSTRING_LEN = 0x0C, + OFF_LUA_UDATA_LEN = 0x0C, + OFF_LUA_TABLE_LSIZENODE = 0x07, + OFF_LUA_CLOSURE_ISC = 0x06, + OFF_LUA_CLOSURE_NUPVALUES = 0x07, + OFF_LUA_PROTO_SIZEK = 0x28, + OFF_LUA_PROTO_SIZECODE = 0x2C, + OFF_LUA_PROTO_SIZELINEINFO = 0x30, + OFF_LUA_PROTO_SIZELOCVARS = 0x38, + OFF_LUA_STATE_STACKSIZE = 0x20, + OFF_LUA_STATE_SIZE_CI = 0x2C, + SIZEOF_LUA_TSTRING_HEADER = 0x11, // header + the terminating NUL + SIZEOF_LUA_UDATA_HEADER = 0x10, + SIZEOF_LUA_UPVAL = 0x20, + SIZEOF_LUA_TABLE = 0x20, + SIZEOF_LUA_TVALUE = 0x10, + SIZEOF_LUA_NODE = 0x28, + SIZEOF_LUA_LCLOSURE_BASE = 0x24, + SIZEOF_LUA_PROTO = 0x48, + SIZEOF_LUA_LOCVAR = 0x0C, + SIZEOF_LUA_STATE = 0x68, + SIZEOF_LUA_CALLINFO = 0x18, + + // --- Lua 5.0 debug hooks (Profile::Cpu) ----------------------------------- + // `lua_sethook(L, func, mask, count)` — `__fastcall`, mask/count on the + // stack, RET 8: stores count at +0x34/+0x38, mask byte +0x30, func +0x3C, + // clears the inited byte +0x32; an empty func or mask clears both. The + // engine never calls it and no other loaded DLL references it. + // Hooks are per lua_State and NOT inherited: luaE_newthread's preinit + // (0x006F6C40) zeroes them (and sets allowhook +0x31 = 1). + LUA_SET_HOOK = 0x006FBA40, + // luaD_callhook (0x006F5F80) calls the hook `__fastcall(L, ar)` when it is + // set and allowhook is 1. CALL (event 0) fires for a C function in + // luaD_precall (0x006F6191) and for a Lua function at the VM's call entry + // 0x006F8730, which OP_CALL (0x006F975C) and OP_TAILCALL (0x006F9827) jump + // back to; RET (1) and TAILRET (4) come from callrethooks (0x006F6540), + // which luaD_poscall runs before popping the record. `ar` (lua_Debug): + // event @+0, the call record's index @+0x5C (0 for TAILRET). + LUA_HOOK_CALL = 0, + LUA_HOOK_RET = 1, + LUA_MASK_CALL = 1, + LUA_MASK_RET = 2, + OFF_LUA_DEBUG_EVENT = 0x00, + OFF_LUA_DEBUG_I_CI = 0x5C, + // lua_State: base_ci (luaD_callhook's index = (ci - [L+0x28]) / 24). + OFF_LUA_STATE_BASE_CI = 0x28, + // CallInfo: base @+0 (luaD_precall stores func + 0x10, so the called + // function's TValue is base - 0x10); tailcalls @+0x14 — luaD_precall + // zeroes it for a Lua call (0x006F60FB), OP_TAILCALL increments the + // replaced record's (`inc [ci-4]` at 0x006F9809) before re-entering. + OFF_LUA_CALLINFO_BASE = 0x00, + OFF_LUA_CALLINFO_TAILCALLS = 0x14, + // --- Lua 5.0 parser internals: `for` loop scoping ------------------------- // forbody(ls, base, line, nvars, isnum) -- stock 5.0.2, decompiled: // adjustlocalvars(nvars); check(TK_DO); enterblock(&bl, 1); prep = @@ -7856,6 +7960,14 @@ enum Offsets { // read it on demand in helpers that run outside a Lua callback (e.g. // RegisterTableFunction during LoadScriptFunctions). VAR_LUA_STATE = 0x00CEEF74, + // `FrameScript_GetContext` — `mov eax,[VAR_LUA_STATE]; ret`. + FUN_FRAMESCRIPT_GET_CONTEXT = 0x007040D0, + // FUN_FIRE_EVENT's `call FUN_FRAMESCRIPT_GET_CONTEXT` (E8, 5 bytes), right + // after its empty-listener early-out — once per signal that has a + // listener. ECX/EDX are dead across it (both reloaded after). Profile:: + // ScriptTime counts signals here rather than MinHooking FUN_FIRE_EVENT, + // which nampower and SuperWoWhook also target (a co-hook there crashed). + CALL_FIRE_EVENT_GET_CONTEXT = 0x00703F78, // zlib 1.2.2, statically linked into WoW.exe (version string at // `0x008745F0`, copyright banners at `0x00815528`/`0x00815608`). @@ -8881,26 +8993,28 @@ enum Offsets { FUN_SERVER_TIME_LOCAL_SECONDS = 0x00429580, // AddOn registry. The engine keeps a flat 4-byte-stride array of - // `AddOnEntry *` at `[VAR_ADDON_ARRAY]`, with the in-use count at - // `[VAR_ADDON_COUNT]`. Each entry's first 12 bytes are an inline - // null-terminated name buffer, so an `AddOnEntry *` can be passed - // directly to `lua_pushstring` to push the addon's name (which is - // exactly what `Script_GetAddOnInfo` does for ret1). + // addon NAME POINTERS at `[VAR_ADDON_ARRAY]` — each element is an + // entry's `[entry + OFF_ADDON_ENTRY_NAME_PTR]` (the display-array + // builder copies that field; `FUN_ADDON_GET_BY_INDEX` returns the + // element) — with the in-use count at `[VAR_ADDON_COUNT]`. Being the + // name string itself, an element can be passed directly to + // `lua_pushstring` (which is exactly what `Script_GetAddOnInfo` does for + // ret1). It is NOT the entry: `entry + 0` is the name hash (the loader's + // bucket compare `cmp [ebx],esi` at 0x0051F2A1). It is also the exact + // pointer the loader installs as the context word, so it is the addon's + // memory/CPU tag. // // Per-field accessors (all `__fastcall(ecx=name) → char* or NULL`): // each one looks up the addon by name in the engine's hash table, // then reads a single hardcoded field from the addon's metadata // hash table and returns its string value (NULL if the addon - // doesn't exist or the field is missing). The `name` arg can be - // either a real C string OR an `AddOnEntry *` — the entry's - // first 12 bytes are an inline null-terminated name, so it - // doubles as a valid `const char *`. + // doesn't exist or the field is missing). // // We use these directly in the `C_AddOns.*` wrappers to avoid // dispatching to `Script_GetAddOnInfo` (which pushes a 7-tuple // when we only want one field, and worse, errors via `lua_error` // for out-of-range numeric indices). - FUN_ADDON_GET_BY_INDEX = 0x0051DF00, // (idx0Based) → AddOnEntry* or NULL + FUN_ADDON_GET_BY_INDEX = 0x0051DF00, // (idx0Based) → name pointer or NULL FUN_ADDON_GET_COUNT = 0x0051DEF0, // () → [VAR_ADDON_COUNT]; the engine's GetNumAddOns read // `"AddOn index must be in the range of 1 to %d"` — the luaL_error // format stock `Script_GetAddOnDependencies` (`0x0048E5E0`) raises with @@ -8909,9 +9023,9 @@ enum Offsets { FUN_ADDON_GET_TITLE_BY_NAME = 0x0051DF20, // (name) → title string or NULL FUN_ADDON_GET_NOTES_BY_NAME = 0x0051E050, // (name) → notes string or NULL - // `__fastcall(const char *nameOrEntry) → AddOnEntry*+0x48 or NULL`. - // Hash-table lookup by name (accepts an entry pointer too — its - // first 12 bytes are the inline name), returning a pointer to the + // `__fastcall(const char *name) → AddOnEntry*+0x48 or NULL`. + // Hash-table lookup by name (a `FUN_ADDON_GET_BY_INDEX` result works + // because it IS a name pointer), returning a pointer to the // matched record's RequiredDeps descriptor (`record + 0x48`), or // NULL on a miss. Stock `Script_GetAddOnDependencies` (`0x0048E5E0`) // calls this then reads count@+4 / data@+8 off the return to walk @@ -8984,6 +9098,19 @@ enum Offsets { // dependency pulled in during them) execute inside it. // `AddOns::LoadState` co-hooks this to tell the two states apart. FUN_ADDON_LOAD_ONE = 0x0051F240, + // The loader's context window — 3.3.5's `LoadAddOn` brackets the same + // span with `lua_gctag = addonName`. After the dependency loop it saves + // the context word, sets it to `[entry+0x24] ? [entry+0x14] : 0`, runs + // the TOC files, Bindings.xml and both SavedVariables files, restores the + // word, and only then fires ADDON_LOADED (0x0051F5AD). EBX = the entry + // throughout. Both sites are reached on every path through the window + // (the context write before the open is conditional; these are not) and + // no jump lands inside either: + // OPEN `33 C0 8D 4D E8` (xor eax,eax; lea ecx,[ebp-0x18]), 5 bytes + // CLOSE `A1 C4 EA CE 00` (mov eax,[0x00CEEAC4]), 5 bytes; also the + // target of the digest skip `jnz` at 0x0051F553 + SITE_ADDON_LOAD_WINDOW_OPEN = 0x0051F38D, + SITE_ADDON_LOAD_WINDOW_CLOSE = 0x0051F576, // `Script_LoadAddOn` — the stock `LoadAddOn(index or name)` global. // Pushes two values either way: `(1, nil)` on success, or @@ -9072,6 +9199,12 @@ enum Offsets { // packet-delivered state (and their MPQ TOCs never change anyway). OFF_ADDON_ENTRY_SECURE = 0x28, + // Signature status (dword) — 3.3.5's `UIADDON::m_status`. Nonzero = an + // insecure (user) addon. `FUN_ADDON_LOAD_ONE` runs the addon's files under + // the context word `[+0x24] ? [+0x14] : 0` (0x0051F37E..0x0051F387) and + // checks the TOC digest only when it is 0 (0x0051F54E). + OFF_ADDON_ENTRY_STATUS = 0x24, + // Complete single-`AddOnEntry` destructor — vtable slot 0 of the // registry's node factory (`[0x00BE1B60]` = `&PTR_FUN_00808c7c`, // installed by the boot init `FUN_0051C360`; the login teardown @@ -10722,10 +10855,31 @@ enum Offsets { XML_TEMPLATE_BUCKET_STRIDE = 0xC, OFF_XML_TEMPLATE_BUCKET_LINKOFF = 0x0, OFF_XML_TEMPLATE_BUCKET_HEAD = 0x8, - // Registry node payload: template name string @+0x14, parsed definition - // XML node @+0x18 (hash @+0x0, virtual flag @+0x1C — unused here). + // Registry node payload: name hash @+0x0 (FUN_SSTR_HASH_HT, the lookup's + // `cmp [ebx],esi`), template name string @+0x14, parsed definition XML + // node @+0x18, and the context word current when it registered @+0x1C — + // the registrar FUN_006EE500(node, name, context, status) stores its + // third argument there (0x006EE6BE), and the XML file loader passes + // VAR_FRAMESCRIPT_EXEC_CONTEXT: the owning addon's name, or 0 for the + // client's own XML. 3.3.5's FrameXML_AcquireHashNode returns it as the + // template's taint. + OFF_XML_TEMPLATE_NODE_HASH = 0x0, OFF_XML_TEMPLATE_NODE_NAME = 0x14, OFF_XML_TEMPLATE_NODE_DEF = 0x18, + OFF_XML_TEMPLATE_NODE_CONTEXT = 0x1C, + // The two places the client applies a single `inherits` template — the + // 1.12 counterparts of the two sites where 3.3.5 sets `lua_gctag` to the + // template's taint (CSimpleFrame::LoadXML and LoadXMLInheritance): + // FUN_XML_FRAME_LOAD: `call FUN_XML_TEMPLATE_LOOKUP` (E8, 5 bytes), then + // the re-entry through the node loader, 12 bytes with no jump into + // them: mov ecx,[ebp+0xC]; mov edx,[ebx]; push ecx; push eax; + // mov ecx,ebx; call [edx+8] (EAX = template node, EBX = loader). + // FUN_XML_FRAME_LOAD_CHILD_FRAMES: `call FUN_XML_TEMPLATE_LOOKUP`, then + // the recursive `call FUN_XML_FRAME_LOAD_CHILD_FRAMES` (E8, 5 bytes). + CALL_XML_FRAME_LOAD_TEMPLATE_LOOKUP = 0x0076984A, + SITE_XML_FRAME_LOAD_TEMPLATE_APPLY = 0x00769853, + CALL_XML_CHILD_FRAMES_TEMPLATE_LOOKUP = 0x0076A085, + CALL_XML_CHILD_FRAMES_TEMPLATE_RECURSE = 0x0076A092, // Parsed XML element node: its tag-name string ("Frame" / "Button" / // "CheckButton" / …) lives at +0x8 — this is the frame "type". OFF_XML_NODE_TAG = 0x8, @@ -10915,6 +11069,11 @@ enum Offsets { // (+0x28 is VTBL_FRAME_SET_FRAME_ALPHA.) VTBL_FRAME_DELETING_DESTRUCTOR = 0x00, VTBL_FRAME_GET_SCRIPT_METHOD = 0x08, + // +0x0C the script-name resolver (this, const char *name) -> slot*, ret 4: + // FUN_FRAME_SCRIPT_RESOLVER for Frame and MessageFrame, each other + // type's extension of it otherwise (benilla-re ui/xmltypes); what + // SetScriptObjectScript (0x007025C0) and SetScript/GetScript use. + VTBL_FRAME_SCRIPT_RESOLVER = 0x0C, VTBL_FRAME_IS_A = 0x10, VTBL_FRAME_IS_OBJECT_TYPE = 0x18, VTBL_FRAME_GET_OBJECT_TYPE = 0x1C, diff --git a/src/addons/Info.cpp b/src/addons/Info.cpp index cc622e0a..d3c1516b 100644 --- a/src/addons/Info.cpp +++ b/src/addons/Info.cpp @@ -27,6 +27,7 @@ #include "Game.h" #include "Offsets.h" #include "addons/LoadState.h" +#include "addons/Registry.h" #include @@ -44,8 +45,7 @@ using CanLoadFn_t = uint8_t(__fastcall *)(const uint8_t *entry, char fullCheck, const char *accountName); using GetAccountName_t = const char *(*)(); // Hash-lookup by name → the matched record's RequiredDeps descriptor -// (`record + 0x48`), NULL on miss. Accepts an entry pointer too (its -// first bytes are the inline name). We use it only to recover the +// (`record + 0x48`), NULL on miss. We use it only to recover the // record base; see `Script_GetAddOnOptionalDependencies`. using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *nameOrEntry); @@ -54,8 +54,7 @@ using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *nameOrEntry); // a NUL-terminated string starting at the input pointer and case- // insensitively match it against the registry, so either of these // shapes works: -// - an AddOnEntry pointer from `GetByIndex` (the entry's first -// 12 bytes ARE the inline name buffer) +// - the name pointer `GetByIndex` returns (`[entry+0x14]`) // - a raw `const char *` to a Lua string // // Mirrors `Script_GetAddOnInfo`'s own input handling exactly — it @@ -265,11 +264,10 @@ int __fastcall Script_GetAddOnSecurity(void *L) { // tracks from the loader itself. // // Resolves both numeric indices (via `FUN_ADDON_GET_BY_INDEX`) and -// string names. `IS_LOADED` accepts an entry pointer as its name arg -// because the entry's first 12 bytes are the inline NUL-terminated -// directory name (same as the other per-field accessors), so we can -// feed it either an entry or a Lua-side string transparently — and that -// same pointer is the name `LoadState` matches on. +// string names. `FUN_ADDON_GET_BY_INDEX` returns the entry's name +// pointer, so `IS_LOADED` (like the other per-field accessors) takes +// either that or a Lua-side string transparently — and that same +// pointer is the name `LoadState` matches on. int __fastcall Script_IsAddOnLoaded(void *L) { const uint8_t *entry = ResolveAddOnName(L); if (entry == nullptr) { @@ -309,49 +307,6 @@ int __fastcall Script_DoesAddOnExist(void *L) { return 1; } -// Resolves arg 1 to the addon's registry record, with the arg handling -// of stock `Script_GetAddOnDependencies` (`0x0048E5E0`, identical to -// 4.3.4's `Script_GetAddOnOptionalDependencies`), not `ResolveAddOnName`: -// `lua_isnumber` first (so a numeric string like `"3"` is an index), an -// out-of-range index raises the engine's own range error with the addon -// count, anything else non-string raises `usage`, and an unknown *name* -// returns nullptr with no error. -// -// Either input is then routed through the by-name hash lookup -// `FUN_ADDON_RESOLVE_REQ_DEPS` to reach the record. -// `FUN_ADDON_GET_BY_INDEX` returns a *name pointer*, NOT the record — -// reading record offsets off it lands in string memory and crashes (a -// numeric-path caller like pfUI's addon list hit exactly that). The -// resolver returns the RequiredDeps descriptor at `record+0x48`; -// subtract to recover the base. -const uint8_t *ResolveRecord(void *L, const char *usage) { - const char *name; - if (Game::Lua::IsNumber(L, 1)) { - const int idx = static_cast(Game::Lua::ToNumber(L, 1)) - 1; - auto byIndex = reinterpret_cast(Offsets::FUN_ADDON_GET_BY_INDEX); - name = static_cast(byIndex(idx)); - if (name == nullptr) { - auto count = reinterpret_cast(Offsets::FUN_ADDON_GET_COUNT); - Game::Lua::Error(L, reinterpret_cast(static_cast( - Offsets::VAR_ADDON_INDEX_RANGE_ERROR_FMT)), - count()); - return nullptr; - } - } else if (Game::Lua::IsString(L, 1)) { - name = Game::Lua::ToString(L, 1); - } else { - Game::Lua::Error(L, usage); - return nullptr; - } - - auto resolve = reinterpret_cast( - Offsets::FUN_ADDON_RESOLVE_REQ_DEPS); - uint8_t *reqDesc = resolve(name); - if (reqDesc == nullptr) - return nullptr; - return reqDesc - Offsets::OFF_ADDON_REQDEPS_DESC; -} - // `C_AddOns.GetAddOnOptionalDependencies(indexOrName)` — the addon's // `## OptionalDeps:` names as multiple return values, in declared order. // The exact shape of the stock `GetAddOnDependencies`, but reading the @@ -365,7 +320,7 @@ const uint8_t *ResolveRecord(void *L, const char *usage) { // Like stock, `lua_checkstack(count)` before the pushes — a C function // is only guaranteed LUA_MINSTACK (20) free slots. int __fastcall Script_GetAddOnOptionalDependencies(void *L) { - const uint8_t *record = ResolveRecord( + const uint8_t *record = Addons::ResolveRecord( L, "Usage: C_AddOns.GetAddOnOptionalDependencies(index or \"name\")"); if (record == nullptr) return 0; @@ -391,7 +346,7 @@ int __fastcall Script_GetAddOnOptionalDependencies(void *L) { // client version) by `AddOns::TocRewrite` before the parser sees it. // nil for an unknown name. int __fastcall Script_GetAddOnInterfaceVersion(void *L) { - const uint8_t *record = ResolveRecord( + const uint8_t *record = Addons::ResolveRecord( L, "Usage: C_AddOns.GetAddOnInterfaceVersion(index or \"name\")"); if (record == nullptr) { Game::Lua::PushNil(L); diff --git a/src/addons/MemoryUsage.cpp b/src/addons/MemoryUsage.cpp new file mode 100644 index 00000000..7f8fbe77 --- /dev/null +++ b/src/addons/MemoryUsage.cpp @@ -0,0 +1,199 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +// `UpdateAddOnMemoryUsage()` / `GetAddOnMemoryUsage(index or "name")` — +// 3.3.5's `Script_UpdateAddOnMemoryUsage` (0x00511510) and +// `Script_GetAddOnMemoryUsage` (0x00511530) over `Gc::Tag`. +// +// An addon's tag is its registry name pointer `[entry+0x14]`: the exact +// pointer `FUN_ADDON_LOAD_ONE` installs as the context word while the +// addon's files run (and the one 3.3.5's `GetAddOnName` returns), so the +// lookup is a pointer match like 3.3.5's `luaC_getsize`. Secure addons run +// under context 0, are never tagged, and read 0 — as in 3.3.5. +// +// 3.3.5's `LoadAddOn` also pins `lua_gctag` to the addon for its whole +// file / Bindings / SavedVariables window, so objects made there stay the +// addon's even when its code switches the context word (calling into another +// addon's function or table makes that addon's word current). The 1.12 loader +// has the same window; two 5-byte call patches open and close a `Gc::Tag` pin +// at its edges (SITE_ADDON_LOAD_WINDOW_OPEN / _CLOSE). + +#include "Common.h" +#include "Game.h" +#include "Offsets.h" +#include "addons/Registry.h" +#include "gc/Tag.h" + +#include +#include + +namespace AddOns::MemoryUsage { + +namespace { + +constexpr uintptr_t addrExecContextDepth = Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH; + +// The loader's own context value: the name for an insecure addon, 0 for a +// secure one (3.3.5's `addon->m_status ? addonName : nullptr`). A 0 pin means +// "no override", so a secure addon's window keeps following the context word. +void __fastcall OpenLoadWindow(const uint8_t *entry) { + const bool insecure = Game::Read(entry, Offsets::OFF_ADDON_ENTRY_STATUS) != 0; + Gc::Tag::PushPin(insecure ? Game::Read(entry, Offsets::OFF_ADDON_ENTRY_NAME_PTR) + : nullptr); +} + +void CloseLoadWindow() { Gc::Tag::PopPin(); } + +// Stands in for `xor eax,eax; lea ecx,[ebp-0x18]` at the window's open. +// EBX = the entry; EBP is the loader's frame. +__declspec(naked) void LoadWindowOpenThunk() { + __asm { + pushad + mov ecx, ebx + call OpenLoadWindow + popad + xor eax, eax + lea ecx, [ebp - 18h] + ret + } +} + +// Stands in for `mov eax,[VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH]` at the close. +__declspec(naked) void LoadWindowCloseThunk() { + __asm { + pushad + call CloseLoadWindow + popad + mov eax, addrExecContextDepth + mov eax, [eax] + ret + } +} + +void PatchLoadWindow() { + static const uint8_t kOpen[5] = {0x33, 0xC0, 0x8D, 0x4D, 0xE8}; + static const uint8_t kClose[5] = {0xA1, 0xC4, 0xEA, 0xCE, 0x00}; + Common::ReplaceWithCall(Offsets::SITE_ADDON_LOAD_WINDOW_OPEN, kOpen, sizeof kOpen, + reinterpret_cast(&LoadWindowOpenThunk)); + Common::ReplaceWithCall(Offsets::SITE_ADDON_LOAD_WINDOW_CLOSE, kClose, sizeof kClose, + reinterpret_cast(&LoadWindowCloseThunk)); +} + +int __fastcall Script_UpdateAddOnMemoryUsage(void * /*L*/) { + Gc::Tag::UpdateUsage(); + return 0; +} + +int __fastcall Script_GetAddOnMemoryUsage(void *L) { + const uint8_t *record = + Addons::ResolveRecord(L, "Usage: GetAddOnMemoryUsage(index or \"name\")"); + const char *tag = + record != nullptr + ? Game::Read(record, Offsets::OFF_ADDON_ENTRY_NAME_PTR) + : nullptr; + const uint64_t bytes = tag != nullptr ? Gc::Tag::Usage(tag) : 0; + Game::Lua::PushNumber(L, static_cast(bytes) * (1.0 / 1024.0)); + return 1; +} + +// `_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(object, Offsets::OFF_LUA_GCHEADER_NEXT); + } + return total; +} + +int __fastcall Script_LuaHeapAudit(void *L) { + const uint8_t *g = Game::Read(L, Offsets::OFF_LUA_STATE_G); + uint64_t bytes = SumList(Game::Read(g, Offsets::OFF_LUA_G_ROOTGC)) + + SumList(Game::Read(g, Offsets::OFF_LUA_G_ROOTUDATA)); + const auto buckets = + Game::Read(g, Offsets::OFF_LUA_G_STRT_HASH); + const int size = Game::Read(g, Offsets::OFF_LUA_G_STRT_SIZE); + for (int i = 0; i < size; ++i) + bytes += SumList(buckets[i]); + Game::Lua::PushNumber(L, static_cast(bytes)); + Game::Lua::PushNumber(L, static_cast(Game::Read(g, Offsets::OFF_LUA_G_NBLOCKS))); + Game::Lua::PushNumber(L, static_cast(Gc::Tag::TaggedCount())); + return 3; +} + +// `_classicapi_GcTagOf(value)` → the owner tag of a table / function / +// userdata / thread: the addon's name when the tag is an addon's, else the +// raw context word as "0x…" (e.g. RunScript's), nil when untagged. +int __fastcall Script_GcTagOf(void *L) { + const char *tag = Gc::Tag::Of(Game::Lua::ToPointer(L, 1)); + if (tag == nullptr) { + Game::Lua::PushNil(L); + return 1; + } + const char *name = nullptr; + Addons::ForEachEntry([&](uintptr_t entry) { + if (Game::Read(entry + Offsets::OFF_ADDON_ENTRY_NAME_PTR) != tag) + return true; + name = tag; + return false; + }); + if (name != nullptr) { + Game::Lua::PushString(L, name); + } else { + char buf[16]; + std::snprintf(buf, sizeof buf, "0x%08X", static_cast(reinterpret_cast(tag))); + Game::Lua::PushString(L, buf); + } + return 1; +} + +const Game::Doc::Function kUpdateAddOnMemoryUsage{ + "Recounts the memory each addon uses; GetAddOnMemoryUsage returns the result of the " + "last recount.", + {}, {}, "AddOnsGlobals"}; + +const Game::Doc::Field kGetArgs[] = { + Game::Doc::Req("name", "uiAddon", "An addon index or folder name."), +}; +const Game::Doc::Field kGetRets[] = { + Game::Doc::Req("memory", "number", + "Kilobytes of Lua memory the addon's code allocated, as of the last " + "UpdateAddOnMemoryUsage."), +}; +const Game::Doc::Function kGetAddOnMemoryUsage{ + "Returns the Lua memory an addon uses, in kilobytes, as of the last " + "UpdateAddOnMemoryUsage.", + kGetArgs, kGetRets, "AddOnsGlobals"}; + +} // namespace + +static void RegisterLuaFunctions() { + // Each pass runs before the load pass; the patch is idempotent. + PatchLoadWindow(); + Game::Lua::RegisterGlobalFunction("UpdateAddOnMemoryUsage", &Script_UpdateAddOnMemoryUsage, + &kUpdateAddOnMemoryUsage); + Game::Lua::RegisterGlobalFunction("GetAddOnMemoryUsage", &Script_GetAddOnMemoryUsage, + &kGetAddOnMemoryUsage); + Game::Lua::RegisterGlobalFunction("_classicapi_LuaHeapAudit", &Script_LuaHeapAudit); + Game::Lua::RegisterGlobalFunction("_classicapi_GcTagOf", &Script_GcTagOf); +} + +static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions}; + +} // namespace AddOns::MemoryUsage diff --git a/src/addons/Registry.cpp b/src/addons/Registry.cpp new file mode 100644 index 00000000..a4fb275c --- /dev/null +++ b/src/addons/Registry.cpp @@ -0,0 +1,63 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +#include "addons/Registry.h" + +#include "Game.h" + +namespace Addons { + +namespace { + +using GetByIndex_t = void *(__fastcall *)(int idx0Based); +using GetAddOnCount_t = int(__fastcall *)(); +using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *name); + +} // namespace + +// Either input is routed through the by-name hash lookup +// `FUN_ADDON_RESOLVE_REQ_DEPS` to reach the record. +// `FUN_ADDON_GET_BY_INDEX` returns a *name pointer*, NOT the record — +// reading record offsets off it lands in string memory and crashes (a +// numeric-path caller like pfUI's addon list hit exactly that). The +// resolver returns the RequiredDeps descriptor at `record+0x48`; +// subtract to recover the base. +const uint8_t *ResolveRecord(void *L, const char *usage) { + const char *name; + if (Game::Lua::IsNumber(L, 1)) { + const int idx = static_cast(Game::Lua::ToNumber(L, 1)) - 1; + auto byIndex = reinterpret_cast(Offsets::FUN_ADDON_GET_BY_INDEX); + name = static_cast(byIndex(idx)); + if (name == nullptr) { + auto count = reinterpret_cast(Offsets::FUN_ADDON_GET_COUNT); + Game::Lua::Error(L, reinterpret_cast(static_cast( + Offsets::VAR_ADDON_INDEX_RANGE_ERROR_FMT)), + count()); + return nullptr; + } + } else if (Game::Lua::IsString(L, 1)) { + name = Game::Lua::ToString(L, 1); + } else { + Game::Lua::Error(L, usage); + return nullptr; + } + + auto resolve = reinterpret_cast( + Offsets::FUN_ADDON_RESOLVE_REQ_DEPS); + uint8_t *reqDesc = resolve(name); + if (reqDesc == nullptr) + return nullptr; + return reqDesc - Offsets::OFF_ADDON_REQDEPS_DESC; +} + +} // namespace Addons diff --git a/src/addons/Registry.h b/src/addons/Registry.h index 965010c3..65098950 100644 --- a/src/addons/Registry.h +++ b/src/addons/Registry.h @@ -47,4 +47,13 @@ template void ForEachEntry(Fn fn) { } } +// Resolves Lua arg 1 (index or name) to the addon's registry record, with +// the arg handling of stock `Script_GetAddOnDependencies` (`0x0048E5E0`) — +// the same shape 3.3.5 gives every `(index or "name")` addon global: +// `lua_isnumber` first (so a numeric string like `"3"` is an index), an +// out-of-range index raises the engine's own range error with the addon +// count, anything else non-string raises `usage`, and an unknown *name* +// returns nullptr with no error. +const uint8_t *ResolveRecord(void *L, const char *usage); + } // namespace Addons diff --git a/src/coroutine/Library.cpp b/src/coroutine/Library.cpp index 30a9c02b..2e870722 100644 --- a/src/coroutine/Library.cpp +++ b/src/coroutine/Library.cpp @@ -21,6 +21,7 @@ #include "Game.h" #include "Offsets.h" +#include "profile/Cpu.h" #include @@ -95,7 +96,11 @@ static int AuxResume(void *L, void *co, int narg) { return -1; } Game::Lua::XMove(L, co, narg); + Profile::Cpu::OnResume(co); const int s = Game::Lua::Resume(co, narg); + // The status code can't tell a yield apart (see above); the call stack can. + if (IsYielded(co)) + Profile::Cpu::OnYield(co); if (s == 0 || IsYielded(co)) { const int nres = Game::Lua::GetTop(co); if (Game::Lua::CheckStack(L, nres + 1) == 0) @@ -114,6 +119,7 @@ static int __fastcall Script_Coroutine_Create(void *L) { } Game::Lua::NewThread(L); // [arg1, NL] void *NL = Game::Lua::ToThread(L, -1); + Profile::Cpu::OnThreadCreated(NL); // threads don't inherit the hook Game::Lua::PushValue(L, 1); // [arg1, NL, fn] Game::Lua::XMove(L, NL, 1); // [arg1, NL]; NL has fn return 1; diff --git a/src/event/Custom.cpp b/src/event/Custom.cpp index 512189ac..154cad6c 100644 --- a/src/event/Custom.cpp +++ b/src/event/Custom.cpp @@ -17,6 +17,7 @@ #include "Offsets.h" #include "debug/Log.h" #include "frame/ScriptArgs.h" +#include "profile/ScriptTime.h" #include #include @@ -323,6 +324,8 @@ bool HasListeners(int slot) { } void FireValues(int slot, void *L, int firstArgIdx, int nargs) { + // A signal, listened to or not (3.3.5's FrameScript_SignalEvent counts first). + Profile::ScriptTime::CountSignal(); if (L == nullptr || !HasListeners(slot)) return; using namespace Game::Lua; diff --git a/src/frame/ClickEvents.cpp b/src/frame/ClickEvents.cpp index ca007a93..54148aa7 100644 --- a/src/frame/ClickEvents.cpp +++ b/src/frame/ClickEvents.cpp @@ -106,6 +106,7 @@ #include "Offsets.h" #include "frame/MouseRegistration.h" #include "frame/RunnerHook.h" +#include "frame/ScriptSlots.h" #include #include @@ -320,6 +321,16 @@ static const Game::HookAutoRegister _clickHook{ static const Frame::RunnerHook::AutoSubscribe _runnerSub{&OnRun}; +void ForEachSlot(const void *frame, Frame::ScriptSlots::Visit visit, void *ctx) { + const auto it = g_cells.find(const_cast(frame)); + if (it == g_cells.end()) + return; + for (const auto &slot : it->second.slot) + visit(slot, ctx); +} + +static const Frame::ScriptSlots::AutoRegister _slots{&ForEachSlot}; + } // namespace void PrepareForReload() { diff --git a/src/frame/ExtraScripts.cpp b/src/frame/ExtraScripts.cpp index 88cf1688..fc1f4ad5 100644 --- a/src/frame/ExtraScripts.cpp +++ b/src/frame/ExtraScripts.cpp @@ -16,6 +16,7 @@ #include "Game.h" #include "Offsets.h" #include "baselib/Ascii.h" +#include "frame/ScriptSlots.h" namespace Frame::ExtraScripts { @@ -54,6 +55,14 @@ void PrepareForReload() { const Game::ReloadAutoRegister _reload{&PrepareForReload}; +void ForEachSlot(const void *frame, Frame::ScriptSlots::Visit visit, void *ctx) { + for (const Script *s = g_head; s != nullptr; s = s->next) + if (const uint32_t *slot = s->Slot(frame)) + visit(slot, ctx); +} + +const Frame::ScriptSlots::AutoRegister _slots{&ForEachSlot}; + } // namespace Script::Script(const char *nameLower, uintptr_t typeIdVar) diff --git a/src/frame/ScriptArgs.cpp b/src/frame/ScriptArgs.cpp index 1d51d7df..f7ad5c81 100644 --- a/src/frame/ScriptArgs.cpp +++ b/src/frame/ScriptArgs.cpp @@ -81,6 +81,14 @@ namespace { // reimplementation risk) — the OFF escape valve for this hot path. bool g_enabled = true; +// Chained at static-init time by each `AutoObserve` constructor. +AutoObserve *g_observers = nullptr; + +void Notify(bool begin, int handlerRef, void *frame) { + for (AutoObserve *o = g_observers; o != nullptr; o = o->next) + o->fn(begin, handlerRef, frame); +} + // Engine cap: the runner stops setting arg globals at index 19 (arg1..arg19). constexpr int kMaxArgs = 19; @@ -377,8 +385,7 @@ using RunArgs_t = void(__cdecl *)(int handlerRef, void *frame, const char *fmt, const void *vaPtr); RunArgs_t g_origRunArgs = nullptr; -void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt, - const void *vaPtr) { +void RunArgs(int handlerRef, void *frame, const char *fmt, const void *vaPtr) { if (g_enabled && handlerRef != 0 && fmt != nullptr) { void *L = Game::Lua::State(); if (L != nullptr && HandlerWantsArgs(L, handlerRef) && @@ -388,13 +395,20 @@ void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt, g_origRunArgs(handlerRef, frame, fmt, vaPtr); } +void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt, + const void *vaPtr) { + Notify(true, handlerRef, frame); + RunArgs(handlerRef, frame, fmt, vaPtr); + Notify(false, handlerRef, frame); +} + // --- FUN_FRAME_INVOKE_SCRIPT co-hook (no-arg scripts) ----------------------- // Same address Tooltip::SetEvents calls directly to fire OnTooltipSet*; with // the switch enabled those fire with (self) too, which is the modern shape. using Invoke_t = void(__fastcall *)(int handlerRef, void *frame); Invoke_t g_origInvoke = nullptr; -void __fastcall Invoke_h(int handlerRef, void *frame) { +void Invoke(int handlerRef, void *frame) { if (g_enabled && handlerRef != 0 && frame != nullptr) { void *L = Game::Lua::State(); if (L != nullptr && HandlerWantsArgs(L, handlerRef) && @@ -404,6 +418,12 @@ void __fastcall Invoke_h(int handlerRef, void *frame) { g_origInvoke(handlerRef, frame); } +void __fastcall Invoke_h(int handlerRef, void *frame) { + Notify(true, handlerRef, frame); + Invoke(handlerRef, frame); + Notify(false, handlerRef, frame); +} + const Game::HookAutoRegister _runArgsHook{ Offsets::FUN_FRAME_RUN_SCRIPT_ARGS, reinterpret_cast(&RunArgs_h), @@ -443,9 +463,13 @@ void RunWithValues(void *L, int handlerRef, void *frame, int firstArgIdx, int na ArgSource src; src.stackBase = firstArgIdx; src.n = nargs; + Notify(true, handlerRef, frame); RunCore(L, handlerRef, frame, src, /*positional*/ g_enabled); + Notify(false, handlerRef, frame); } +AutoObserve::AutoObserve(Observer f) : fn(f), next(g_observers) { g_observers = this; } + void RunSlotWithValues(void *L, const uint32_t *slot, void *frame, int firstArgIdx, int nargs) { if (slot == nullptr || slot[0] == 0) return; diff --git a/src/frame/ScriptArgs.h b/src/frame/ScriptArgs.h index 15267a79..a94f4034 100644 --- a/src/frame/ScriptArgs.h +++ b/src/frame/ScriptArgs.h @@ -33,4 +33,18 @@ void RunWithValues(void *L, int handlerRef, void *frame, int firstArgIdx, int na // scripts it runs. Does nothing for an empty slot. void RunSlotWithValues(void *L, const uint32_t *slot, void *frame, int firstArgIdx, int nargs); +// Every frame-script handler run passes through this module — the engine's +// two runners (FUN_FRAME_INVOKE_SCRIPT, FUN_FRAME_RUN_SCRIPT_ARGS, co-hooked +// here) and RunWithValues — so it plays 3.3.5's FrameScript_Execute, the one +// function that runs a script. An observer is called with `begin` true +// before the handler runs and false after, nested runs nesting. Declare a +// file-scope `static const Frame::ScriptArgs::AutoObserve`. +using Observer = void (*)(bool begin, int handlerRef, void *frame); + +struct AutoObserve { + explicit AutoObserve(Observer fn); + Observer fn; + AutoObserve *next; +}; + } // namespace Frame::ScriptArgs diff --git a/src/frame/ScriptSlots.cpp b/src/frame/ScriptSlots.cpp new file mode 100644 index 00000000..134605e4 --- /dev/null +++ b/src/frame/ScriptSlots.cpp @@ -0,0 +1,64 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +#include "frame/ScriptSlots.h" + +#include "Game.h" +#include "Offsets.h" + +namespace Frame::ScriptSlots { + +namespace { + +AutoRegister *g_head = nullptr; + +// The base object's (0x00702590), the frame's (FUN_FRAME_SCRIPT_RESOLVER), +// then each type's additions; GameTooltip's last. A name the frame's type +// does not have resolves to 0. +constexpr const char *kEngineNames[] = { + "OnEvent", + "OnLoad", "OnSizeChanged", "OnUpdate", "OnShow", "OnHide", "OnEnter", "OnLeave", + "OnMouseDown", "OnMouseUp", "OnMouseWheel", "OnDragStart", "OnDragStop", + "OnReceiveDrag", "OnChar", "OnKeyDown", "OnKeyUp", + "OnClick", "OnDoubleClick", + "OnEnterPressed", "OnEscapePressed", "OnSpacePressed", "OnTabPressed", "OnTextChanged", + "OnTextSet", "OnCursorChanged", "OnInputLanguageChanged", "OnEditFocusGained", + "OnEditFocusLost", + "OnUpdateModel", "OnAnimFinished", + "OnHorizontalScroll", "OnVerticalScroll", "OnScrollRangeChanged", + "OnHyperlinkEnter", "OnHyperlinkLeave", "OnHyperlinkClick", + "OnMessageScrollChanged", + "OnValueChanged", + "OnColorSelect", + "OnMovieFinished", "OnMovieShowSubtitle", "OnMovieHideSubtitle", + "OnTooltipAddMoney", "OnTooltipCleared", "OnTooltipSetDefaultAnchor", +}; + +// `__thiscall(frame, name) -> slot`, ret 4, modelled with a dummy edx. +using Resolver_t = const uint32_t *(__fastcall *)(void *frame, void *edx, const char *name); + +} // namespace + +AutoRegister::AutoRegister(Enumerator f) : fn(f), next(g_head) { g_head = this; } + +void ForEach(void *frame, Visit visit, void *ctx) { + const void *vtable = Game::Read(frame, 0); + const auto resolve = Game::Read(vtable, Offsets::VTBL_FRAME_SCRIPT_RESOLVER); + for (const char *name : kEngineNames) + if (const uint32_t *slot = resolve(frame, nullptr, name)) + visit(slot, ctx); + for (AutoRegister *r = g_head; r != nullptr; r = r->next) + r->fn(frame, visit, ctx); +} + +} // namespace Frame::ScriptSlots diff --git a/src/frame/ScriptSlots.h b/src/frame/ScriptSlots.h new file mode 100644 index 00000000..f1c58624 --- /dev/null +++ b/src/frame/ScriptSlots.h @@ -0,0 +1,45 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +#pragma once + +#include + +// Every script slot a frame holds — 4.3.4's per-class GetScriptTime walks +// its own slot members; 1.12 exposes a frame's slots only by name, through +// the script-name resolver at vtable slot 0xC (FUN_FRAME_SCRIPT_RESOLVER and +// each type's extension of it). `ForEach` asks it for every name the client's +// frame types define (benilla-re ui/xmltypes "Handler names by type", plus +// the base object's OnEvent and GameTooltip's three). +// +// Scripts ClassicAPI adds keep their slots outside the frame, and their +// resolver co-hooks CREATE a slot when asked for a name — right for +// SetScript, wrong for a read that visits every frame. So each such store +// registers an enumerator over the slots it already holds: +// static const Frame::ScriptSlots::AutoRegister _slots{&ForEachSlot}; + +namespace Frame::ScriptSlots { + +// `slot` is the {handler ref, exec context} pair; the handler may be 0. +using Visit = void (*)(const uint32_t *slot, void *ctx); +using Enumerator = void (*)(const void *frame, Visit visit, void *ctx); + +struct AutoRegister { + explicit AutoRegister(Enumerator fn); + Enumerator fn; + AutoRegister *next; +}; + +void ForEach(void *frame, Visit visit, void *ctx); + +} // namespace Frame::ScriptSlots diff --git a/src/gc/Hooks.cpp b/src/gc/Hooks.cpp new file mode 100644 index 00000000..b017680e --- /dev/null +++ b/src/gc/Hooks.cpp @@ -0,0 +1,92 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +#include "gc/Hooks.h" + +#include "Game.h" +#include "Offsets.h" + +#include +#include + +namespace Gc::Hooks { + +namespace { + +// Chained at static-init time by each `AutoSubscribe` constructor, before +// `DllMain`; zero-initialized ahead of any dynamic init. +AutoSubscribe *g_head = nullptr; + +void NotifyCreate(const void *object) { + for (AutoSubscribe *s = g_head; s != nullptr; s = s->next) + if (s->onCreate != nullptr) + s->onCreate(object); +} + +using Link_t = void(__fastcall *)(void *L, void *object, uint8_t tt); +Link_t g_link = nullptr; + +void __fastcall Link_h(void *L, void *object, uint8_t tt) { + g_link(L, object, tt); + NotifyCreate(object); +} + +using NewLStr_t = void *(__fastcall *)(void *L, const char *str, size_t len, uint32_t hash); +NewLStr_t g_newLStr = nullptr; + +void *__fastcall NewLStr_h(void *L, const char *str, size_t len, uint32_t hash) { + void *s = g_newLStr(L, str, len, hash); + NotifyCreate(s); + return s; +} + +using NewUdata_t = void *(__fastcall *)(void *L, size_t size); +NewUdata_t g_newUdata = nullptr; + +void *__fastcall NewUdata_h(void *L, size_t size) { + void *u = g_newUdata(L, size); + NotifyCreate(u); + return u; +} + +using FreeObj_t = void(__fastcall *)(void *L, void *object); +FreeObj_t g_freeObj = nullptr; + +void __fastcall FreeObj_h(void *L, void *object) { + for (AutoSubscribe *s = g_head; s != nullptr; s = s->next) + if (s->onFree != nullptr) + s->onFree(object); + g_freeObj(L, object); +} + +const Game::HookAutoRegister _linkHook{ + Offsets::FUN_LUA_C_LINK, reinterpret_cast(&Link_h), + reinterpret_cast(&g_link)}; +const Game::HookAutoRegister _newLStrHook{ + Offsets::FUN_LUA_S_NEWLSTR, reinterpret_cast(&NewLStr_h), + reinterpret_cast(&g_newLStr)}; +const Game::HookAutoRegister _newUdataHook{ + Offsets::FUN_LUA_S_NEWUDATA, reinterpret_cast(&NewUdata_h), + reinterpret_cast(&g_newUdata)}; +const Game::HookAutoRegister _freeObjHook{ + Offsets::FUN_LUA_C_FREEOBJ, reinterpret_cast(&FreeObj_h), + reinterpret_cast(&g_freeObj)}; + +} // namespace + +AutoSubscribe::AutoSubscribe(Callback create, Callback free) + : onCreate(create), onFree(free), next(g_head) { + g_head = this; +} + +} // namespace Gc::Hooks diff --git a/src/gc/Hooks.h b/src/gc/Hooks.h new file mode 100644 index 00000000..631eea0b --- /dev/null +++ b/src/gc/Hooks.h @@ -0,0 +1,47 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +#pragma once + +// Shared interceptor for the Lua collector's object lifecycle. Every +// collectable object is born in one of three engine functions and dies in +// one (see `FUN_LUA_C_LINK` in Offsets.h for the xref proof): +// +// luaC_link tables, closures, protos, closed upvalues, threads +// newlstr strings +// luaS_newudata full userdata +// freeobj every free, from the sweep +// +// MinHook allows one hook per address, and both per-addon memory accounting +// (`Gc::Tag`) and script profiling need these, so — like `Event::SignalHook` — +// this module owns the four hooks and fans out. Declare a file-scope +// `static const Gc::Hooks::AutoSubscribe` in your module; either callback may +// be null. +// +// `onCreate` runs after the engine has linked the object (its type tag is +// set); `onFree` runs before the engine frees it (it is still readable). +// Both sit on the allocator's hottest path: keep them O(1) and allocation-free +// in the common case. + +namespace Gc::Hooks { + +using Callback = void (*)(const void *object); + +struct AutoSubscribe { + AutoSubscribe(Callback onCreate, Callback onFree); + Callback onCreate; + Callback onFree; + AutoSubscribe *next; +}; + +} // namespace Gc::Hooks diff --git a/src/gc/Tag.cpp b/src/gc/Tag.cpp new file mode 100644 index 00000000..e39cfb64 --- /dev/null +++ b/src/gc/Tag.cpp @@ -0,0 +1,118 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +#include "gc/Tag.h" + +#include "Game.h" +#include "Offsets.h" +#include "gc/Hooks.h" + +#include + +#include + +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. +ankerl::unordered_dense::map g_tags; + +// `GCUsage` list: tag → bytes at the last UpdateUsage. Like 3.3.5's list, a +// tag's entry survives once seen and is zeroed, not dropped, on a recount. +ankerl::unordered_dense::map g_usage; + +// `lua_gctag`, innermost last. +std::vector g_pins; + +void OnCreate(const void *object) { + const char *ctx = Game::Read(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT); + if (ctx == nullptr) + return; + const char *pin = g_pins.empty() ? nullptr : g_pins.back(); + g_tags[object] = pin != nullptr ? pin : ctx; +} + +void OnFree(const void *object) { g_tags.erase(object); } + +const Gc::Hooks::AutoSubscribe _gc{&OnCreate, &OnFree}; + +uint32_t U8(const void *o, uintptr_t off) { return Game::Read(o, off); } +uint32_t U16(const void *o, uintptr_t off) { return Game::Read(o, off); } +uint32_t U32(const void *o, uintptr_t off) { return Game::Read(o, off); } + +} // namespace + +const char *Of(const void *object) { + const auto it = g_tags.find(object); + return it == g_tags.end() ? nullptr : it->second; +} + +uint32_t SizeOf(const void *o) { + constexpr uint32_t kTValue = Offsets::SIZEOF_LUA_TVALUE; + switch (U8(o, Offsets::OFF_LUA_GCHEADER_TT)) { + case Game::Lua::TYPE_STRING: + return U32(o, Offsets::OFF_LUA_TSTRING_LEN) + Offsets::SIZEOF_LUA_TSTRING_HEADER; + case Game::Lua::TYPE_TABLE: { + const uint32_t lsizenode = U8(o, Offsets::OFF_LUA_TABLE_LSIZENODE); + return Offsets::SIZEOF_LUA_TABLE + kTValue * U32(o, Offsets::OFF_LUA_TABLE_SIZEARRAY) + + (lsizenode != 0 ? uint32_t{Offsets::SIZEOF_LUA_NODE} << lsizenode : 0); + } + case Game::Lua::TYPE_FUNCTION: { + const uint32_t nup = U8(o, Offsets::OFF_LUA_CLOSURE_NUPVALUES); + return U8(o, Offsets::OFF_LUA_CLOSURE_ISC) != 0 + ? (nup + 1) * kTValue + : nup * 4 + Offsets::SIZEOF_LUA_LCLOSURE_BASE; + } + case Game::Lua::TYPE_USERDATA: + return U32(o, Offsets::OFF_LUA_UDATA_LEN) + Offsets::SIZEOF_LUA_UDATA_HEADER; + case Game::Lua::TYPE_THREAD: + return Offsets::SIZEOF_LUA_STATE + kTValue * U32(o, Offsets::OFF_LUA_STATE_STACKSIZE) + + Offsets::SIZEOF_LUA_CALLINFO * U16(o, Offsets::OFF_LUA_STATE_SIZE_CI); + case Offsets::LUA_TT_PROTO: + return Offsets::SIZEOF_LUA_PROTO + 4 * U32(o, Offsets::OFF_LUA_PROTO_SIZECODE) + + 4 * U32(o, Offsets::OFF_LUA_PROTO_SIZEP) + + kTValue * U32(o, Offsets::OFF_LUA_PROTO_SIZEK) + + 4 * U32(o, Offsets::OFF_LUA_PROTO_SIZELINEINFO) + + Offsets::SIZEOF_LUA_LOCVAR * U32(o, Offsets::OFF_LUA_PROTO_SIZELOCVARS) + + 4 * U32(o, Offsets::OFF_LUA_PROTO_SIZEUPVALUES); + case Offsets::LUA_TT_UPVAL: + return Offsets::SIZEOF_LUA_UPVAL; + default: + return 0; + } +} + +size_t TaggedCount() { return g_tags.size(); } + +void PushPin(const char *tag) { g_pins.push_back(tag); } + +void PopPin() { + if (!g_pins.empty()) + g_pins.pop_back(); +} + +void UpdateUsage() { + for (auto &entry : g_usage) + entry.second = 0; + for (const auto &[object, tag] : g_tags) + g_usage[tag] += SizeOf(object); +} + +uint64_t Usage(const char *tag) { + const auto it = g_usage.find(tag); + return it == g_usage.end() ? 0 : it->second; +} + +} // namespace Gc::Tag diff --git a/src/gc/Tag.h b/src/gc/Tag.h new file mode 100644 index 00000000..6a1dae91 --- /dev/null +++ b/src/gc/Tag.h @@ -0,0 +1,64 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +#pragma once + +#include +#include + +// Per-object owner tags — 3.3.5's `GCheader::taint` + `lua_gctag`, the data +// behind `GetAddOnMemoryUsage` (and later `GetAddOnCPUUsage`). +// +// 3.3.5 stamps every new collectable object with +// tag = (lua_tainted && lua_gctag) ? lua_gctag : lua_tainted +// in luaC_link / newlstr / luaS_newudata. 1.12 already maintains +// `lua_tainted` — the context word `VAR_FRAMESCRIPT_EXEC_CONTEXT`, which the +// 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 +// through `Gc::Hooks`. Tags are compared by pointer and never dereferenced. +// +// `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). + +namespace Gc::Tag { + +// The tag of a live object, or nullptr. +const char *Of(const void *object); + +// Bytes the object occupies, by the sizes the engine's own free functions +// pass to the allocator. +uint32_t SizeOf(const void *object); + +// Number of live tagged objects. +size_t TaggedCount(); + +// `lua_gctag` for the scope's lifetime; nests. +void PushPin(const char *tag); +void PopPin(); + +struct PinScope { + explicit PinScope(const char *tag) { PushPin(tag); } + ~PinScope() { PopPin(); } + PinScope(const PinScope &) = delete; + PinScope &operator=(const PinScope &) = delete; +}; + +// `luaC_sizeall`: recount every tag's bytes from the live objects. +void UpdateUsage(); + +// `luaC_getsize`: a tag's bytes as of the last `UpdateUsage`; 0 if unseen. +uint64_t Usage(const char *tag); + +} // namespace Gc::Tag diff --git a/src/profile/Cpu.cpp b/src/profile/Cpu.cpp new file mode 100644 index 00000000..0b6c57dd --- /dev/null +++ b/src/profile/Cpu.cpp @@ -0,0 +1,412 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +// Script profiling: the `scriptProfile` cvar, `GetFunctionCPUUsage`, +// `UpdateAddOnCPUUsage`, `GetAddOnCPUUsage` and `ResetCPUUsage`. +// +// 3.3.5 gives every closure a `Profile {calls, depth, start, time, +// totaltime}` when the state is created with profiling on, and keeps it from +// a second interpreter (`luaV_execute_profile` + `luaD_precall_profile`) +// that reads the clock at each call and return. 1.12 has no profile field +// and one interpreter, but Lua 5.0 calls its debug hook at exactly those +// points — CALL for every C and Lua call (tail calls included), RET at every +// return — and nothing else uses the hook. So with `scriptProfile` 1 the hook +// is set for the session and applies 3.3.5's arithmetic to records kept here, +// per closure, dropped when the closure is collected (`Gc::Hooks`): +// +// a Lua function's `time` is its own running time: each call it makes ends +// a segment (`time += now - last`) and its return ends the last one; +// `depth` counts the calls it has outstanding — when it goes 0 → 1 the +// span starts (`start`), and when the call returns to depth 0 the span is +// added to `totaltime`, which also takes the segments run at depth 0 — so +// `totaltime` is the inclusive time, recursion counted once; +// a C function's `time` and `totaltime` both take its whole call; +// `calls` counts every call, a tail call included. +// +// The hook keeps a shadow of each thread's call stack (closure, call record +// index, tail-call count) to know the caller of each event. Two things leave +// that stack without a RET event, and both are recognized from the record +// index of the next event: a tail call (OP_TAILCALL replaces the record and +// raises its tail-call count), where the replaced function ends its segment as +// 3.3.5's OP_TAILCALL does; and an error unwinding frames, where each removed +// frame that had a call outstanding loses that depth without any time, as +// 3.3.5's luaD_pcall error path does. +// +// The hook is installed only with `scriptProfile` 1, read as each state starts +// (3.3.5 creates the state with it): with profiling off nothing runs. + +#include "profile/Cpu.h" + +#include "Game.h" +#include "Offsets.h" +#include "addons/Registry.h" +#include "cvar/Factory.h" +#include "gc/Hooks.h" +#include "gc/Tag.h" +#include "profile/ScriptTime.h" + +#include +#include + +#include +#include + +namespace Profile::Cpu { + +namespace { + +using SetHook_t = int(__fastcall *)(void *L, void *func, int mask, int count); + +struct Record { + uint32_t calls = 0; + int depth = 0; + int64_t start = 0; + int64_t time = 0; + int64_t totaltime = 0; +}; + +struct Frame { + const void *closure; + int ci; + uint32_t tailcalls; + bool isC; + bool done; // a C frame whose time was settled at a yield + int64_t last; + int64_t callStart; +}; + +using Stack = std::vector; + +bool g_enabled = false; +bool g_hooked = false; // the hook was set on the current main state +double g_msPerTick = 0.0; +CVar::Factory::Handle g_cvar = nullptr; + +ankerl::unordered_dense::map g_records; +ankerl::unordered_dense::map> g_stacks; +ankerl::unordered_dense::map g_addonTime; + +// The last thread an event came from, so a run of events on one thread skips +// the map lookup. Reset whenever g_stacks changes shape. +const void *g_lastThread = nullptr; +Stack *g_lastStack = nullptr; + +Stack &StackOf(const void *L) { + if (L == g_lastThread) + return *g_lastStack; + auto &slot = g_stacks[L]; + if (!slot) + slot = std::make_unique(); + g_lastThread = L; + g_lastStack = slot.get(); + return *slot; +} + +Record &RecordOf(const void *closure) { return g_records[closure]; } + +// Removes frames at or above record `ci` that left without a return: an +// error unwound them. The topmost had no call outstanding; each below it had +// one, which is abandoned. +void Unwind(Stack &s, int ci) { + bool topmost = true; + while (!s.empty() && s.back().ci >= ci) { + const Frame f = s.back(); + s.pop_back(); + if (!topmost && !f.isC) + RecordOf(f.closure).depth--; + topmost = false; + } +} + +void OnCall(void *L, Stack &s, int ci, int64_t now) { + const uint8_t *info = Game::Read(L, Offsets::OFF_LUA_STATE_BASE_CI) + + ci * Offsets::SIZEOF_LUA_CALLINFO; + const uint8_t *func = Game::Read(info, Offsets::OFF_LUA_CALLINFO_BASE) - + Offsets::SIZEOF_LUA_TVALUE; + const void *closure = Game::Read(func, Offsets::OFF_LUA_TVALUE_VALUE); + const bool isC = Game::Read(closure, Offsets::OFF_LUA_CLOSURE_ISC) != 0; + const uint32_t tailcalls = + isC ? 0 : Game::Read(info, Offsets::OFF_LUA_CALLINFO_TAILCALLS); + + bool tailcall = false; + if (!isC && !s.empty() && s.back().ci == ci && !s.back().isC && + tailcalls == s.back().tailcalls + 1) { + // OP_TAILCALL: the replaced function's last segment. + const Frame f = s.back(); + s.pop_back(); + Record &r = RecordOf(f.closure); + r.time += now - f.last; + if (r.depth == 0) + r.totaltime += now - f.last; + tailcall = true; + } + Unwind(s, ci); + if (!tailcall && !s.empty() && !s.back().isC) { + // OP_CALL: the caller's segment ends and its call is outstanding. + Frame &caller = s.back(); + Record &r = RecordOf(caller.closure); + r.time += now - caller.last; + if (++r.depth == 1) { + r.start = now; + r.totaltime += now - caller.last; + } + caller.last = now; + } + RecordOf(closure).calls++; + s.push_back({closure, ci, tailcalls, isC, false, now, now}); +} + +void OnReturn(Stack &s, int ci, int64_t now) { + Unwind(s, ci + 1); + if (s.empty() || s.back().ci != ci) + return; // the call began before profiling did + const Frame f = s.back(); + s.pop_back(); + Record &r = RecordOf(f.closure); + if (f.isC) { + if (!f.done) { + r.time += now - f.callStart; + r.totaltime += now - f.callStart; + } + } else { + r.time += now - f.last; + if (r.depth == 0) + r.totaltime += now - f.last; + } + if (!s.empty() && !s.back().isC) { + Frame &caller = s.back(); + Record &rc = RecordOf(caller.closure); + if (rc.depth-- == 1) + rc.totaltime += now - rc.start; + caller.last = now; + } +} + +void __fastcall Hook(void *L, void *ar) { + const int event = Game::Read(ar, Offsets::OFF_LUA_DEBUG_EVENT); + if (event != Offsets::LUA_HOOK_CALL && event != Offsets::LUA_HOOK_RET) + return; + const int64_t now = Now(); + const int ci = Game::Read(ar, Offsets::OFF_LUA_DEBUG_I_CI); + Stack &s = StackOf(L); + if (event == Offsets::LUA_HOOK_CALL) + OnCall(L, s, ci, now); + else + OnReturn(s, ci, now); +} + +void SetHook(void *L, bool on) { + reinterpret_cast(Offsets::LUA_SET_HOOK)( + L, on ? reinterpret_cast(&Hook) : nullptr, + on ? Offsets::LUA_MASK_CALL | Offsets::LUA_MASK_RET : 0, 0); +} + +void OnFree(const void *object) { + if (g_records.empty() && g_stacks.empty()) + return; + switch (Game::Read(object, Offsets::OFF_LUA_GCHEADER_TT)) { + case Game::Lua::TYPE_FUNCTION: + g_records.erase(object); + break; + case Game::Lua::TYPE_THREAD: + if (g_stacks.erase(object) != 0) { + g_lastThread = nullptr; + g_lastStack = nullptr; + } + break; + default: + break; + } +} + +const Gc::Hooks::AutoSubscribe _gc{nullptr, &OnFree}; + +// --- Lua API ---------------------------------------------------------------- + +int __fastcall Script_GetFunctionCPUUsage(void *L) { + if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_FUNCTION) { + Game::Lua::Error(L, "Usage: GetFunctionCPUUsage(function[, includeSubroutines])"); + return 0; + } + const bool includeSubroutines = Game::Lua::ToFlag(L, 2, true); + int64_t time = 0; + int64_t totaltime = 0; + uint32_t calls = 0; + FunctionTime(Game::Lua::ToPointer(L, 1), &time, &totaltime, &calls); + Game::Lua::PushNumber(L, Ms(includeSubroutines ? totaltime : time)); + Game::Lua::PushNumber(L, static_cast(calls)); + return 2; +} + +// 3.3.5's luaC_timeall: each closure's own time, summed by the closure's tag. +int __fastcall Script_UpdateAddOnCPUUsage(void * /*L*/) { + if (!g_enabled) + return 0; + for (auto &entry : g_addonTime) + entry.second = 0; + for (const auto &[closure, record] : g_records) + if (const char *tag = Gc::Tag::Of(closure)) + g_addonTime[tag] += record.time; + return 0; +} + +int __fastcall Script_GetAddOnCPUUsage(void *L) { + const uint8_t *record = + Addons::ResolveRecord(L, "Usage: GetAddOnCPUUsage(index or \"name\")"); + double ms = 0.0; + if (record != nullptr) { + const auto it = g_addonTime.find( + Game::Read(record, Offsets::OFF_ADDON_ENTRY_NAME_PTR)); + if (it != g_addonTime.end()) + ms = Ms(it->second); + } + Game::Lua::PushNumber(L, ms); + return 1; +} + +// 3.3.5's FrameScript_ResetCPUUsage: the script and event totals, then +// luaC_timereset — calls and times, not the live depth/start. +int __fastcall Script_ResetCPUUsage(void * /*L*/) { + ScriptTime::Reset(); + if (!g_enabled) + return 0; + for (auto &entry : g_addonTime) + entry.second = 0; + for (auto &entry : g_records) { + entry.second.calls = 0; + entry.second.time = 0; + entry.second.totaltime = 0; + } + return 0; +} + +const Game::Doc::Field kFunctionArgs[] = { + Game::Doc::Req("func", "function"), + Game::Doc::Opt("includeSubroutines", "bool", "true", + "Include the time spent in the functions it calls."), +}; +const Game::Doc::Field kFunctionRets[] = { + Game::Doc::Req("usage", "number", "Milliseconds spent in the function."), + Game::Doc::Req("calls", "number", "Times the function was called."), +}; +const Game::Doc::Function kGetFunctionCPUUsage{ + "Returns the time spent in a function and how often it was called, while script " + "profiling is on.", + kFunctionArgs, kFunctionRets, "ProfilingGlobals"}; + +const Game::Doc::Function kUpdateAddOnCPUUsage{ + "Recounts the time each addon's functions have run; GetAddOnCPUUsage returns the " + "result of the last recount.", + {}, {}, "ProfilingGlobals"}; + +const Game::Doc::Field kAddOnArgs[] = { + Game::Doc::Req("name", "uiAddon", "An addon index or folder name."), +}; +const Game::Doc::Field kAddOnRets[] = { + Game::Doc::Req("usage", "number", "Milliseconds the addon's functions have run."), +}; +const Game::Doc::Function kGetAddOnCPUUsage{ + "Returns the time an addon's functions have run, as of the last UpdateAddOnCPUUsage, " + "while script profiling is on.", + kAddOnArgs, kAddOnRets, "ProfilingGlobals"}; + +const Game::Doc::Function kResetCPUUsage{ + "Sets every script profiling count back to zero.", {}, {}, "ProfilingGlobals"}; + +} // namespace + +bool Enabled() { return g_enabled; } + +double Ms(int64_t ticks) { return static_cast(ticks) * g_msPerTick; } + +int64_t Now() { + LARGE_INTEGER t; + QueryPerformanceCounter(&t); + return t.QuadPart; +} + +void FunctionTime(const void *closure, int64_t *time, int64_t *totaltime, uint32_t *calls) { + *time = 0; + *totaltime = 0; + *calls = 0; + if (!g_enabled) + return; + const auto it = g_records.find(closure); + if (it == g_records.end()) + return; + *time = it->second.time; + *totaltime = it->second.totaltime; + *calls = it->second.calls; +} + +void OnThreadCreated(void *thread) { + if (g_enabled) + SetHook(thread, true); +} + +void OnResume(void *thread) { + if (!g_enabled) + return; + const int64_t now = Now(); + for (Frame &f : StackOf(thread)) + f.last = now; +} + +void OnYield(void *thread) { + if (!g_enabled) + return; + Stack &s = StackOf(thread); + if (s.empty() || !s.back().isC || s.back().done) + return; + // The yield itself: its call ends here, not when the thread resumes. + Frame &f = s.back(); + Record &r = RecordOf(f.closure); + const int64_t elapsed = Now() - f.callStart; + r.time += elapsed; + r.totaltime += elapsed; + f.done = true; +} + +static void RegisterLuaFunctions() { + g_cvar = CVar::Factory::Register("scriptProfile", "Whether or not script profiling is enabled", + Offsets::CVAR_CATEGORY_DEBUG, "0"); + + // A new state starts with nothing recorded, as 3.3.5's does. + void *L = Game::Lua::State(); + if (g_hooked && L != nullptr) + SetHook(L, false); + g_records.clear(); + g_stacks.clear(); + g_addonTime.clear(); + g_lastThread = nullptr; + g_lastStack = nullptr; + LARGE_INTEGER freq; + QueryPerformanceFrequency(&freq); + g_msPerTick = 1000.0 / static_cast(freq.QuadPart); + g_enabled = CVar::Factory::GetInt(g_cvar) != 0; + g_hooked = g_enabled && L != nullptr; + if (g_hooked) + SetHook(L, true); + + Game::Lua::RegisterGlobalFunction("GetFunctionCPUUsage", &Script_GetFunctionCPUUsage, + &kGetFunctionCPUUsage); + Game::Lua::RegisterGlobalFunction("UpdateAddOnCPUUsage", &Script_UpdateAddOnCPUUsage, + &kUpdateAddOnCPUUsage); + Game::Lua::RegisterGlobalFunction("GetAddOnCPUUsage", &Script_GetAddOnCPUUsage, + &kGetAddOnCPUUsage); + Game::Lua::RegisterGlobalFunction("ResetCPUUsage", &Script_ResetCPUUsage, &kResetCPUUsage); +} + +static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions}; + +} // namespace Profile::Cpu diff --git a/src/profile/Cpu.h b/src/profile/Cpu.h new file mode 100644 index 00000000..7e1398e2 --- /dev/null +++ b/src/profile/Cpu.h @@ -0,0 +1,44 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +#pragma once + +#include + +// Script profiling — 3.3.5's per-closure `Profile` records, kept through +// Lua's own call/return hooks. See Cpu.cpp. + +namespace Profile::Cpu { + +// True for the session when `scriptProfile` was 1 as the Lua state started. +bool Enabled(); + +// Milliseconds in `ticks` of the profiling clock. +double Ms(int64_t ticks); + +// The profiling clock (QueryPerformanceCounter). +int64_t Now(); + +// 3.3.5's lua_gcgetfunctiontime: a closure's own and inclusive time (in +// clock ticks) and its call count; all 0 when profiling is off or the +// closure never ran. +void FunctionTime(const void *closure, int64_t *time, int64_t *totaltime, uint32_t *calls); + +// Coroutine bookkeeping, called by `coroutine.*`: a new thread needs the +// hook (threads do not inherit it), and a suspended thread's time is not +// charged to the functions waiting in it. +void OnThreadCreated(void *thread); +void OnResume(void *thread); +void OnYield(void *thread); + +} // namespace Profile::Cpu diff --git a/src/profile/FrameTime.cpp b/src/profile/FrameTime.cpp new file mode 100644 index 00000000..d5549c8f --- /dev/null +++ b/src/profile/FrameTime.cpp @@ -0,0 +1,104 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +// `GetFrameCPUUsage(frame[, includeChildren])` — 3.3.5's Script_GetFrameCPUUsage +// (0x00517890): each of the frame's scripts' inclusive time and calls +// (FrameScript_Object::GetScriptTime), then, unless told not to, its child +// frames' the same way (CSimpleFrame::GetScriptTime). Every script counts, +// OnUpdate included, as retail's does. + +#include "Game.h" +#include "Offsets.h" +#include "frame/ScriptSlots.h" +#include "profile/Cpu.h" + +#include + +namespace Profile::FrameTime { + +namespace { + +struct Totals { + void *L; + int64_t ticks; + uint32_t calls; +}; + +void AddSlot(const uint32_t *slot, void *ctx) { + if (slot[0] == 0) + return; + auto *t = static_cast(ctx); + Game::Lua::RawGetI(t->L, Game::Lua::REGISTRY_INDEX, static_cast(slot[0])); + int64_t time = 0; + int64_t totaltime = 0; + uint32_t calls = 0; + Cpu::FunctionTime(Game::Lua::ToPointer(t->L, -1), &time, &totaltime, &calls); + Game::Lua::SetTop(t->L, -2); + t->ticks += totaltime; + t->calls += calls; +} + +void AddFrame(void *frame, bool children, Totals *t) { + Frame::ScriptSlots::ForEach(frame, &AddSlot, t); + if (children) + Game::ForEachFrameListObject(frame, Offsets::OFF_FRAME_CHILD_LIST, + [&](void *child) { AddFrame(child, true, t); }); +} + +int __fastcall Script_GetFrameCPUUsage(void *L) { + if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) { + Game::Lua::Error(L, "Usage: GetFrameCPUUsage(frame[, includeChildren])"); + return 0; + } + Game::Lua::RawGetI(L, 1, 0); + void *frame = const_cast(Game::Lua::ToPointer(L, -1)); + Game::Lua::SetTop(L, -2); + if (frame == nullptr) { + Game::Lua::Error(L, "GetFrameCPUUsage(): Couldn't find 'this' in frame object"); + return 0; + } + if (!Game::Lua::IsA(frame, Offsets::VAR_FRAME_LUA_TYPE_ID)) { + Game::Lua::Error(L, "GetFrameCPUUsage(): Wrong object type, expected frame"); + return 0; + } + Totals t{L, 0, 0}; + if (Cpu::Enabled()) + AddFrame(frame, Game::Lua::ToFlag(L, 2, true), &t); + Game::Lua::PushNumber(L, Cpu::Ms(t.ticks)); + Game::Lua::PushNumber(L, static_cast(t.calls)); + return 2; +} + +const Game::Doc::Field kArgs[] = { + Game::Doc::Req("frame", "SimpleFrame"), + Game::Doc::Opt("includeChildren", "bool", "true", "Include the frame's child frames."), +}; +const Game::Doc::Field kRets[] = { + Game::Doc::Req("usage", "number", "Milliseconds the frame's scripts have run."), + Game::Doc::Req("calls", "number", "Times the frame's scripts were called."), +}; +const Game::Doc::Function kGetFrameCPUUsage{ + "Returns the time a frame's scripts have run and how often they were called, while " + "script profiling is on.", + kArgs, kRets, "ProfilingGlobals"}; + +} // namespace + +static void RegisterLuaFunctions() { + Game::Lua::RegisterGlobalFunction("GetFrameCPUUsage", &Script_GetFrameCPUUsage, + &kGetFrameCPUUsage); +} + +static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions}; + +} // namespace Profile::FrameTime diff --git a/src/profile/ScriptTime.cpp b/src/profile/ScriptTime.cpp new file mode 100644 index 00000000..5523de9d --- /dev/null +++ b/src/profile/ScriptTime.cpp @@ -0,0 +1,146 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +// `GetScriptCPUUsage()` and `GetEventCPUUsage([event])`. +// +// 3.3.5's FrameScript_Execute — the one function that runs a script — times +// each run while profiling is on and adds it to `s_scriptTimeUsed`, and, for +// an event dispatch, to that event's `m_timeUsed`; every signal raises the +// event's `m_signalCount`, profiling or not. A nested run is timed on its own +// as well as inside its parent, as there. `GetEventCPUUsage` sums every +// event's time and count whatever name it is given (3.3.5's +// FrameScript_GetEventCPUUsage never reads it, and retail answers the same +// for a name as for none), so totals are all that is kept. +// +// Frame::ScriptArgs observes every handler run. An event dispatch is a run +// of the frame's OnEvent slot — the dispatchers are the only callers of that +// slot. Signals are counted at each dispatcher: the engine's no-arg one +// through Event::SignalHook, its variadic FUN_FIRE_EVENT at its inner +// get-context call (only reached when the event has a listener), and +// Event::Custom's own. + +#include "profile/ScriptTime.h" + +#include "Common.h" +#include "Game.h" +#include "Offsets.h" +#include "event/SignalHook.h" +#include "frame/ScriptArgs.h" +#include "profile/Cpu.h" + +#include +#include + +namespace Profile::ScriptTime { + +namespace { + +int64_t g_scriptTime = 0; +int64_t g_eventTime = 0; +uint32_t g_signals = 0; + +// Start times of the runs in progress, innermost last. +std::vector g_starts; + +void OnRun(bool begin, int handlerRef, void *frame) { + if (!Cpu::Enabled()) + return; + if (begin) { + g_starts.push_back(Cpu::Now()); + return; + } + if (g_starts.empty()) + return; + const int64_t elapsed = Cpu::Now() - g_starts.back(); + g_starts.pop_back(); + g_scriptTime += elapsed; + if (frame != nullptr && + Game::Read(frame, Offsets::OFF_FRAME_ONEVENT_SLOT) == handlerRef) + g_eventTime += elapsed; +} + +const Frame::ScriptArgs::AutoObserve _run{&OnRun}; + +bool OnNoArgSignal(int /*eventID*/) { + CountSignal(); + return false; +} + +const Event::SignalHook::AutoSubscribe _noArgSignal{&OnNoArgSignal}; + +// Stands in for FUN_FIRE_EVENT's `call FUN_FRAMESCRIPT_GET_CONTEXT`. +void *GetContextCounted() { + CountSignal(); + return Game::Read(Offsets::VAR_LUA_STATE); +} + +int __fastcall Script_GetScriptCPUUsage(void *L) { + Game::Lua::PushNumber(L, Cpu::Ms(g_scriptTime)); + return 1; +} + +int __fastcall Script_GetEventCPUUsage(void *L) { + Game::Lua::PushNumber(L, Cpu::Ms(g_eventTime)); + Game::Lua::PushNumber(L, static_cast(g_signals)); + return 2; +} + +const Game::Doc::Field kScriptRets[] = { + Game::Doc::Req("usage", "number", "Milliseconds frame scripts have run."), +}; +const Game::Doc::Function kGetScriptCPUUsage{ + "Returns the time frame scripts have run, while script profiling is on.", + {}, kScriptRets, "ProfilingGlobals"}; + +const Game::Doc::Field kEventArgs[] = { + Game::Doc::Opt("event", "cstring", nullptr, "Ignored; the totals cover every event."), +}; +const Game::Doc::Field kEventRets[] = { + Game::Doc::Req("usage", "number", + "Milliseconds event handlers have run, while script profiling is on."), + Game::Doc::Req("numEvents", "number", "Events fired."), +}; +const Game::Doc::Function kGetEventCPUUsage{ + "Returns the time event handlers have run and how many events fired.", + kEventArgs, kEventRets, "ProfilingGlobals"}; + +void PatchCode() { + Common::RedirectCall(Offsets::CALL_FIRE_EVENT_GET_CONTEXT, + Offsets::FUN_FRAMESCRIPT_GET_CONTEXT, + reinterpret_cast(&GetContextCounted)); +} + +} // namespace + +void CountSignal() { ++g_signals; } + +void Reset() { + g_scriptTime = 0; + g_eventTime = 0; + g_signals = 0; +} + +static void RegisterLuaFunctions() { + // A new state starts from zero, as 3.3.5's FrameScript_Initialize does. + Reset(); + g_starts.clear(); + PatchCode(); + Game::Lua::RegisterGlobalFunction("GetScriptCPUUsage", &Script_GetScriptCPUUsage, + &kGetScriptCPUUsage); + Game::Lua::RegisterGlobalFunction("GetEventCPUUsage", &Script_GetEventCPUUsage, + &kGetEventCPUUsage); +} + +static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions}; + +} // namespace Profile::ScriptTime diff --git a/src/profile/ScriptTime.h b/src/profile/ScriptTime.h new file mode 100644 index 00000000..7b1ea10d --- /dev/null +++ b/src/profile/ScriptTime.h @@ -0,0 +1,28 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +#pragma once + +// Script and event CPU totals — 3.3.5's `s_scriptTimeUsed` and the event +// objects' time / signal counts. See ScriptTime.cpp. + +namespace Profile::ScriptTime { + +// One event signal. The engine's dispatchers are counted here already; a +// dispatcher of our own (Event::Custom::FireValues) calls this once per fire. +void CountSignal(); + +// ResetCPUUsage's script and event part. +void Reset(); + +} // namespace Profile::ScriptTime diff --git a/src/tooltip/SetEvents.cpp b/src/tooltip/SetEvents.cpp index 0c56feaa..85881af2 100644 --- a/src/tooltip/SetEvents.cpp +++ b/src/tooltip/SetEvents.cpp @@ -47,6 +47,7 @@ #include "Game.h" #include "Offsets.h" #include "equipmentset/Tooltip.h" +#include "frame/ScriptSlots.h" #include "unit/Tooltip.h" #include @@ -249,6 +250,14 @@ static const Game::HookAutoRegister _buildGOHook{ reinterpret_cast(&BuildGO_h), reinterpret_cast(&g_buildGOOriginal)}; +void ForEachSlot(const void *frame, Frame::ScriptSlots::Visit visit, void *ctx) { + for (int k = 0; k < SK_COUNT; ++k) + if (const uint32_t *slot = SlotFor(const_cast(frame), k, /*create*/ false)) + visit(slot, ctx); +} + +const Frame::ScriptSlots::AutoRegister _slots{&ForEachSlot}; + } // namespace void FireFrameStack(void *tooltip) { FireScript(tooltip, SK_FRAMESTACK); } diff --git a/src/xml/FrameLoad.cpp b/src/xml/FrameLoad.cpp index c7b38a25..ed92fc30 100644 --- a/src/xml/FrameLoad.cpp +++ b/src/xml/FrameLoad.cpp @@ -26,6 +26,7 @@ namespace { using GetAttr_t = const char *(__thiscall *)(const void *node, const char *name); using TemplateLookup_t = const void *(__fastcall *)(const char *name); using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n); +using Hash_t = uint32_t(__stdcall *)(const char *s); using StatusAdd_t = void(__cdecl *)(void *status, int level, const char *fmt, ...); } // namespace @@ -56,6 +57,28 @@ const void *Template(const char *name) { return reinterpret_cast(Offsets::FUN_XML_TEMPLATE_LOOKUP)(name); } +// FUN_XML_TEMPLATE_LOOKUP's own walk — hash, masked bucket, chain compare on +// hash then SStrCmpI — returning the node's context instead of its definition. +const char *TemplateContext(const char *name) { + const uint32_t mask = Game::Read(Offsets::VAR_XML_TEMPLATE_MASK); + const uint8_t *base = Game::Read(Offsets::VAR_XML_TEMPLATE_TABLE); + if (name == nullptr || mask == 0xFFFFFFFFu || base == nullptr) + return nullptr; + const uint32_t hash = reinterpret_cast(Offsets::FUN_SSTR_HASH_HT)(name); + const uint8_t *bucket = base + (hash & mask) * Offsets::XML_TEMPLATE_BUCKET_STRIDE; + const int linkOff = Game::Read(bucket, Offsets::OFF_XML_TEMPLATE_BUCKET_LINKOFF); + auto cmp = reinterpret_cast(Offsets::FUN_SSTR_CMP_I); + for (const uint8_t *node = Game::Read(bucket, Offsets::OFF_XML_TEMPLATE_BUCKET_HEAD); + node != nullptr && (reinterpret_cast(node) & 1) == 0; + node = Game::Read(node, linkOff + 4)) { + if (Game::Read(node, Offsets::OFF_XML_TEMPLATE_NODE_HASH) == hash && + cmp(Game::Read(node, Offsets::OFF_XML_TEMPLATE_NODE_NAME), name, + 0x7FFFFFFF) == 0) + return Game::Read(node, Offsets::OFF_XML_TEMPLATE_NODE_CONTEXT); + } + return nullptr; +} + void LoadNode(void *object, const void *node, void *status) { using NodeLoader_t = uintptr_t(__thiscall *)(void *self, const void *node, void *status); void *self = static_cast(object) + Offsets::OFF_XML_LOADER_SUBOBJECT; diff --git a/src/xml/FrameLoad.h b/src/xml/FrameLoad.h index 996d8b44..498572a8 100644 --- a/src/xml/FrameLoad.h +++ b/src/xml/FrameLoad.h @@ -41,6 +41,10 @@ const char *Attr(const void *node, const char *name); // The template definition node `inherits=` resolves `name` to // (FUN_XML_TEMPLATE_LOOKUP); nullptr when no template has that name. const void *Template(const char *name); +// The context word the template `name` registered under — its owning addon's +// name, or nullptr for the client's own templates and for unknown names. +// 3.3.5 applies a template under this as `lua_gctag`. +const char *TemplateContext(const char *name); // Loads `node` (with its own `inherits`) onto the frame `object` through the // object's node loader `(object+0x24)->vtable[+8]` — the call the frame // loader makes for an inherited node. diff --git a/src/xml/Inherits.cpp b/src/xml/Inherits.cpp index 10b56be9..09c64eba 100644 --- a/src/xml/Inherits.cpp +++ b/src/xml/Inherits.cpp @@ -53,11 +53,16 @@ // the whole string finds nothing) and keeps EDX. // // Textures and font strings keep one template, as in 4.3.4. +// +// Each listed template is applied under a `Gc::Tag` pin on its owner, as +// 3.3.5's LoadXML / LoadXMLInheritance set `lua_gctag` per template; +// `Xml::TemplateTag` pins the single-name engine path the same way. #include "xml/Inherits.h" #include "Game.h" #include "Offsets.h" +#include "gc/Tag.h" #include "xml/FrameLoad.h" #include @@ -118,8 +123,10 @@ void __fastcall ChildFrames_h(void *frame, void *edx, const void *node, void *st return; } ForEach(inherits, [&](const char *name) { - if (const void *t = Template(name)) + if (const void *t = Template(name)) { + Gc::Tag::PinScope pin(TemplateContext(name)); ChildFrames_h(frame, edx, t, status); + } }); const void *frames = reinterpret_cast(Offsets::FUN_XML_NODE_GET_CHILD)(node, "Frames"); if (frames == nullptr) @@ -196,9 +203,10 @@ uintptr_t LoadFrame(Loader original, void *self, void *edx, const void *node, vo return original(self, edx, node, status); void *object = static_cast(self) - Offsets::OFF_XML_LOADER_SUBOBJECT; ForEach(inherits, [&](const char *name) { - if (const void *t = Template(name)) + if (const void *t = Template(name)) { + Gc::Tag::PinScope pin(TemplateContext(name)); LoadNode(object, t, status); - else + } else Warn(status, reinterpret_cast(Offsets::STR_XML_NO_INHERITED_NODE), name); }); SuppressInherits suppress(node); diff --git a/src/xml/TemplateTag.cpp b/src/xml/TemplateTag.cpp new file mode 100644 index 00000000..05e2a664 --- /dev/null +++ b/src/xml/TemplateTag.cpp @@ -0,0 +1,108 @@ +// This file is part of ClassicAPI. +// +// ClassicAPI is free software: you can redistribute it and/or modify it under the terms +// of the GNU General Public License as published by the Free Software Foundation, either +// version 3 of the License, or (at your option) any later version. +// +// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// ClassicAPI. If not, see . + +// Template ownership for `Gc::Tag` — 3.3.5's `lua_gctag = taint` around each +// inherited template's application (CSimpleFrame::LoadXML and +// LoadXMLInheritance), so the objects a template's content makes (its compiled +// scripts, mostly) belong to the addon that defined the template, not the one +// instantiating it. A client template registers under context 0, which the +// tag rule reads as "no override". +// +// 1.12 applies a single-name `inherits` at two engine sites +// (CALL_XML_FRAME_LOAD_TEMPLATE_LOOKUP / SITE_XML_FRAME_LOAD_TEMPLATE_APPLY and +// CALL_XML_CHILD_FRAMES_TEMPLATE_LOOKUP / _RECURSE): each looks the name up, +// then applies the node. The lookup calls are redirected to record the +// template's context, the applications are bracketed with the pin. Lists go +// through `Xml::Inherits`, which pins them itself. + +#include "Common.h" +#include "Game.h" +#include "Offsets.h" +#include "gc/Tag.h" +#include "xml/FrameLoad.h" + +#include + +namespace Xml::TemplateTag { + +namespace { + +using ChildFrames_t = void(__fastcall *)(void *frame, void *edx, const void *node, void *status); + +// The context of the template the last lookup found; consumed by the +// application that follows it (only a `test eax,eax; jz` lies between). +const char *g_pending = nullptr; + +const void *__fastcall LookupTemplate(const char *name) { + const void *node = Template(name); + g_pending = node != nullptr ? TemplateContext(name) : nullptr; + return node; +} + +void BeginPin() { + Gc::Tag::PushPin(g_pending); + g_pending = nullptr; +} + +void EndPin() { Gc::Tag::PopPin(); } + +// Stands in for the frame loader's 12-byte re-entry. EAX = the template node, +// EBX = the loader sub-object, EBP = the loader's frame ([ebp+0xC] = status). +__declspec(naked) void ApplyThunk() { + __asm { + push eax + call BeginPin + pop eax + mov ecx, [ebp + 0Ch] + mov edx, [ebx] + push ecx + push eax + mov ecx, ebx + call dword ptr [edx + 8] + call EndPin + ret + } +} + +// Replaces the child-frame builder's recursive call into itself (through the +// entry, so `Xml::Inherits` still sees it). +void __fastcall ChildFramesTemplate(void *frame, void *edx, const void *node, void *status) { + BeginPin(); + reinterpret_cast(Offsets::FUN_XML_FRAME_LOAD_CHILD_FRAMES)(frame, edx, node, + status); + EndPin(); +} + +void PatchCode() { + static const uint8_t kApply[12] = {0x8B, 0x4D, 0x0C, 0x8B, 0x13, 0x51, + 0x50, 0x8B, 0xCB, 0xFF, 0x52, 0x08}; + Common::RedirectCall(Offsets::CALL_XML_FRAME_LOAD_TEMPLATE_LOOKUP, + Offsets::FUN_XML_TEMPLATE_LOOKUP, + reinterpret_cast(&LookupTemplate)); + Common::ReplaceWithCall(Offsets::SITE_XML_FRAME_LOAD_TEMPLATE_APPLY, kApply, sizeof kApply, + reinterpret_cast(&ApplyThunk)); + Common::RedirectCall(Offsets::CALL_XML_CHILD_FRAMES_TEMPLATE_LOOKUP, + Offsets::FUN_XML_TEMPLATE_LOOKUP, + reinterpret_cast(&LookupTemplate)); + Common::RedirectCall(Offsets::CALL_XML_CHILD_FRAMES_TEMPLATE_RECURSE, + Offsets::FUN_XML_FRAME_LOAD_CHILD_FRAMES, + reinterpret_cast(&ChildFramesTemplate)); +} + +// Each pass runs before FrameXML and the addons load; the patches are +// idempotent. +const Game::ModuleAutoRegister _autoreg{&PatchCode}; + +} // namespace + +} // namespace Xml::TemplateTag