Files
Bagshui/Components/CpuProfile.lua
T
OEVGandClaude Opus 5.5 0dc37a2038 Add /bs cpu profiling report
Reads ClassicAPI's script profiler (scriptProfile CVar) and prints CPU
time per addon, per Bagshui window and per Bagshui function since the
last reset, to find what causes the window-drag freezes.

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

178 lines
5.7 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, Bagshui's busiest frames, and Bagshui's busiest
-- functions. `/bs cpu reset` starts a new measurement.
--
-- 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_FRAMES = 4
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 Bagshui's own frames (scripts on the frame and its children).
---@return table list { name, ms }
local function FrameUsage()
local list = {}
if type(_G.GetFrameCPUUsage) ~= "function" then
return list
end
local function Add(name, frame)
if frame then
local ok, ms = pcall(_G.GetFrameCPUUsage, frame, true)
if ok and type(ms) == "number" and ms > 0 then
table.insert(list, { name = name, ms = ms })
end
end
end
Add("Event frame", Bagshui.eventFrame)
for name, component in pairs(Bagshui.components) do
if type(component) == "table" and type(component.uiFrame) == "table" then
Add(name .. " window", component.uiFrame)
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)
PrintTop("Bagshui frames", FrameUsage(), TOP_FRAMES)
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
BsSlash:AddHandler("CPU", function(tokens)
if tokens[2] and string.lower(tokens[2]) == "reset" then
CpuProfile:Reset()
else
CpuProfile:Report()
end
end, true)
end)