Add addonperf stub module for TBC+ addon profiling API

Registers GetAddOnMemoryUsage, UpdateAddOnMemoryUsage, GetAddOnCPUUsage,
UpdateAddOnCPUUsage, ResetAddOnCPUUsage, and GetScriptCPUUsage as global
Lua functions. All stubs for now — implementation will hook lua_Alloc
and FrameScript dispatch for per-addon tracking.
This commit is contained in:
MarcelineVQ
2026-03-12 23:38:36 -07:00
parent 751063b4b0
commit c770e053aa
3 changed files with 129 additions and 0 deletions
+1
View File
@@ -29,6 +29,7 @@ const module_list = [_]ModuleDesc{
.{ .name = "clickthrough", .desc = "Enable GO click-through (enlarge GO model bounds)" },
.{ .name = "dpslog", .desc = "Enable structured combat log events for addons", .default = false },
.{ .name = "transform44", .desc = "Enable transformMatrix4x4 hook", .default = false },
.{ .name = "addonperf", .desc = "Enable addon memory/CPU profiling API" },
};
pub fn build(b: *std.Build) void {
+100
View File
@@ -0,0 +1,100 @@
//! addonperf — Expose addon memory and CPU usage to Lua.
//!
//! Vanilla WoW (1.12.1) lacks the TBC+ addon profiling API:
//! GetAddOnMemoryUsage(index|name) -> kB
//! UpdateAddOnMemoryUsage()
//! GetAddOnCPUUsage(index|name) -> ms
//! UpdateAddOnCPUUsage()
//! ResetAddOnCPUUsage(index|name)
//! GetScriptCPUUsage() -> enabled
//!
//! This module hooks the Lua engine to track per-addon memory and CPU,
//! then registers these functions so addons like !Swatter / Blizzard_DebugTools
//! can query performance data.
const std = @import("std");
const hook = @import("zhook");
const logging = @import("../logging.zig");
const mod_mutex = @import("../mutex.zig");
pub const module_name: [*:0]const u8 = "addonperf";
var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false;
var log: logging.Logger = .{};
pub fn isActive() bool {
return g_is_hook_owner;
}
// =============================================================================
// Lua API stubs
// =============================================================================
/// GetAddOnMemoryUsage(index|"name") -> kB (float)
/// Returns memory used by the specified addon in kilobytes.
pub fn luaGetAddOnMemoryUsage(L: *anyopaque) callconv(.c) u32 {
// TODO: track per-addon Lua memory via lua_gc / allocator hooks
_ = L;
return 0; // pushes nothing — returns 0 results
}
/// UpdateAddOnMemoryUsage()
/// Triggers a memory usage snapshot for all addons.
pub fn luaUpdateAddOnMemoryUsage(L: *anyopaque) callconv(.c) u32 {
_ = L;
return 0;
}
/// GetAddOnCPUUsage(index|"name") -> totalMs, recentMs
/// Returns total and recent CPU time for the specified addon.
pub fn luaGetAddOnCPUUsage(L: *anyopaque) callconv(.c) u32 {
// TODO: hook FrameScript dispatch to measure per-addon handler time
_ = L;
return 0;
}
/// UpdateAddOnCPUUsage()
/// Triggers a CPU usage snapshot for all addons.
pub fn luaUpdateAddOnCPUUsage(L: *anyopaque) callconv(.c) u32 {
_ = L;
return 0;
}
/// ResetAddOnCPUUsage(index|"name")
/// Resets CPU counters for the specified addon (or all if no arg).
pub fn luaResetAddOnCPUUsage(L: *anyopaque) callconv(.c) u32 {
_ = L;
return 0;
}
/// GetScriptCPUUsage() -> enabled (1/nil)
/// Returns whether CPU profiling is active.
pub fn luaGetScriptCPUUsage(L: *anyopaque) callconv(.c) u32 {
const L_ptr = @intFromPtr(L);
// Always enabled when this module is loaded
hook.call(fn (usize, i32) callconv(hook.cc.fastcall) void, 0x6F36B0, .{ L_ptr, @as(i32, 1) }); // lua_pushboolean
return 1;
}
// =============================================================================
// Install / remove
// =============================================================================
pub fn installHooks() void {
const result = mod_mutex.acquire(module_name);
g_mutex = result.handle;
g_is_hook_owner = result.is_owner;
if (!g_is_hook_owner) return;
log = logging.Logger.open(module_name, .both);
log.print("addonperf: stub module loaded\n");
}
pub fn removeHooks() void {
if (g_is_hook_owner) {
log.close();
mod_mutex.release(&g_mutex);
}
g_is_hook_owner = false;
}
+28
View File
@@ -19,6 +19,7 @@ const build_opts = struct {
const clickthrough = @import("build_options").enable_clickthrough;
const dpslog = @import("build_options").enable_dpslog;
const transform44 = @import("build_options").enable_transform44;
const addonperf = @import("build_options").enable_addonperf;
};
// Conditional module imports
@@ -36,9 +37,21 @@ const bigcursor = if (build_opts.bigcursor) @import("bigcursor/bigcursor.zig") e
const clickthrough = if (build_opts.clickthrough) @import("clickthrough/clickthrough.zig") else struct {};
const dpslog = if (build_opts.dpslog) @import("dpslog/dpslog.zig") else struct {};
const transform44 = if (build_opts.transform44) @import("transform44/transform44.zig") else struct {};
const addonperf = if (build_opts.addonperf) @import("addonperf/addonperf.zig") else struct {};
const WINAPI = std.builtin.CallingConvention.winapi;
const SYSTEMTIME = extern struct {
wYear: u16,
wMonth: u16,
wDayOfWeek: u16,
wDay: u16,
wHour: u16,
wMinute: u16,
wSecond: u16,
wMilliseconds: u16,
};
// =============================================================================
// Lua Protection Bypass
// =============================================================================
@@ -99,6 +112,14 @@ fn registerLuaFunctions() void {
if (build_opts.transform44) {
registerFunction("SetWeatherOverride", @intFromPtr(&transform44.luaSetWeatherOverride));
}
if (build_opts.addonperf) {
registerFunction("GetAddOnMemoryUsage", @intFromPtr(&addonperf.luaGetAddOnMemoryUsage));
registerFunction("UpdateAddOnMemoryUsage", @intFromPtr(&addonperf.luaUpdateAddOnMemoryUsage));
registerFunction("GetAddOnCPUUsage", @intFromPtr(&addonperf.luaGetAddOnCPUUsage));
registerFunction("UpdateAddOnCPUUsage", @intFromPtr(&addonperf.luaUpdateAddOnCPUUsage));
registerFunction("ResetAddOnCPUUsage", @intFromPtr(&addonperf.luaResetAddOnCPUUsage));
registerFunction("GetScriptCPUUsage", @intFromPtr(&addonperf.luaGetScriptCPUUsage));
}
if (build_opts.worldmarkers and markers.isActive()) {
// User-facing functions stay global
registerFunction("WorldMarker", @intFromPtr(&markers.luaWorldMarker));
@@ -190,6 +211,7 @@ const LoadModelFn = fn (u32, u32, u32) callconv(hook.cc.thiscall) u32;
var model_load_hook: hook.Detour(LoadModelFn) = .{};
// Windows API imports for async handling
extern "kernel32" fn GetLocalTime(lpSystemTime: *SYSTEMTIME) callconv(WINAPI) void;
extern "kernel32" fn EnterCriticalSection(lpCriticalSection: *anyopaque) callconv(WINAPI) void;
extern "kernel32" fn LeaveCriticalSection(lpCriticalSection: *anyopaque) callconv(WINAPI) void;
extern "kernel32" fn SetEvent(hEvent: *anyopaque) callconv(WINAPI) i32;
@@ -227,6 +249,11 @@ fn openFileDetour(
flags: u32,
handle_out: *u32,
) callconv(hook.cc.stdcall) u32 {
var st: SYSTEMTIME = undefined;
GetLocalTime(&st);
log.to(.file).fmt("{d}/{d} {d:0>2}:{d:0>2}:{d:0>2}.{d:0>3} {s}\n", .{
st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, std.mem.span(path),
});
if (findEmbeddedFile(path)) |entry| {
// Allocate and zero-fill 0x60-byte file context
const ctx = allocateGameBuffer(0x60) orelse {
@@ -619,6 +646,7 @@ const modules = [_]ModuleHooks{
if (build_opts.clickthrough) .{ .name = clickthrough.module_name, .install = clickthrough.installHooks, .remove = clickthrough.removeHooks, .is_active = clickthrough.isActive } else .{},
if (build_opts.dpslog) .{ .name = dpslog.module_name, .install = dpslog.installHooks, .remove = dpslog.removeHooks, .is_active = dpslog.isActive } else .{},
if (build_opts.transform44) .{ .name = transform44.module_name, .install = transform44.installHooks, .remove = transform44.removeHooks, .is_active = transform44.isActive } else .{},
if (build_opts.addonperf) .{ .name = addonperf.module_name, .install = addonperf.installHooks, .remove = addonperf.removeHooks, .is_active = addonperf.isActive } else .{},
if (build_opts.worldmarkers) .{ .name = markers.module_name, .install = markers.installHooks, .remove = markers.removeHooks, .is_active = markers.isActive } else .{},
if (build_opts.interact) .{ .name = interact.module_name, .install = interact.installHooks, .remove = interact.removeHooks, .is_active = interact.isActive } else .{},
if (build_opts.outline) .{ .name = outline.module_name, .remove = outline.cleanup, .is_active = outline.isActive } else .{},