Files
Bagshui/Components/CpuProfile.lua
T
OEVGandClaude Opus 5.5 0c1b566cf6 /bs cpu: add clamp on/off test switch, drop frame CPU line
ClassicAPI's frame CPU numbers exceeded wall-clock time, so they are no
longer shown. `/bs cpu clamp on|off` toggles SetClampedToScreen on the
inventory windows for the session to test the engine-side drag stalls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwt5PZSpEDkXYzRuRhH
2026-11-01 02:53:35 +00:00

179 lines
5.8 KiB
Lua

-- Bagshui CPU Profile
-- Exposes: Bagshui.components.CpuProfile
--
-- `/bs cpu` reads ClassicAPI's script profiler and prints where CPU time went since the last
-- reset (or /reload): the busiest addons and Bagshui's busiest functions.
-- `/bs cpu reset` starts a new measurement.
-- `/bs cpu clamp on|off` switches the windows' keep-on-screen clamp for this session only,
-- to test whether it causes the engine-side stalls while dragging.
--
-- Needs ClassicAPI and the scriptProfile CVar (`/console scriptProfile 1`, then /reload).
-- Profiling slows every addon down a little, so turn it off again when done.
Bagshui:AddComponent(function()
local CpuProfile = {}
Bagshui.components.CpuProfile = CpuProfile
local TOP_ADDONS = 5
local TOP_FUNCTIONS = 8
--- Print one line to chat with a consistent prefix.
local function Out(text)
_G.DEFAULT_CHAT_FRAME:AddMessage("|cffdb4e97Bagshui CPU|r " .. text)
end
--- Format milliseconds for display.
local function Ms(ms)
if ms >= 1000 then
return string.format("%.2fs", ms / 1000)
end
return string.format("%dms", math.floor(ms + 0.5))
end
--- Is the profiler available and switched on?
---@return boolean
function CpuProfile:IsActive()
if type(_G.GetFunctionCPUUsage) ~= "function" or type(_G.UpdateAddOnCPUUsage) ~= "function" then
return false
end
local ok, value = pcall(_G.GetCVar, "scriptProfile")
return ok and tostring(value) == "1"
end
--- Sort helper: biggest `ms` first.
local function ByMs(a, b)
return a.ms > b.ms
end
--- CPU time per addon since the last reset.
---@return table list { name, ms }
local function AddOnUsage()
local list = {}
pcall(_G.UpdateAddOnCPUUsage)
for i = 1, _G.GetNumAddOns() do
local name = _G.GetAddOnInfo(i)
local ok, ms = pcall(_G.GetAddOnCPUUsage, i)
if ok and type(ms) == "number" and ms > 0 then
table.insert(list, { name = name, ms = ms })
end
end
table.sort(list, ByMs)
return list
end
--- CPU time of every function reachable from Bagshui's tables.
--- `total` includes the functions it calls; `self` is time spent in the function itself.
---@return table byTotal, table bySelf Lists of { name, ms }.
local function FunctionUsage()
local seen = {}
local byTotal, bySelf = {}, {}
local function Scan(prefix, tbl)
if type(tbl) ~= "table" then
return
end
for key, value in pairs(tbl) do
if type(value) == "function" and type(key) == "string" and not seen[value] then
seen[value] = true
local okTotal, total = pcall(_G.GetFunctionCPUUsage, value, true)
local okSelf, own = pcall(_G.GetFunctionCPUUsage, value, false)
local name = prefix .. key
if okTotal and type(total) == "number" and total > 0 then
table.insert(byTotal, { name = name, ms = total })
end
if okSelf and type(own) == "number" and own > 0 then
table.insert(bySelf, { name = name, ms = own })
end
end
end
end
-- Prototypes first, so shared methods are named after the class that defines them.
for name, prototype in pairs(Bagshui.prototypes) do
Scan(name .. ":", prototype)
end
for name, component in pairs(Bagshui.components) do
Scan(name .. ":", component)
end
Scan("Bagshui:", Bagshui)
Scan("BsUtil.", BsUtil)
table.sort(byTotal, ByMs)
table.sort(bySelf, ByMs)
return byTotal, bySelf
end
--- Print up to `count` entries of `list`, prefixed by `heading`.
local function PrintTop(heading, list, count)
if table.getn(list) == 0 then
return
end
local parts = {}
for i = 1, math.min(count, table.getn(list)) do
table.insert(parts, list[i].name .. " " .. Ms(list[i].ms))
end
Out("|cffffd100" .. heading .. ":|r " .. table.concat(parts, ", "))
end
--- Print the report.
function CpuProfile:Report()
if not self:IsActive() then
Out("profiling is off. Type |cffffffff/console scriptProfile 1|r and |cffffffff/reload|r (needs ClassicAPI).")
return
end
local since = self.resetAt and string.format("%.0fs", _G.GetTime() - self.resetAt) or "the last /reload"
Out("CPU time since " .. since .. " (/bs cpu reset to start over)")
PrintTop("Addons", AddOnUsage(), TOP_ADDONS)
local byTotal, bySelf = FunctionUsage()
PrintTop("Bagshui functions, own time", bySelf, TOP_FUNCTIONS)
PrintTop("Bagshui functions, including calls", byTotal, TOP_FUNCTIONS)
end
--- Start a new measurement.
function CpuProfile:Reset()
if not self:IsActive() then
self:Report()
return
end
pcall(_G.ResetCPUUsage)
self.resetAt = _G.GetTime()
Out("counters reset. Reproduce the hitch, then type /bs cpu.")
end
--- Switch the keep-on-screen clamp on all inventory windows (not saved; /reload restores it).
---@param enable boolean
function CpuProfile:SetClamp(enable)
local count = 0
for _, component in pairs(Bagshui.components) do
if
type(component) == "table"
and component.inventoryType
and type(component.uiFrame) == "table"
and component.uiFrame.SetClampedToScreen
then
component.uiFrame:SetClampedToScreen(enable)
count = count + 1
end
end
Out(string.format("keep-on-screen clamp %s for %d windows until /reload.", enable and "ON" or "OFF", count))
end
BsSlash:AddHandler("CPU", function(tokens)
local verb = tokens[2] and string.lower(tokens[2])
if verb == "reset" then
CpuProfile:Reset()
elseif verb == "clamp" then
local state = tokens[3] and string.lower(tokens[3])
if state == "on" or state == "off" then
CpuProfile:SetClamp(state == "on")
else
Out("usage: /bs cpu clamp on|off")
end
else
CpuProfile:Report()
end
end, true)
end)