You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

795 lines
23 KiB

if not WeakAuras.IsLibsOK() then
return
end
---@type string
local AddonName = ...
---@class Private
local Private = select(2, ...)
---@class WeakAuras
local WeakAuras = WeakAuras
local L = WeakAuras.L
local prettyPrint = WeakAuras.prettyPrint
local LGF = LibStub("LibGetFrame-1.0")
local profileData = {}
profileData.systems = {}
profileData.auras = {}
local currentProfileState, ProfilingTimer
local table_to_string
table_to_string = function(tbl, depth)
if depth and depth >= 3 then
return "{ ... }"
end
local str
for k, v in pairs(tbl) do
if type(v) ~= "userdata" then
if type(v) == "table" then
v = table_to_string(v, (depth and depth + 1 or 1))
elseif type(v) == "function" then
v = "function"
elseif type(v) == "string" then
v = '"' .. v .. '"'
end
if type(k) == "string" then
k = '"' .. k .. '"'
end
str = (str and str .. "|cff999999,|r " or "|cff999999{|r ") .. "|cffffff99[" .. tostring(k) .. "]|r |cff999999=|r |cffffffff" .. tostring(v) .. "|r"
end
end
return (str or "{ ") .. " }"
end
WeakAurasProfilingReportMixin = {}
function WeakAurasProfilingReportMixin:OnShow()
if self.initialised then
return
end
self.initialised = true
ButtonFrameTemplate_HidePortrait(self)
self:SetTitle(L["WeakAuras Profiling Report"])
self:SetSize(500, 300)
end
function WeakAurasProfilingReportMixin:ClearText()
self.ScrollBox.messageFrame:SetText("")
end
function WeakAurasProfilingReportMixin:AddText(v)
if not v then
return
end
--- @type string?
local m = self.ScrollBox.messageFrame:GetText()
if m ~= "" then
m = m .. "|n"
end
if type(v) == "table" then
v = table_to_string(v)
end
self.ScrollBox.messageFrame.originalText = m .. v
self.ScrollBox.messageFrame:SetText(self.ScrollBox.messageFrame.originalText)
end
local function StartProfiling(map, id)
if not map[id] then
map[id] = {}
map[id].count = 1
map[id].start = debugprofilestop()
map[id].elapsed = 0
map[id].spike = 0
return
end
if map[id].count == 0 then
map[id].count = 1
map[id].start = debugprofilestop()
else
map[id].count = map[id].count + 1
end
end
local function StopProfiling(map, id)
map[id].count = map[id].count - 1
if map[id].count == 0 then
local elapsed = debugprofilestop() - map[id].start
map[id].elapsed = map[id].elapsed + elapsed
if elapsed > map[id].spike then
map[id].spike = elapsed
end
end
end
local function StartProfileSystem(system)
StartProfiling(profileData.systems, "wa")
StartProfiling(profileData.systems, system)
end
local function StartProfileAura(id)
StartProfiling(profileData.auras, id)
end
local function StopProfileSystem(system)
StopProfiling(profileData.systems, "wa")
StopProfiling(profileData.systems, system)
end
local function StopProfileAura(id)
StopProfiling(profileData.auras, id)
end
local function StartProfileUID(uid)
StartProfiling(profileData.auras, Private.UIDtoID(uid))
end
local function StopProfileUID(uid)
StopProfiling(profileData.auras, Private.UIDtoID(uid))
end
function Private.ProfileRenameAura(oldid, id)
profileData.auras[id] = profileData.auras[id]
profileData.auras[oldid] = nil
end
local RegisterProfile = function(startType)
if startType == "boss" then
startType = "encounter"
end
local delayedStart
if startType == "encounter" then
WeakAurasProfilingFrame:UnregisterAllEvents()
prettyPrint(L["Your next encounter will automatically be profiled."])
WeakAurasProfilingFrame:RegisterEvent("ENCOUNTER_START")
WeakAurasProfilingFrame:RegisterEvent("ENCOUNTER_END")
currentProfileState = startType
delayedStart = true
elseif startType == "combat" then
WeakAurasProfilingFrame:UnregisterAllEvents()
prettyPrint(L["Your next instance of combat will automatically be profiled."])
WeakAurasProfilingFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
WeakAurasProfilingFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
currentProfileState = startType
delayedStart = true
elseif startType == "autostart" then
prettyPrint(L["Profiling automatically started."])
currentProfileState = "profiling"
elseif startType and startType:match("%d") then
WeakAurasProfilingFrame:UnregisterAllEvents()
local time = startType + 0
prettyPrint(L["Profiling started. It will end automatically in %d seconds"]:format(time))
ProfilingTimer = WeakAuras.timer:ScheduleTimer(WeakAuras.StopProfile, time)
currentProfileState = "profiling"
else
WeakAurasProfilingFrame:UnregisterAllEvents()
prettyPrint(L["Profiling started."])
currentProfileState = "profiling"
end
WeakAurasProfilingFrame:UpdateButtons()
return delayedStart
end
---@diagnostic disable-next-line: duplicate-set-field
function WeakAuras.StartProfile(startType)
if currentProfileState == "profiling" then
prettyPrint(L["Profiling already started."])
return
end
if RegisterProfile(startType) then
-- Scheduled for later
return
end
profileData.systems = {}
profileData.auras = {}
profileData.systems.time = {}
profileData.systems.time.start = debugprofilestop()
profileData.systems.time.elapsed = nil
profileData.systems.time.count = 1
Private.StartProfileSystem = StartProfileSystem
Private.StartProfileAura = StartProfileAura
Private.StartProfileUID = StartProfileUID
Private.StopProfileSystem = StopProfileSystem
Private.StopProfileAura = StopProfileAura
Private.StopProfileUID = StopProfileUID
LGF.StartProfile()
end
local function doNothing() end
---@diagnostic disable-next-line: duplicate-set-field
function WeakAuras.StopProfile()
if currentProfileState ~= "profiling" then
prettyPrint(L["Profiling not running."])
return
end
prettyPrint(L["Profiling stopped."])
profileData.systems.time.elapsed = debugprofilestop() - profileData.systems.time.start
profileData.systems.time.count = 0
Private.StartProfileSystem = doNothing
Private.StartProfileAura = doNothing
Private.StartProfileUID = doNothing
Private.StopProfileSystem = doNothing
Private.StopProfileAura = doNothing
Private.StopProfileUID = doNothing
LGF.StopProfile()
currentProfileState = nil
if WeakAurasProfilingFrame then
WeakAurasProfilingFrame:UnregisterAllEvents()
WeakAurasProfilingFrame:UpdateButtons()
end
if ProfilingTimer then
WeakAuras.timer:CancelTimer(ProfilingTimer)
ProfilingTimer = nil
end
end
function WeakAuras.ToggleProfile()
if not profileData.systems.time or profileData.systems.time.count ~= 1 then
WeakAuras.StartProfile()
else
WeakAuras.StopProfile()
end
end
local function CancelScheduledProfile()
prettyPrint(L["Your scheduled automatic profile has been cancelled."])
currentProfileState = nil
WeakAurasProfilingFrame:UnregisterAllEvents()
WeakAurasProfilingFrame:UpdateButtons()
end
WeakAuras.CancelScheduledProfile = CancelScheduledProfile
local function AutoStartStopProfiling(frame, event)
if event == "ENCOUNTER_START" or event == "PLAYER_REGEN_DISABLED" then
WeakAuras.StartProfile("autostart")
elseif event == "ENCOUNTER_END" or event == "PLAYER_REGEN_ENABLED" then
WeakAuras.StopProfile()
end
end
local function ColoredSpike(spike)
local r, g, b
if spike < 2 then
r, g, b = WeakAuras.GetHSVTransition(spike / 2, 0, 1, 0, 1, 1, 1, 0, 1)
elseif spike < 2.5 then
r, g, b = WeakAuras.GetHSVTransition((spike - 2) * 2, 1, 1, 0, 1, 1, 0.65, 0, 1)
elseif spike < 3 then
r, g, b = WeakAuras.GetHSVTransition((spike - 2.5) * 2, 1, 0.65, 0, 1, 1, 0, 0, 1)
else
r, g, b = 1, 0, 0
end
return ("|cff%02x%02x%02x%.2fms|r"):format(r * 255, g * 255, b * 255, spike)
end
local function PrintOneProfile(popup, name, map, total)
if map.count ~= 0 then
popup:AddText(name .. " ERROR: count is not zero:" .. " " .. map.count)
end
local percent = ""
if total then
percent = (", %.2f%%"):format(100 * map.elapsed / total)
end
local spikeInfo = ""
if map.spike then
spikeInfo = ColoredSpike(map.spike)
end
popup:AddText(("%s |cff999999%.2fms%s (%s)|r"):format(name, map.elapsed, percent, spikeInfo))
end
local function SortProfileMap(map, sortField)
local result = {}
for k in pairs(map) do
tinsert(result, k)
end
sort(result, function(a, b)
if sortField then
local x, y = map[a][sortField], map[b][sortField]
if x and y then
return x > y
end
end
return map[a].elapsed > map[b].elapsed
end)
return result
end
local function TotalProfileTime(map)
local total = 0
for k, v in pairs(map) do
if k ~= "time" and k ~= "wa" then
total = total + v.elapsed
end
end
return total
end
local function unitEventToMultiUnit(event)
local count
event, count = event:gsub("nameplate%d+$", "nameplate")
if count == 1 then
return event
end
event, count = event:gsub("boss%d$", "boss")
if count == 1 then
return event
end
event, count = event:gsub("arena%d$", "arena")
if count == 1 then
return event
end
event, count = event:gsub("raid%d+$", "group")
if count == 1 then
return event
end
event, count = event:gsub("raidpet%d+$", "group")
if count == 1 then
return event
end
event, count = event:gsub("party%d$", "party")
if count == 1 then
return event
end
event, count = event:gsub("partypet%d$", "party")
return event
end
---@diagnostic disable-next-line: duplicate-set-field
function WeakAuras.PrintProfile()
local popup = WeakAurasProfilingReport
if not profileData.systems.time then
prettyPrint(L["No Profiling information saved."])
return
end
if profileData.systems.time.count == 1 then
prettyPrint(L["Profiling still running, stop before trying to print."])
return
end
popup:ClearAllPoints()
if WeakAurasProfilingFrame and WeakAurasProfilingFrame:IsShown() then
popup:SetParent(WeakAurasProfilingFrame)
popup:SetPoint("TOPLEFT", WeakAurasProfilingFrame, "TOPRIGHT", 5, 0)
else
popup:SetParent(UIParent)
if WeakAurasSaved.ProfilingWindow then
popup:SetPoint("TOPLEFT", UIParent, "TOPLEFT", WeakAurasSaved.ProfilingWindow.xOffset or 0, WeakAurasSaved.ProfilingWindow.yOffset or 0)
else
popup:SetPoint("CENTER")
end
end
popup:ClearText()
PrintOneProfile(popup, "|cff9900ffTotal time:|r", profileData.systems.time)
PrintOneProfile(popup, "|cff9900ffTime inside WA:|r", profileData.systems.wa)
popup:AddText(string.format("|cff9900ffTime spent inside WA:|r %.2f%%", 100 * profileData.systems.wa.elapsed / profileData.systems.time.elapsed))
popup:AddText("")
popup:AddText("Note: Not every aspect of each aura can be tracked.")
popup:AddText("You can ask on our discord https://discord.gg/weakauras for help interpreting this output.")
popup:AddText("")
popup:AddText("|cff9900ffAuras:|r")
local total = TotalProfileTime(profileData.auras)
popup:AddText("Total time attributed to auras: ", floor(total) .. "ms")
for _, k in ipairs(SortProfileMap(profileData.auras), "spike") do
PrintOneProfile(popup, k, profileData.auras[k], total)
end
popup:AddText("")
popup:AddText("|cff9900ffSystems:|r")
-- make a new table for system data with multiUnits grouped
local systemRegrouped = {}
for k, v in pairs(profileData.systems) do
local event = unitEventToMultiUnit(k)
if systemRegrouped[event] == nil then
systemRegrouped[event] = CopyTable(v)
else
if v.elapsed then
systemRegrouped[event].elapsed = (systemRegrouped[event].elapsed or 0) + v.elapsed
end
if v.spike then
systemRegrouped[event].spike = (systemRegrouped[event].spike or 0) + v.spike
end
end
end
for i, k in ipairs(SortProfileMap(systemRegrouped)) do
if k ~= "time" and k ~= "wa" then
PrintOneProfile(popup, k, systemRegrouped[k], profileData.systems.wa.elapsed)
end
end
popup:AddText("")
popup:AddText("|cff9900ffLibGetFrame:|r")
for id, map in pairs(LGF.GetProfileData()) do
PrintOneProfile(popup, id, map)
end
popup:Show()
end
WeakAurasProfilingLineMixin = {
spikeTooltip = L["Maximum time used on a single frame"],
timeTooltip = L["Cumulated time used during profiling"],
}
function WeakAurasProfilingLineMixin:Init(e)
-- button.pct:SetText(pct)
self.progressBar.name:SetText(e.name)
self.time:SetText(("%.2fms"):format(e.time))
self.spike:SetText(ColoredSpike(e.spike))
self.progressBar:SetValue(e.pct)
end
local COLUMN_INFO = {
{
title = L["Name"],
width = 300,
attribute = "name",
},
{
title = L["Time"],
width = 80,
attribute = "time",
},
{
title = L["Spike"],
width = 80,
attribute = "spike",
},
}
local function ApplyAlternateState(frame, alternate)
if alternate then
frame.progressBar:SetStatusBarColor(0.7, 0.7, 0.7, 0.7)
else
frame.progressBar:SetStatusBarColor(0.5, 0.5, 0.5, 0.7)
end
end
local modes = {
L["Auras"],
L["Systems"],
}
WeakAurasProfilingMixin = {}
function WeakAurasProfilingMixin:OnShow()
if self.initialised then
return
end
self.initialised = true
ButtonFrameTemplate_HidePortrait(self)
self:SetTitle(L["WeakAuras Profiling"])
self:SetSize(500, 300)
self.mode = 1
UIDropDownMenu_SetText(self.buttons.modeDropDown, modes[1])
self.buttons.report:SetText(L["Report Summary"])
self.buttons.report.tooltip = L["A detailed overview of your auras and WeakAuras systems\nCopy the whole text to Weakaura's Discord if you need assistance."]
local minimizeButton = CreateFrame("Button", nil, self, "MaximizeMinimizeButtonFrameTemplate")
minimizeButton:SetPoint("RIGHT", self.CloseButton, "LEFT")
minimizeButton:SetOnMaximizedCallback(function()
self.minimized = false
self.buttons:Show()
self.ColumnDisplay:Show()
self.ScrollBox:Show()
self.ScrollBar:Show()
self.stats:Show()
self:ClearAllPoints()
self:SetPoint("TOPRIGHT", UIParent, "BOTTOMLEFT", self.right, self.top)
self:SetHeight(self.prevHeight)
end)
minimizeButton:SetOnMinimizedCallback(function()
self.minimized = true
self.buttons:Hide()
self.ColumnDisplay:Hide()
self.ScrollBox:Hide()
self.ScrollBar:Hide()
self.stats:Hide()
self.right, self.top = self:GetRight(), self:GetTop()
self:ClearAllPoints()
self:SetPoint("TOPRIGHT", UIParent, "BOTTOMLEFT", self.right, self.top)
self.prevHeight = self:GetHeight()
self:SetHeight(60)
end)
self.ColumnDisplay:LayoutColumns(COLUMN_INFO)
local view = CreateScrollBoxListLinearView()
view:SetElementInitializer("WeakAurasProfilingLineTemplate", function(frame, elementData)
frame:Init(elementData)
end)
ScrollUtil.InitScrollBoxListWithScrollBar(self.ScrollBox, self.ScrollBar, view)
ScrollUtil.RegisterAlternateRowBehavior(self.ScrollBox, ApplyAlternateState)
self.sortField = "time"
self.bars = CreateDataProvider()
self:SortByColumnIndex(2)
self.ScrollBox:SetDataProvider(self.bars)
self:InitDropDown()
self:InitModeDropDown()
self:SetScript("OnEvent", AutoStartStopProfiling)
self:SetScript("OnUpdate", function()
if profileData.systems.time and profileData.systems.time.count > 0 then
self:RefreshBars()
end
end)
self:UpdateButtons()
end
function WeakAurasProfilingResultButton_OnClick(self)
WeakAuras.PrintProfile()
end
local function nextEncounterButton_OnClick(self)
if currentProfileState ~= "encounter" then
WeakAuras.StartProfile("encounter")
end
local parent = self:GetParent()
local profilingFrame = parent.dropdown.Button:GetParent():GetParent():GetParent()
profilingFrame:ResetBars()
profilingFrame:UpdateButtons()
end
local function nextCombatButton_OnClick(self)
if currentProfileState ~= "combat" then
WeakAuras.StartProfile("combat")
end
local parent = self:GetParent()
local profilingFrame = parent.dropdown.Button:GetParent():GetParent():GetParent()
profilingFrame:ResetBars()
profilingFrame:UpdateButtons()
end
local function startNowButton_OnClick(self)
WeakAuras.StartProfile()
local parent = self:GetParent()
local profilingFrame = parent.dropdown.Button:GetParent():GetParent():GetParent()
profilingFrame:ResetBars()
profilingFrame:UpdateButtons()
end
function WeakAurasProfilingStopButton_OnClick(self)
if currentProfileState == "profiling" then
WeakAuras.StopProfile()
else
CancelScheduledProfile()
end
self:GetParent():GetParent():UpdateButtons()
end
function WeakAurasProfilingMixin:InitDropDown()
local function Initializer(dropDown, level)
local entries = {
{
text = L["Start Now"],
func = startNowButton_OnClick,
},
{
text = L["Next Encounter"],
func = nextEncounterButton_OnClick,
},
{
text = L["Next Combat"],
func = nextCombatButton_OnClick,
},
}
for _, entry in ipairs(entries) do
local info = UIDropDownMenu_CreateInfo()
info.notCheckable = true
info.text = entry.text
info.func = entry.func
UIDropDownMenu_AddButton(info)
end
end
local dropDown = self.buttons.startDropDown
local dropDownButton = self.buttons.start
UIDropDownMenu_SetInitializeFunction(dropDown, Initializer)
UIDropDownMenu_SetDisplayMode(dropDown, "MENU")
dropDownButton.Text:SetText(L["Start Profiling"])
dropDownButton:SetScript("OnMouseDown", function(o, button)
UIMenuButtonStretchMixin.OnMouseDown(dropDownButton, button)
ToggleDropDownMenu(1, nil, dropDown, dropDownButton, 130, 20)
end)
end
local function selectMode(self, mode)
local parent = self:GetParent()
local profilingFrame = parent.dropdown.Button:GetParent():GetParent():GetParent()
profilingFrame.mode = mode
UIDropDownMenu_SetText(profilingFrame.buttons.modeDropDown, modes[mode])
profilingFrame:ResetBars()
profilingFrame:RefreshBars(nil, true)
end
function WeakAurasProfilingMixin:InitModeDropDown()
local function Initializer(dropDown, level)
for i = 1, 2 do
local info = UIDropDownMenu_CreateInfo()
info.text = modes[i]
info.func = selectMode
info.checked = i == self.mode
info.arg1 = i
UIDropDownMenu_AddButton(info)
end
end
local dropDown = self.buttons.modeDropDown
UIDropDownMenu_SetWidth(dropDown, 120)
UIDropDownMenu_JustifyText(dropDown, "LEFT")
UIDropDownMenu_Initialize(dropDown, Initializer)
end
local lastRefresh
function WeakAurasProfilingMixin:RefreshBars(_, force)
if force or (not lastRefresh or lastRefresh < GetTime() - 1) then
lastRefresh = GetTime()
else
return
end
if not profileData.systems.time then
return
end
local data
if self.mode == 1 then -- auras
data = profileData.auras
elseif self.mode == 2 then -- systems
data = {}
for k, v in pairs(profileData.systems) do
if k ~= "time" and k ~= "wa" then
local event = unitEventToMultiUnit(k)
if data[event] == nil then
data[event] = CopyTable(v)
else
if v.elapsed then
data[event].elapsed = (data[event].elapsed or 0) + v.elapsed
end
if v.spike then
data[event].spike = (data[event].spike or 0) + v.spike
end
end
end
end
end
local total = TotalProfileTime(data)
for _, name in ipairs(SortProfileMap(data)) do
if name ~= "time" and name ~= "wa" then
local elapsed = data[name].elapsed
local pct = 100 * elapsed / total
local spike = data[name].spike
self:UpdateBar(name, elapsed, pct, spike)
end
end
self.bars:Sort()
if profileData.systems.wa then
local timespent = profileData.systems.time.elapsed or (debugprofilestop() - profileData.systems.time.start)
self.stats:SetText(
("|cFFFFFFFFTime in WA: %.2fs / %ds (%.1f%%)"):format(profileData.systems.wa.elapsed / 1000, timespent / 1000, 100 * profileData.systems.wa.elapsed / timespent)
)
end
end
function WeakAurasProfilingMixin:ResetBars()
self.bars:Flush()
end
function WeakAurasProfilingMixin:UpdateBar(name, time, pct, spike)
local elementData = self.bars:FindElementDataByPredicate(function(elementData)
return elementData.name == name
end)
if elementData then
elementData.time = time
elementData.pct = pct
elementData.spike = spike
local button = WeakAurasProfilingFrame.ScrollBox:FindFrame(elementData)
if button then
button.time:SetText(("%.2fms"):format(time))
button.spike:SetText(ColoredSpike(spike))
button.progressBar:SetValue(pct)
end
else
self.bars:Insert({ name = name, time = time, pct = pct, spike = spike })
end
end
function WeakAurasProfilingMixin:SortByColumnIndex(index)
local previousField = self.sortField
self.sortField = COLUMN_INFO[index].attribute
if previousField == self.sortField then
self.sortDirection = not self.sortDirection
else
self.sortDirection = true
end
self.bars:SetSortComparator(function(lhs, rhs)
local field = self.sortField
local lhsF, rhsF = lhs[field] or 0, rhs[field] or 0
if type(lhsF) == "string" then
lhsF = lhsF:lower()
end
if type(rhsF) == "string" then
rhsF = rhsF:lower()
end
if lhsF == rhsF then
return lhs.name < rhs.name
end
if self.sortDirection then
return lhsF > rhsF
else
return lhsF < rhsF
end
end)
end
function WeakAurasProfilingMixin:UpdateButtons()
local b = self.buttons
if currentProfileState == "combat" or currentProfileState == "encounter" then
b.stop:SetText(L["Cancel"])
b.stop:Show()
b.start:Hide()
elseif currentProfileState == "profiling" then
b.stop:SetText(L["Stop"])
b.stop:Show()
b.start:Hide()
else
b.start:Show()
b.stop:Hide()
if profileData.systems.time then
b.report:Show()
end
end
end
function WeakAurasProfilingMixin:Start()
self:Show()
end
function WeakAurasProfilingMixin:Stop()
WeakAuras.StopProfile()
self:UpdateButtons()
self:ResetBars()
end
function WeakAurasProfilingMixin:Toggle()
if self:IsShown() then
self:Stop()
else
self:Start()
end
end
function WeakAurasProfilingColumnDisplay_OnClick(self, columnIndex)
self:GetParent():SortByColumnIndex(columnIndex)
end