Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions BENCHMARKING.md
Original file line number Diff line number Diff line change
@@ -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
132 changes: 117 additions & 15 deletions Bars/Abstract/Bar.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

------------------------------------------------------------
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Loading