-- 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)