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">
+
+