diff --git a/BENCHMARKING.md b/BENCHMARKING.md new file mode 100644 index 0000000..95f1eab --- /dev/null +++ b/BENCHMARKING.md @@ -0,0 +1,124 @@ +# Performance Benchmarking System + +This system provides tools to measure and validate performance optimizations for SenseiClassResourceBar. + +## Quick Start + +### 1. Enable Profiling +``` +/scrbprofile start +``` + +### 2. Run Benchmarks +``` +/scrbbench full +``` + +### 3. View Results +``` +/scrbprofile print +``` + +## Profiler Commands + +- `/scrbprofile start` - Enable performance profiling +- `/scrbprofile stop` - Disable profiling +- `/scrbprofile reset` - Clear all statistics +- `/scrbprofile print` - Show detailed performance report +- `/scrbprofile chat` - Show summary in chat (top 5 functions) + +## Benchmark Commands + +- `/scrbbench combat [seconds]` - Simulate combat scenario (default: 30s) + - Forces frequent bar updates to simulate real combat + - Good for testing OnUpdate performance + +- `/scrbbench layout [iterations]` - Test layout recalculation (default: 100) + - Measures ApplyLayout() performance + - Should be rare in normal use + +- `/scrbbench display [iterations]` - Test display updates (default: 1000) + - Measures UpdateDisplay() performance + - This is the most frequently called function + +- `/scrbbench memory` - Show current memory usage + +- `/scrbbench full` - Run complete benchmark suite + - Layout test (50 iterations) + - Display test (500 iterations) + - Memory snapshot + +## Example Workflow + +### Before Optimizations + +1. Load into game with your character +2. Enable profiling: `/scrbprofile start` +3. Run benchmark suite: `/scrbbench full` +4. Note the results +5. Run a combat simulation: `/scrbbench combat 60` +6. View detailed stats: `/scrbprofile print` +7. **Save/screenshot the results** + +### After Optimizations + +1. Reload UI (or restart) +2. Enable profiling: `/scrbprofile start` +3. Run the same benchmarks +4. Compare results with "before" data + +## What to Look For + +### Key Metrics + +1. **OnUpdate:Fast / OnUpdate:Slow** + - Calls/second should be high (10/s for fast, 4/s for slow) + - Average time per call should be LOW (<0.1ms ideally) + - This is where polling overhead shows up + +2. **BarMixin:UpdateDisplay** + - Most frequently called function + - Should have low average time (<0.2ms) + - High total time is acceptable if calls are necessary + +3. **BarMixin:ApplyLayout** + - Should be called RARELY (not every frame) + - Can have higher average time (it's expensive) + - Low call count = good architecture + +4. **BarMixin:UpdateFragmentedPowerDisplay** + - Only relevant for classes with fragmented power (Runes, ComboPoints, etc.) + - Should have reasonable average time (<0.5ms) + +### Performance Targets + +- **OnUpdate should be minimal or conditional** - Only use when necessary (cooldowns, smooth animations) +- **UpdateDisplay should be efficient** - Low average time per call (<0.2ms ideal) +- **ApplyLayout should be rare** - Called only on config changes, not frequently +- **Memory should be stable** - No continuous growth during normal play +- **Event-driven updates preferred** - React to game events rather than polling when possible + +## Understanding the Output + +``` +Function Calls Total(ms) Avg(ms) Min(ms) Max(ms) Calls/s +───────────────────────────────────────────────────────────────────────────────────────── +OnUpdate:Fast 10000 1234.56 0.123 0.050 2.500 333.3 +BarMixin:UpdateDisplay 5000 567.89 0.114 0.080 1.200 166.7 +BarMixin:ApplyLayout 10 123.45 12.345 10.000 15.000 0.3 +``` + +- **Calls**: How many times the function was called +- **Total(ms)**: Total time spent in this function (cumulative) +- **Avg(ms)**: Average time per call (Total/Calls) +- **Min(ms)**: Fastest single execution +- **Max(ms)**: Slowest single execution (spikes) +- **Calls/s**: How frequently this function is called + +## Tips + +- Run benchmarks in a consistent location (e.g., Stormwind) +- Disable other addons for cleaner results +- Run each test 2-3 times and average the results +- Test with different classes (some have more complex bars) +- Test both in and out of combat diff --git a/Bars/Abstract/Bar.lua b/Bars/Abstract/Bar.lua index 8bbdad0..eef8a90 100755 --- a/Bars/Abstract/Bar.lua +++ b/Bars/Abstract/Bar.lua @@ -76,6 +76,8 @@ function BarMixin:Init(config, parent, frameLevel) self._displayOrder = {} self._cachedTextFormat = nil self._cachedTextPattern = nil + self._lastDisplayedText = "" + -- Pre-allocate rune tracking tables (for Death Knights) self._runeReadyList = {} self._runeCdList = {} @@ -84,6 +86,15 @@ function BarMixin:Init(config, parent, frameLevel) for i = 1, 6 do self._runeInfoPool[i] = { index = 0, remaining = 0, frac = 0 } end + + -- Pre-allocate combo point tracking (for Rogues, Druids, etc.) + self._chargedLookup = {} + + -- Pre-allocate essence state list (for Evokers) + self._essenceStateList = {} + + -- Track combat state for conditional OnUpdate + self._inCombat = InCombatLockdown() self.Frame = Frame end @@ -217,6 +228,48 @@ end ---@param event string ---@param ... any function BarMixin:OnEvent(event, ...) + -- Override in child classes +end + +-- Intelligently manage OnUpdate based on resource type +function BarMixin:UpdateOnUpdateState() + local resource = self:GetResource() + + -- Enable OnUpdate for resources that need continuous updates: + -- 1. Cooldown tracking (Runes, Essences) - always needed + -- 2. Regenerating resources (Energy, Focus, Fury) - always needed for smooth regen + -- 3. Decay over time (Rage, Runic Power) - only needed OUT of combat + local needsOnUpdate = false + + if resource == Enum.PowerType.Runes or resource == Enum.PowerType.Essence then + -- Runes and Essences always need OnUpdate for cooldown/regen display + needsOnUpdate = true + elseif resource == Enum.PowerType.Energy or resource == Enum.PowerType.Focus or resource == Enum.PowerType.Fury then + -- Energy, Focus, and Fury regenerate continuously - need OnUpdate for smooth display + -- UNIT_POWER_UPDATE events don't fire frequently enough during passive regen + needsOnUpdate = true + elseif resource == Enum.PowerType.Rage or resource == Enum.PowerType.RunicPower then + -- Rage and Runic Power decay over time + -- Only need OnUpdate when OUT of combat for smooth decay + -- In combat, UNIT_POWER_UPDATE events handle updates + needsOnUpdate = not self._inCombat + end + + -- Check user preference + local data = self:GetData() + if data and data.fasterUpdates == true and needsOnUpdate then + self:EnableFasterUpdates() + elseif needsOnUpdate then + self:DisableFasterUpdates() + else + -- No OnUpdate needed - rely on events only + self:DisableOnUpdate() + end +end + +function BarMixin:DisableOnUpdate() + self.fasterUpdates = false + self.Frame:SetScript("OnUpdate", nil) end -- You should handle what to change here too and set self.fasterUpdates to true @@ -227,7 +280,9 @@ function BarMixin:EnableFasterUpdates() frame.elapsed = (frame.elapsed or 0) + delta if frame.elapsed >= 0.1 then frame.elapsed = 0 + if addonTable.Profiler then addonTable.Profiler:Start("OnUpdate:Fast") end self:UpdateDisplay() + if addonTable.Profiler then addonTable.Profiler:Stop("OnUpdate:Fast") end end end end @@ -242,7 +297,9 @@ function BarMixin:DisableFasterUpdates() frame.elapsed = (frame.elapsed or 0) + delta if frame.elapsed >= 0.25 then frame.elapsed = 0 + if addonTable.Profiler then addonTable.Profiler:Start("OnUpdate:Slow") end self:UpdateDisplay() + if addonTable.Profiler then addonTable.Profiler:Stop("OnUpdate:Slow") end end end end @@ -254,10 +311,18 @@ end ------------------------------------------------------------ function BarMixin:UpdateDisplay(layoutName, force) - if not self:IsShown() and not force then return end + if addonTable.Profiler then addonTable.Profiler:Start("BarMixin:UpdateDisplay") end + + if not self:IsShown() and not force then + if addonTable.Profiler then addonTable.Profiler:Stop("BarMixin:UpdateDisplay") end + return + end local data = self:GetData(layoutName) - if not data then return end + if not data then + if addonTable.Profiler then addonTable.Profiler:Stop("BarMixin:UpdateDisplay") end + return + end -- Cache data to avoid redundant GetData() calls @@ -321,12 +386,29 @@ function BarMixin:UpdateDisplay(layoutName, force) end end - self.TextValue:SetFormattedText(self._cachedFormat, unpack(valuesToDisplay, 1, self._cachedNum)) + -- Only update text if values actually changed (dirty state tracking) + -- Skip comparison for secret values to avoid errors + local newText = string.format(self._cachedFormat, unpack(valuesToDisplay, 1, self._cachedNum)) + if not issecretvalue(newText) and self._lastDisplayedText ~= newText then + self.TextValue:SetText(newText) + self._lastDisplayedText = newText + elseif issecretvalue(newText) then + -- For secret values, always update since we can't compare + self.TextValue:SetText(newText) + end + else + -- Clear text if not showing + if self._lastDisplayedText ~= "" then + self.TextValue:SetFormattedText("") + self._lastDisplayedText = "" + end end if addonTable.fragmentedPowerTypes[resource] then self:UpdateFragmentedPowerDisplay(layoutName, data, max) end + + if addonTable.Profiler then addonTable.Profiler:Stop("BarMixin:UpdateDisplay") end end ------------------------------------------------------------ @@ -544,10 +626,18 @@ function BarMixin:GetSize(layoutName, data) end function BarMixin:ApplyLayout(layoutName, force) - if not self:IsShown() and not force then return end + if addonTable.Profiler then addonTable.Profiler:Start("BarMixin:ApplyLayout") end + + if not self:IsShown() and not force then + if addonTable.Profiler then addonTable.Profiler:Stop("BarMixin:ApplyLayout") end + return + end local data = self:GetData(layoutName) - if not data then return end + if not data then + if addonTable.Profiler then addonTable.Profiler:Stop("BarMixin:ApplyLayout") end + return + end -- Init Fragmented Power Bars if needed local resource = self:GetResource() @@ -576,11 +666,8 @@ function BarMixin:ApplyLayout(layoutName, force) self:UpdateTicksLayout(layoutName, data) - if data.fasterUpdates then - self:EnableFasterUpdates() - else - self:DisableFasterUpdates() - end + -- Intelligently enable/disable OnUpdate based on resource type + self:UpdateOnUpdateState() if addonTable.fragmentedPowerTypes[resource] then self:UpdateFragmentedPowerDisplay(layoutName, data) @@ -595,6 +682,8 @@ function BarMixin:ApplyLayout(layoutName, force) end end end + + if addonTable.Profiler then addonTable.Profiler:Stop("BarMixin:ApplyLayout") end end function BarMixin:ApplyFontSettings(layoutName, data) @@ -977,11 +1066,19 @@ function BarMixin:CreateFragmentedPowerBars(layoutName, data) end function BarMixin:UpdateFragmentedPowerDisplay(layoutName, data, maxPower) + if addonTable.Profiler then addonTable.Profiler:Start("BarMixin:UpdateFragmentedPowerDisplay") end + data = data or self:GetData(layoutName) - if not data then return end + if not data then + if addonTable.Profiler then addonTable.Profiler:Stop("BarMixin:UpdateFragmentedPowerDisplay") end + return + end local resource = self:GetResource() - if not resource then return end + if not resource then + if addonTable.Profiler then addonTable.Profiler:Stop("BarMixin:UpdateFragmentedPowerDisplay") end + return + end -- Use passed maxPower to avoid redundant UnitPowerMax call maxPower = maxPower or (resource == "MAELSTROM_WEAPON" and 5 or UnitPowerMax("player", resource)) if maxPower <= 0 then return end @@ -1001,7 +1098,10 @@ function BarMixin:UpdateFragmentedPowerDisplay(layoutName, data, maxPower) local overchargedCpColor = addonTable:GetOverrideResourceColor("OVERCHARGED_COMBO_POINTS") or color local charged = GetUnitChargedPowerPoints("player") or {} - local chargedLookup = {} + + -- Reuse pre-allocated table instead of creating new one + local chargedLookup = self._chargedLookup + for k in pairs(chargedLookup) do chargedLookup[k] = nil end for _, index in ipairs(charged) do chargedLookup[index] = true end @@ -1089,9 +1189,9 @@ function BarMixin:UpdateFragmentedPowerDisplay(layoutName, data, maxPower) self._LastEssence = current - -- Reuse pre-allocated table for performance + -- Reuse pre-allocated tables for performance local displayOrder = self._displayOrder - local stateList = {} + local stateList = self._essenceStateList for i = 1, maxEssence do if i <= current then stateList[i] = "full" @@ -1317,6 +1417,8 @@ function BarMixin:UpdateFragmentedPowerDisplay(layoutName, data, maxPower) end end end + + if addonTable.Profiler then addonTable.Profiler:Stop("BarMixin:UpdateFragmentedPowerDisplay") end end addonTable.BarMixin = BarMixin \ No newline at end of file diff --git a/Bars/Abstract/PowerBar.lua b/Bars/Abstract/PowerBar.lua old mode 100644 new mode 100755 index 0dada62..47942c7 --- a/Bars/Abstract/PowerBar.lua +++ b/Bars/Abstract/PowerBar.lua @@ -18,6 +18,10 @@ function PowerBarMixin:OnLoad() self.Frame:RegisterUnitEvent("UNIT_MAXPOWER", "player") self.Frame:RegisterEvent("PET_BATTLE_OPENING_START") self.Frame:RegisterEvent("PET_BATTLE_CLOSE") + + -- Event-driven power updates instead of constant OnUpdate polling + self.Frame:RegisterUnitEvent("UNIT_POWER_UPDATE", "player") + self.Frame:RegisterUnitEvent("UNIT_DISPLAYPOWER", "player") local playerClass = select(2, UnitClass("player")) @@ -35,6 +39,8 @@ function PowerBarMixin:OnEvent(event, ...) self:ApplyVisibilitySettings() self:ApplyLayout(nil, true) + self:UpdateDisplay(true) + self:UpdateOnUpdateState() elseif event == "PLAYER_REGEN_ENABLED" or event == "PLAYER_REGEN_DISABLED" or event == "PLAYER_TARGET_CHANGED" @@ -42,12 +48,30 @@ function PowerBarMixin:OnEvent(event, ...) or event == "PLAYER_MOUNT_DISPLAY_CHANGED" or event == "PET_BATTLE_OPENING_START" or event == "PET_BATTLE_CLOSE" then + -- Update combat state for OnUpdate management + if event == "PLAYER_REGEN_DISABLED" then + self._inCombat = true + elseif event == "PLAYER_REGEN_ENABLED" then + self._inCombat = false + end + self:ApplyVisibilitySettings(nil, event == "PLAYER_REGEN_DISABLED") self:UpdateDisplay() + + -- Update OnUpdate state when entering/leaving combat (for decaying resources) + if event == "PLAYER_REGEN_ENABLED" or event == "PLAYER_REGEN_DISABLED" then + self:UpdateOnUpdateState() + end elseif event == "UNIT_MAXPOWER" and unit == "player" then self:ApplyLayout(nil, true) + self:UpdateDisplay(true) + + elseif (event == "UNIT_POWER_UPDATE" or event == "UNIT_DISPLAYPOWER") and unit == "player" then + + -- Event-driven update: only update when power actually changes + self:UpdateDisplay() end end diff --git a/Bars/HealthBar.lua b/Bars/HealthBar.lua old mode 100644 new mode 100755 index a36e726..c394c2b --- a/Bars/HealthBar.lua +++ b/Bars/HealthBar.lua @@ -58,6 +58,10 @@ function HealthBarMixin:OnLoad() self.Frame:RegisterEvent("PLAYER_MOUNT_DISPLAY_CHANGED") self.Frame:RegisterEvent("PET_BATTLE_OPENING_START") self.Frame:RegisterEvent("PET_BATTLE_CLOSE") + + -- Event-driven health updates + self.Frame:RegisterUnitEvent("UNIT_HEALTH", "player") + self.Frame:RegisterUnitEvent("UNIT_MAXHEALTH", "player") end function HealthBarMixin:OnEvent(event, ...) @@ -79,6 +83,11 @@ function HealthBarMixin:OnEvent(event, ...) self:ApplyVisibilitySettings(nil, event == "PLAYER_REGEN_DISABLED") self:UpdateDisplay() + elseif (event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH") and unit == "player" then + + -- Event-driven update: only update when health actually changes + self:UpdateDisplay() + end end diff --git a/Helpers/Benchmark.lua b/Helpers/Benchmark.lua new file mode 100644 index 0000000..f8824fb --- /dev/null +++ b/Helpers/Benchmark.lua @@ -0,0 +1,189 @@ +local _, addonTable = ... + +------------------------------------------------------------ +-- BENCHMARK UTILITIES +-- Helps create reproducible performance test scenarios +------------------------------------------------------------ + +local Benchmark = {} + +-- Simulates a combat scenario by forcing frequent updates +function Benchmark:RunCombatSimulation(duration) + duration = duration or 30 -- Default 30 seconds + + print("|cFF00FF00[SCRB Benchmark]|r Starting combat simulation for " .. duration .. " seconds...") + print("|cFF00FF00[SCRB Benchmark]|r Make sure profiling is enabled: /scrbprofile start") + + local startTime = GetTime() + local endTime = startTime + duration + local updateCount = 0 + + -- Create a frame to simulate combat updates + local frame = CreateFrame("Frame") + frame:SetScript("OnUpdate", function(self, elapsed) + local now = GetTime() + + if now >= endTime then + frame:SetScript("OnUpdate", nil) + print("|cFF00FF00[SCRB Benchmark]|r Combat simulation complete!") + print("|cFF00FF00[SCRB Benchmark]|r " .. updateCount .. " update cycles simulated") + print("|cFF00FF00[SCRB Benchmark]|r Use /scrbprofile print to see results") + return + end + + -- Force all bars to update + if addonTable.barInstances then + for _, bar in pairs(addonTable.barInstances) do + if bar and bar.UpdateDisplay then + bar:UpdateDisplay() + updateCount = updateCount + 1 + end + end + end + end) +end + +-- Test layout recalculation performance +function Benchmark:TestLayoutPerformance(iterations) + iterations = iterations or 100 + + print("|cFF00FF00[SCRB Benchmark]|r Testing layout performance with " .. iterations .. " iterations...") + + local startTime = debugprofilestop() + + if addonTable.barInstances then + for i = 1, iterations do + for _, bar in pairs(addonTable.barInstances) do + if bar and bar.ApplyLayout then + bar:ApplyLayout(nil, true) + end + end + end + end + + local endTime = debugprofilestop() + local elapsed = endTime - startTime + + local barCount = 0 + if addonTable.barInstances then + for _ in pairs(addonTable.barInstances) do + barCount = barCount + 1 + end + end + + local totalCalls = iterations * barCount + local avgTime = totalCalls > 0 and (elapsed / totalCalls) or 0 + + print("|cFF00FF00[SCRB Benchmark]|r Layout test complete!") + print(string.format(" Total time: %.2f ms", elapsed)) + print(string.format(" Iterations: %d x %d bars = %d calls", iterations, barCount, totalCalls)) + print(string.format(" Average: %.3f ms per call", avgTime)) +end + +-- Test display update performance +function Benchmark:TestDisplayPerformance(iterations) + iterations = iterations or 1000 + + print("|cFF00FF00[SCRB Benchmark]|r Testing display update performance with " .. iterations .. " iterations...") + + local startTime = debugprofilestop() + + if addonTable.barInstances then + for i = 1, iterations do + for _, bar in pairs(addonTable.barInstances) do + if bar and bar.UpdateDisplay then + bar:UpdateDisplay() + end + end + end + end + + local endTime = debugprofilestop() + local elapsed = endTime - startTime + + local barCount = 0 + if addonTable.barInstances then + for _ in pairs(addonTable.barInstances) do + barCount = barCount + 1 + end + end + + local totalCalls = iterations * barCount + local avgTime = totalCalls > 0 and (elapsed / totalCalls) or 0 + + print("|cFF00FF00[SCRB Benchmark]|r Display update test complete!") + print(string.format(" Total time: %.2f ms", elapsed)) + print(string.format(" Iterations: %d x %d bars = %d calls", iterations, barCount, totalCalls)) + print(string.format(" Average: %.3f ms per call", avgTime)) +end + +-- Memory usage snapshot +function Benchmark:MemorySnapshot() + UpdateAddOnMemoryUsage() + local memory = GetAddOnMemoryUsage("SenseiClassResourceBar") + + print("|cFF00FF00[SCRB Benchmark]|r Memory Usage:") + print(string.format(" Current: %.2f KB", memory)) + + return memory +end + +-- Run a full benchmark suite +function Benchmark:RunFullSuite() + print("|cFF00FF00[SCRB Benchmark]|r ═══════════════════════════════════════") + print("|cFF00FF00[SCRB Benchmark]|r Running Full Benchmark Suite") + print("|cFF00FF00[SCRB Benchmark]|r ═══════════════════════════════════════") + + -- Memory before + local memBefore = self:MemorySnapshot() + + -- Layout test + print("") + self:TestLayoutPerformance(50) + + -- Display test + print("") + self:TestDisplayPerformance(500) + + -- Memory after + print("") + local memAfter = self:MemorySnapshot() + local memDelta = memAfter - memBefore + print(string.format(" Memory delta: %.2f KB", memDelta)) + + print("") + print("|cFF00FF00[SCRB Benchmark]|r ═══════════════════════════════════════") + print("|cFF00FF00[SCRB Benchmark]|r Benchmark Suite Complete!") + print("|cFF00FF00[SCRB Benchmark]|r ═══════════════════════════════════════") +end + +-- Slash command +SLASH_SCRBBENCH1 = "/scrbbench" +SlashCmdList["SCRBBENCH"] = function(msg) + local cmd, arg = msg:match("^(%S*)%s*(.-)$") + cmd = cmd:lower() + + if cmd == "combat" then + local duration = tonumber(arg) or 30 + Benchmark:RunCombatSimulation(duration) + elseif cmd == "layout" then + local iterations = tonumber(arg) or 100 + Benchmark:TestLayoutPerformance(iterations) + elseif cmd == "display" then + local iterations = tonumber(arg) or 1000 + Benchmark:TestDisplayPerformance(iterations) + elseif cmd == "memory" or cmd == "mem" then + Benchmark:MemorySnapshot() + elseif cmd == "full" or cmd == "suite" then + Benchmark:RunFullSuite() + else + print("|cFF00FF00[SCRB Benchmark]|r Commands:") + print(" /scrbbench combat [seconds] - Simulate combat (default: 30s)") + print(" /scrbbench layout [iterations] - Test layout performance (default: 100)") + print(" /scrbbench display [iterations] - Test display updates (default: 1000)") + print(" /scrbbench memory - Show current memory usage") + print(" /scrbbench full - Run complete benchmark suite") + end +end + +addonTable.Benchmark = Benchmark diff --git a/Helpers/Profiler.lua b/Helpers/Profiler.lua new file mode 100644 index 0000000..02bee12 --- /dev/null +++ b/Helpers/Profiler.lua @@ -0,0 +1,198 @@ +local _, addonTable = ... + +------------------------------------------------------------ +-- PERFORMANCE PROFILER +-- Measures execution time and call counts for optimization validation +------------------------------------------------------------ + +local Profiler = { + enabled = false, + data = {}, + sessionStart = 0, +} + +function Profiler:Init() + self.enabled = true + self.sessionStart = debugprofilestop() + self.data = {} + print("|cFF00FF00[SCRB Profiler]|r Profiling enabled. Use /scrb profile to view stats.") +end + +function Profiler:Reset() + self.sessionStart = debugprofilestop() + self.data = {} + print("|cFF00FF00[SCRB Profiler]|r Statistics reset.") +end + +function Profiler:Track(funcName) + if not self.enabled then return end + + if not self.data[funcName] then + self.data[funcName] = { + calls = 0, + totalTime = 0, + minTime = math.huge, + maxTime = 0, + } + end +end + +function Profiler:Start(funcName) + if not self.enabled then return end + + self:Track(funcName) + + -- Store start time in a stack to handle nested calls + if not self.callStack then + self.callStack = {} + end + + table.insert(self.callStack, { + name = funcName, + startTime = debugprofilestop(), + }) +end + +function Profiler:Stop(funcName) + if not self.enabled or not self.callStack or #self.callStack == 0 then return end + + local endTime = debugprofilestop() + local callInfo = table.remove(self.callStack) + + -- Verify we're stopping the right function + if callInfo.name ~= funcName then + print("|cFFFF0000[SCRB Profiler]|r Warning: Mismatched Stop() call. Expected:", callInfo.name, "Got:", funcName) + return + end + + local elapsed = endTime - callInfo.startTime + local stats = self.data[funcName] + + stats.calls = stats.calls + 1 + stats.totalTime = stats.totalTime + elapsed + stats.minTime = math.min(stats.minTime, elapsed) + stats.maxTime = math.max(stats.maxTime, elapsed) +end + +-- Wrap a function to automatically profile it +function Profiler:Wrap(funcName, func) + if not self.enabled then return func end + + return function(...) + self:Start(funcName) + local results = {func(...)} + self:Stop(funcName) + return unpack(results) + end +end + +function Profiler:GetStats() + local sessionDuration = (debugprofilestop() - self.sessionStart) / 1000 -- Convert to seconds + local stats = {} + + for funcName, data in pairs(self.data) do + local avgTime = data.calls > 0 and (data.totalTime / data.calls) or 0 + + table.insert(stats, { + name = funcName, + calls = data.calls, + totalTime = data.totalTime, + avgTime = avgTime, + minTime = data.minTime == math.huge and 0 or data.minTime, + maxTime = data.maxTime, + callsPerSec = sessionDuration > 0 and (data.calls / sessionDuration) or 0, + }) + end + + -- Sort by total time (highest first) + table.sort(stats, function(a, b) + return a.totalTime > b.totalTime + end) + + return stats, sessionDuration +end + +function Profiler:Print() + if not self.enabled then + print("|cFFFF0000[SCRB Profiler]|r Profiler is not enabled. Use /scrb profile start to enable.") + return + end + + local stats, sessionDuration = self:GetStats() + + print("|cFF00FF00[SCRB Profiler]|r Performance Statistics:") + print(string.format("Session Duration: %.2f seconds", sessionDuration)) + print("─────────────────────────────────────────────────────────────────────────") + print(string.format("%-35s %8s %10s %8s %8s %8s %8s", + "Function", "Calls", "Total(ms)", "Avg(ms)", "Min(ms)", "Max(ms)", "Calls/s")) + print("─────────────────────────────────────────────────────────────────────────") + + for _, stat in ipairs(stats) do + print(string.format("%-35s %8d %10.2f %8.3f %8.3f %8.3f %8.1f", + stat.name, + stat.calls, + stat.totalTime, + stat.avgTime, + stat.minTime, + stat.maxTime, + stat.callsPerSec + )) + end + + print("─────────────────────────────────────────────────────────────────────────") + + -- Calculate total overhead + local totalTime = 0 + local totalCalls = 0 + for _, stat in ipairs(stats) do + totalTime = totalTime + stat.totalTime + totalCalls = totalCalls + stat.calls + end + + print(string.format("Total: %d calls, %.2f ms (%.2f%% of session)", + totalCalls, totalTime, (totalTime / (sessionDuration * 1000)) * 100)) +end + +function Profiler:PrintToChat() + if not self.enabled then + print("|cFFFF0000[SCRB Profiler]|r Profiler is not enabled.") + return + end + + local stats, sessionDuration = self:GetStats() + + print("|cFF00FF00[SCRB Profiler]|r Top 5 functions by total time:") + for i = 1, math.min(5, #stats) do + local stat = stats[i] + print(string.format("%d. %s: %d calls, %.2fms total, %.3fms avg", + i, stat.name, stat.calls, stat.totalTime, stat.avgTime)) + end +end + +-- Slash command handler +SLASH_SCRBPROFILE1 = "/scrbprofile" +SlashCmdList["SCRBPROFILE"] = function(msg) + msg = msg:lower():trim() + + if msg == "start" or msg == "enable" then + Profiler:Init() + elseif msg == "stop" or msg == "disable" then + Profiler.enabled = false + print("|cFF00FF00[SCRB Profiler]|r Profiling disabled.") + elseif msg == "reset" then + Profiler:Reset() + elseif msg == "print" or msg == "show" or msg == "" then + Profiler:Print() + elseif msg == "chat" then + Profiler:PrintToChat() + else + print("|cFF00FF00[SCRB Profiler]|r Commands:") + print(" /scrbprofile start - Enable profiling") + print(" /scrbprofile stop - Disable profiling") + print(" /scrbprofile reset - Reset statistics") + print(" /scrbprofile print - Show detailed stats") + print(" /scrbprofile chat - Show summary in chat") + end +end + +addonTable.Profiler = Profiler diff --git a/Helpers/embeds.xml b/Helpers/embeds.xml old mode 100644 new mode 100755 index 764eb47..74d5ff7 --- a/Helpers/embeds.xml +++ b/Helpers/embeds.xml @@ -2,6 +2,8 @@ ..\..\Blizzard_SharedXML\UI.xsd">