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
178 lines
5.7 KiB
Lua
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)
|