Skip to content

Developer Stuff

syrifgit edited this page Nov 15, 2024 · 10 revisions

page to put things and stuff

Profiling Code

If you want generic plug-and-play, you need from below:

  • The macros
  • Updated ProfileCPU function: function Hekili:ProfileCPU( name, func )
  • Data storage function: function Hekili:AddProfileData( name, timeSpent )
  • Printer function: function Hekili:PrintProfileSummary()
  • Storage cleanup: function Hekili:ClearProfileData()

Anything else is optional

In-Game Macros

Print the profiles after combat

/run Hekili:PrintProfileSummary()
/run Hekili:PrintNextPredictionProfile()

Clear the data after printing

/run Hekili:ClearProfileData()
/run Hekili:ResetNextPredictionProfile()

Hekili.lua

Updated ProfileCPU function

This is the function that allows you to write stuff like this to have a function added to the profiler

  • Hekili:ProfileCPU( "QueueEvent", state.QueueEvent )
  • Hekili:ProfileCPU( "ThreadedUpdate", Hekili.Update )
  • Hekili:ProfileCPU( "GetNextPrediction", Hekili.GetNextPrediction )

Theoretically, you can add any function you want.

            local cpuProfileDB = {}
            Hekili.CPUProfileResults = {}
        
            function Hekili:ProfileCPU(name, func)
                -- Store the original function for reference
                print("Profiling function:", name)
                cpuProfileDB[name] = func
        
                -- Initialize profiling data if it doesn’t already exist
                if not self.CPUProfileResults[name] then
                    self.CPUProfileResults[name] = {
                        totalCPUTime = 0,
                        calls = 0,
                        maxCPU = 0,
                        minCPU = math.huge
                    }
                end
        
                -- Redefine the function with a profiling wrapper
                self[name] = function(...)
                    local startCPU = debugprofilestop()
                    local result = { func(...) }
                    local endCPU = debugprofilestop()
        
                    -- Calculate CPU time for this call
                    local cpuTime = endCPU - startCPU
        
                    -- Ensure profileData is available
                    local profileData = self.CPUProfileResults[name]
                    if profileData then
                        profileData.totalCPUTime = profileData.totalCPUTime + cpuTime
                        profileData.calls = profileData.calls + 1
                        profileData.maxCPU = math.max(profileData.maxCPU, cpuTime)
                        profileData.minCPU = math.min(profileData.minCPU, cpuTime)
                    else
                        self:Debug("Warning: profileData is nil for function " .. name)
                    end
        
                    -- Return the original function's results
                    return unpack(result)
                end
            end  

Hekili:AddProfileData( name, timeSpent ) - Function for storing data over time (live profiling), scalable

function Hekili:AddProfileData( name, timeSpent )
    self.ProfileResults = self.ProfileResults or {}

    if not self.ProfileResults[ name ] then
        self.ProfileResults[ name ] = { totalTime = 0, count = 0, max = 0, min = math.huge }
    end

    local result = self.ProfileResults[ name ]
    result.totalTime = result.totalTime + timeSpent
    result.count = result.count + 1
    result.max = math.max( result.max, timeSpent )
    result.min = math.min( result.min, timeSpent )
end

Hekili:PrintProfileSummary() - Print Function for the profiler

function Hekili:PrintProfileSummary()
    -- Ensure there is profiling data to print
    if not self.ProfileResults or next(self.ProfileResults) == nil then
        print("No profiling data recorded.")
        return
    end

    -- Prepare a list of keys sorted by total time in descending order
    local sortedEntries = {}
    for name, data in pairs(self.ProfileResults) do
        table.insert(sortedEntries, { name = name, data = data })
    end

    -- Sort by totalTime in descending order
    table.sort(sortedEntries, function(a, b)
        return a.data.totalTime > b.data.totalTime
    end)

    -- General profiling summary
    print("\nProfiling Summary:")
    print(string.format("%-25s | %12s | %8s | %10s | %10s | %10s", "Function Name", "Total Time", "Calls", "Avg Time", "Max Time", "Min Time"))
    print(string.rep("-", 85))

    for _, entry in ipairs(sortedEntries) do
        local name, data = entry.name, entry.data
        local avg = data.totalTime / data.count
        print(string.format("%-25s | %10.3f ms | %6d | %8.3f ms | %8.3f ms | %8.3f ms",
            name, data.totalTime, data.count, avg, data.max, data.min))
    end

    -- Specific section for entries containing "reset" or "Reset"
    print("\nReset Profiling Summary:")
    print(string.format("%-25s | %12s | %8s | %10s | %10s | %10s", "Reset Entry", "Total Time", "Calls", "Avg Time", "Max Time", "Min Time"))
    print(string.rep("-", 85))

    for _, entry in ipairs(sortedEntries) do
        local name, data = entry.name, entry.data
        if name:match("[rR]eset") then  -- Match any entry containing "reset" or "Reset"
            local avg = data.totalTime / data.count
            print(string.format("%-25s | %10.3f ms | %6d | %8.3f ms | %8.3f ms | %8.3f ms",
                name, data.totalTime, data.count, avg, data.max, data.min))
        end
    end

    -- Check for CPU profiling data
    if Hekili.CPUProfileResults then
        print("\nCPU Profile Summary:")

        -- Sort CPU profiling data by total CPU time in descending order
        local cpuSortedEntries = {}
        for funcName, data in pairs(Hekili.CPUProfileResults) do
            table.insert(cpuSortedEntries, { funcName = funcName, data = data })
        end

        table.sort(cpuSortedEntries, function(a, b)
            return a.data.totalCPUTime > b.data.totalCPUTime
        end)

        -- Print all entries where the call count is greater than 0
        for i = 1, #cpuSortedEntries do
            local entry = cpuSortedEntries[i]
            local funcName = entry.funcName
            local data = entry.data

            if data.calls > 0 then  -- Only print entries with calls > 0
                local avgCPU = data.totalCPUTime / data.calls

                -- Top row: function name
                print(string.format("Function: %-22s", funcName))

                -- Second row: table format for profiling data
                print(string.format("Calls | Total CPU Time |   Avg CPU   |   Max CPU   |   Min CPU"))
                print(string.format(" %6d | %14.3f ms | %9.3f ms | %9.3f ms | %9.3f ms",
                    data.calls, data.totalCPUTime, avgCPU, data.maxCPU, data.minCPU))
                print(string.rep("-", 60))  -- Separator line for better readability
            end
        end
    else
        print("No CPU profiling data recorded.")
    end

    -- Add engine performance data if available
    if Hekili.Engine and Hekili.Engine.threadUpdates then
        local engineData = Hekili.Engine.threadUpdates
        print("\nEngine Thread Performance Summary:")
        print(string.format("%-25s |     %-4s |     %-4s", "Metric", "Mean", "Peak"))
        print(string.rep("-", 55))  -- Reduced width for separator line
        print(string.format("%-18s | %7.3f ms | %7.3f ms", "Mean Clock Time", engineData.meanClockTime or 0, engineData.peakClockTime or 0))
        print(string.format("%-18s | %7.3f ms | %7.3f ms", "Mean Work Time", engineData.meanWorkTime or 0, engineData.peakWorkTime or 0))
        print(string.format("%-20s | %7.3f    | %7.3f", "Mean Frames", engineData.meanFrames or 0, engineData.peakFrames or 0))
        print(string.format("%-18s | %7.3f ms | %7.3f ms", "Total Wasted Time", engineData.totalWasted or 0, engineData.peakWasted or 0))
        print(string.format("%-14s | %7.3f ms", "Mean Wasted Time", engineData.meanWasted or 0))
    else
        print("No engine performance data recorded.")
    end
end

Hekili:ClearProfileData() - Clear data for print function

-- Function to clear all profiling data
function Hekili:ClearProfileData()
    -- Ensure the ProfileResults and CPUProfileResults tables exist
    self.ProfileResults = self.ProfileResults or {}
    self.CPUProfileResults = self.CPUProfileResults or {}

    -- Wipe the ProfileResults table
    wipe(self.ProfileResults)
    print("Profiling data cleared.")

    -- Wipe the CPUProfileResults table
    wipe(self.CPUProfileResults)
    print("CPU profiling data cleared.")

    -- Clear the engine performance data if available
    if Hekili.Engine and Hekili.Engine.threadUpdates then
        Hekili.Engine.threadUpdates = {
            meanClockTime  = 0,
            meanWorkTime   = 0,
            meanFrames     = 0,
            meanWasted     = 0,
            firstUpdate    = 0,
            updates        = 0,
            updatesPerSec  = 0,
            peakClockTime  = 0,
            peakWorkTime   = 0,
            peakFrames     = 0,
            peakWasted     = 0,
            totalWasted    = 0
        }
        print("Engine performance data cleared.")
    else
        print("No engine performance data to clear.")
    end
end

Scripts.lua

Updated CheckScript with profiling for individual APL entries

function scripts:CheckScript(scriptID, action, elem)
    -- Capture the action we're checking.
    local prev_action = state.this_action
    if action then state.this_action = action end

    -- Retrieve the script from the database.
    local script = self.DB[scriptID]

    if not script then
        state.this_action = prev_action
        return false
    end

    -- Profiling for the entire CheckScript function.
    local startCheckScript
    if InCombatLockdown() then
        startCheckScript = debugprofilestop()
    end

    -- Initialize the result variables.
    local result, errorMessage

    if not elem then
        -- Profiling for general Conditions.
        local startConditions, timeSpentConditions
        if InCombatLockdown() then
            startConditions = debugprofilestop()
        end

        if script.Error then
            result = false
            errorMessage = script.Error

        elseif not script.Conditions then
            result = true

        else
            -- Error handling for Conditions to catch potential issues.
            local success, conditionsResult = pcall(script.Conditions)
            if success then
                result = conditionsResult
            else
                result = false
                errorMessage = "Error in Conditions: " .. conditionsResult
            end
        end

        -- Record time spent on Conditions evaluation.
        if InCombatLockdown() and startConditions then
            timeSpentConditions = debugprofilestop() - startConditions
            Hekili:AddProfileData("CheckScript:Conditions:" .. scriptID, timeSpentConditions)
        end

    else
        -- Profiling for Modifier checks.
        local startModifier, timeSpentModifier
        if InCombatLockdown() then
            startModifier = debugprofilestop()
        end

        if not script.Modifiers[elem] then
            result = nil
            errorMessage = elem .. " not set."

        else
            local success, value = pcall(script.Modifiers[elem])
            if success then
                result = value
            else
                result = false
                errorMessage = "Error in Modifier " .. elem .. ": " .. value
            end
        end

        -- Record time spent on the Modifier evaluation.
        if InCombatLockdown() and startModifier then
            timeSpentModifier = debugprofilestop() - startModifier
            Hekili:AddProfileData("CheckScript:Modifier:" .. scriptID .. ":" .. elem, timeSpentModifier)
        end
    end

    -- Restore the previous action state.
    state.this_action = prev_action

    -- Record the total time for CheckScript if applicable.
    if InCombatLockdown() and startCheckScript then
        local timeSpentTotal = debugprofilestop() - startCheckScript
        Hekili:AddProfileData("CheckScript:Total:" .. scriptID, timeSpentTotal)
    end

    -- Return the result with potential error message.
    return result, errorMessage
end

GetModifiers with profiling

function scripts:GetModifiers(scriptID, out)
    print("GetModifiers called for scriptID:", scriptID)  -- Debug print
    -- Start profiling for the whole GetModifiers function
    local startTotal
    if InCombatLockdown() then
        print("In combat, starting profiling for GetModifiers")  -- Debug print
        startTotal = debugprofilestop()
    end

    out = out or {}
    local script = self.DB[scriptID]

    if not script then
        print("No script found for scriptID:", scriptID)  -- Debug print
        -- End profiling if exiting early
        if InCombatLockdown() and startTotal then
            local endTotal = debugprofilestop() - startTotal
            Hekili:AddProfileData("GetModifiers:" .. scriptID, endTotal)
        end
        return out
    end

    for k, v in pairs(script.Modifiers) do
        local start, timeSpent
        if InCombatLockdown() then
            start = debugprofilestop()
            print("Profiling modifier:", k)  -- Debug print
        end
        
        local success, value = pcall(v)
        
        if success then
            out[k] = value
        else
            print("Failed to execute modifier:", k)  -- Debug print for failure
        end

        -- Record profiling data for each modifier
        if InCombatLockdown() and start then
            timeSpent = debugprofilestop() - start
            print("Time spent on modifier:", k, "=", timeSpent)  -- Debug print
            Hekili:AddProfileData("Modifier:" .. k, timeSpent)
        end
    end

    -- End profiling for the entire function
    if InCombatLockdown() and startTotal then
        local endTotal = debugprofilestop() - startTotal
        Hekili:AddProfileData("GetModifiers:" .. scriptID, endTotal)
        print("Total time spent on GetModifiers:", endTotal)  -- Debug print
    end

    return out
end

GetConditionsAndValues with profiling

    function scripts:GetConditionsAndValues(scriptID, listName, actID, recheck)
        -- Skip if snapshot or debug is not enabled
        if troubleshootingSnapshotTimes or not Hekili.ActiveDebug then return "[no data]" end
    
        -- Adjust scriptID if listName and actID are provided
        if listName and actID then
            scriptID = scriptID .. ":" .. listName .. ":" .. actID
        end
    
        -- Start profiling for the whole GetConditionsAndValues function
        local startTotal = debugprofilestop()
        
        -- Retrieve the script from the database
        local script = self.DB[scriptID]
        
        -- Check if this is a recheck and profile accordingly
        if recheck then
            local startRecheck = debugprofilestop()
            local result = embedConditionsAndValues(script.RecheckScript, script.RecheckElements)
            local endRecheck = debugprofilestop() - startRecheck
            Hekili:AddProfileData("RecheckConditions:" .. scriptID, endRecheck)
            
            -- End profiling for total function and return
            local endTotal = debugprofilestop() - startTotal
            Hekili:AddProfileData("GetConditionsAndValues:" .. scriptID, endTotal)
            
            return result
        end
    
        -- Profile if there is a custom Print function
        if script.Print then
            local startPrint = debugprofilestop()
            local result = script.Print()
            local endPrint = debugprofilestop() - startPrint
            Hekili:AddProfileData("PrintFunction:" .. scriptID, endPrint)
            
            -- End profiling for total function and return
            local endTotal = debugprofilestop() - startTotal
            Hekili:AddProfileData("GetConditionsAndValues:" .. scriptID, endTotal)
            
            return result
        end
    
        -- Profile standard condition evaluation with embedConditionsAndValues
        local startEmbed = debugprofilestop()
        local result = embedConditionsAndValues(script.SimC, script.Elements)
        local endEmbed = debugprofilestop() - startEmbed
        Hekili:AddProfileData("ConditionEvaluation:" .. scriptID, endEmbed)
    
        -- End profiling for total function
        local endTotal = debugprofilestop() - startTotal
        Hekili:AddProfileData("GetConditionsAndValues:" .. scriptID, endTotal)
        
        return result
    end

Projects

Track refreshable status of debuffs in real-time across all enemies

Has a lot of potential, needs work.

Targets.lua

ns.refreshableAuraCount = function(auraName)
    local startTime = debugprofilestop()  -- Start timing

    local aura = class.auras[auraName]
    if not aura then 
        return 0 
    end

    local auraID = aura.id
    -- Handle dynamic duration (if duration is a function, evaluate it)
    local duration = type(aura.duration) == "function" and aura.duration() or aura.duration or 30
    local refreshThreshold = 0.3 * duration  -- Calculate threshold as 30% of the duration
    local refreshableCount = 0

    -- Iterate over each nameplate unit (up to 40)
    for i = 1, 40 do
        local unit = "nameplate" .. i
        if not UnitExists(unit) then break end -- Stop if no more nameplates are present

        local auraFound = false

        -- Check each debuff slot on the unit to find the specific aura
        for j = 1, 40 do
            local auraData = C_UnitAuras.GetAuraDataBySlot(unit, j)
            
            if auraData and auraData.spellId == auraID then
                auraFound = true
                local currentTime = GetTime()
                local expirationTime = auraData.expirationTime or 0
                local remainingTime = expirationTime - currentTime

                if remainingTime <= refreshThreshold then
                    refreshableCount = refreshableCount + 1
                end
                break
            end
        end

        if not auraFound then
            -- If the aura is not found on the unit, consider it refreshable.
            refreshableCount = refreshableCount + 1
        end
    end

    -- Calculate elapsed time and add it to the profiling data
    local timeSpent = debugprofilestop() - startTime
    Hekili:AddProfileData("Refreshable", timeSpent)

    return refreshableCount
end

SPECFILE.lua

-- Count of refreshable Garrotes
spec:RegisterStateExpr( "refreshable_garrotes", function ()
    return ns.refreshableAuraCount("garrote")
end )

-- Count of refreshable Ruptures
spec:RegisterStateExpr( "refreshable_ruptures", function ()
    return ns.refreshableAuraCount("rupture")
end )