vanilla-wow-addons – Rev 1

Subversion Repositories:
Rev:

local mod = klhtm
local me = { }
mod.diag = me

--[[
KTM_Diagnostic.lua

This module monitors computer performance and how it is affected by the rest of the mod. It is useful to check for
methods that are using excessive amounts of memory or processor time.

Whenever the KTM_Core.lua is about to call a module's .onevent() or .onupdate() methods, it instead sends the method to mod.diag.logmethodcall(module, methodtype). The time taken in milliseconds and memory used in kilobytes is recorded. For each module that has a .onevent() or .onupdate() method, a separate entry is kept, which records the total value,average rate, maximum rate over 5 seconds and rate in the last 5 seconds.

To print out the memory data, run the command "/ktm test memory"; for timing data, "/ktm test time".

Times are recorded in one second intervals. So whenever a method is called, the .current value of the relevant dataset is incremented. Then every second, there is a collation, where .current is added to .history, and .total and .recordinterval are updated. This is done in me.onupdate().

]]


me.lastcollation = GetTime() -- value of GetTime(). Happens once a second.
me.datalogstart = GetTime() -- when we started logging times. For the "average rate" value.

--[[
timing / memory data set. Each module-method combination has a separate set. e.g. there is one for the .onevent() function of the .combat module.
]]
me.createnewdataset = function()

        return
        {
                ["total"] = 0,
                ["history"] = {0, 0, 0, 0, 0},  -- values for the last 5 seconds
                ["historylength"] = 5,
                ["current"] = 0,                                -- working value for the current second
                ["recordinterval"] = 0,                 -- maximum sum of .history
        }

end

--[[
All the data we keep is in this table. At startup, we can only be sure the "total" sets will exist - we don't know which modules have a .onupdate or .onevent method. As soon as those methods are called for the first time, we will add a data set for them.
]]
me.data = 
{
        ["memory"] =
        {
                ["onupdate"] = 
                {
                        ["total"] = me.createnewdataset(),
                },
                ["onevent"] = 
                {
                        ["total"] = me.createnewdataset(),
                },
                ["total"] = me.createnewdataset(),
        },
        ["time"] =
        {
                ["onupdate"] = 
                {
                        ["total"] = me.createnewdataset(),
                },
                ["onevent"] = 
                {
                        ["total"] = me.createnewdataset(),
                },
                ["total"] = me.createnewdataset(),
        }
}

--[[
mod.diag.logmethodcall(module, calltype)
Runs the special Core.lua methods, and logs the timing and memory usage.
<module> is a string, the key for a module, e.g. "combat", "string", "netin", etc.
<calltype> is either "onupdate" or "onevent".
]]
me.logmethodcall = function(module, calltype)

        local method = mod[module][calltype]
        local memory
        local time
        
        local manualstart = GetTime()
        memory, time = me.getfunctionstats(method)
        local manualtime = math.floor(0.5 + (GetTime() - manualstart) * 1000)
        
        -- Scrub out bullshit values! > 100ms = bs, > 10 ms = should be traced. < 0 kb = bs (gc).
        -- Sometimes debugprofile gives completely spasticated values for unknown reasons. Also we might get a negative
        -- value for memory if a garbage collection occurs.
        if time > 100 or time < 0 or math.abs(time - manualtime) > 5 then
                time = 0
        elseif time > 10 then
                if mod.out.checktrace("info", me, "timing") then
                        mod.out.printtrace(string.format("Time from %s %s is %s ms (manual says %s ms) (memory was %s).", module, calltype, me.formatdecimal(time), manualtime, memory))
                end
        end
        
        if memory < 0 then
                memory = 0
        end
        
        me.adddatapoint("time", calltype, module, time)
        me.adddatapoint("memory", calltype, module, memory)
        
end

--[[
me.getfunctionstats(method)
Runs the the function <method>, and records how much time was taken, and memory used.
Returns: <memory, in whole kilobytes>, <time, in fractional milliseconds>.
]]
me.getfunctionstats = function(method) 

        local memorystart = gcinfo()
        local timestart = debugprofilestop()
        
        method()
        
        local timetaken = (debugprofilestop() - timestart) 
        local memoryused = gcinfo() - memorystart
        
        return memoryused, timetaken
end


--[[
me.adddatapoint(datatype, calltype, module, value)
Adds <value> to the .current property of the relevant dataset. Also adds it to any parent sets, e.g. "total".
<datatype> is "memory" or "time"
<calltype> is "onupdate" or "onevent"
<module> is "combat", "string", "data", etc.
]]
me.adddatapoint = function(datatype, calltype, module, value)

        if me.data[datatype][calltype][module] == nil then
                me.data[datatype][calltype][module] = me.createnewdataset()
        end
        
        me.data[datatype][calltype][module].current = me.data[datatype][calltype][module].current + value
        me.data[datatype][calltype].total.current = me.data[datatype][calltype].total.current + value
        me.data[datatype].total.current = me.data[datatype].total.current + value

end

--[[
Special Core.lua method. If this "second" has ended (i.e. it's been at least 1 second since the last collation),
then run a collation.
]]
me.onupdate = function()

        local timenow = GetTime()
        
        -- update at most once a second
        if timenow < me.lastcollation + 1.0 then
                return
        end

        me.collatetable(me.data, timenow - me.lastcollation)
        me.lastcollation = timenow
end

--[[
me.collatetable(data, period)
Collating a dataset is to finalise the ".current" value, add it to history, update total, etc.
This is a recursive method. <data> might be a data set (base case), or it might be a tree containing datasets (note the
structure of me.data). If it is a tree, then me.collatetable will be called on any subtrees inside it.
<period> = time in seconds this collation is over. It will be at least 1, and close to 1, but not exactly 1. Since we want
to record the rate, e.g. KB/sec, we have to divide by this value.
]]
me.collatetable = function(data, period)

        if data.current == nil then
                -- this guy is a table, with subtables. do the subtables.
                
                local value
                for _, value in data do
                        me.collatetable(value, period)
                end
                
                return
        end

        -- this guy is an actual data set       
        local x
        local interval = 0
        
        for x = data.historylength - 1, 1, -1 do
                data.history[x + 1] = data.history[x]
                interval = interval + data.history[x]
        end
        
        -- take into account period length (we want / second)
        data.history[1] = data.current / period
        interval = interval + data.history[1]
        data.total = data.total + data.current
        
        -- check for new record 5 second burst
        if interval > data.recordinterval then
                data.recordinterval = interval
        end
        
        -- reset the current value, it's a new second
        data.current = 0

end

--[[
mod.diag.printdataset(datatype, units)
This is called when a console command "/ktm test time" or "/ktm test memory" is run.
<datatype> is "time" or "memory"
<datatype> is a description of the units, e.g. "Milliseconds per Second".
]]
me.printalldata = function(datatype, units)

        -- print total, then subcategories, recursively.
        mod.out.print(string.format("|cff6666ffThis is a listing of |cffffff00%s |cff6666ffusage. Units are |cffffff00%s.|r", datatype, units))
        
        local data = me.data[datatype]
        local key
        local value
        
        -- grand total
        me.printdataset("|cffff3333Total", data.total)
        
        -- onupdates
        me.printdataset("|cff66ff66OnUpdates", data.onupdate.total)
        
        for key, value in data.onupdate do
                if key ~= "total" then
                        me.printdataset("|cffffff00" .. key, value)
                end
        end
        
        -- onevents
        me.printdataset("|cff66ff66OnEvents", data.onevent.total)
        
        for key, value in data.onevent do
                if key ~= "total" then
                        me.printdataset("|cffffff00" .. key, value)
                end
        end
        
end

--[[
me.printdataset(name, set)
This prints the values of an individual data set.
<name> is the name of the module or category, with colouring.
<set> is the actual data set.
]]
me.printdataset = function(name, set)

        local x
        local last5 = 0
        
        for x = 1, 5 do
                last5 = last5 + set.history[x]
        end
        
        mod.out.print(string.format("%s:|r Total = %s, Avg = %s, Burst = %s, Recent = %s.", 
                name, me.formatdecimal(set.total), me.formatdecimal(set.total / (GetTime() - me.datalogstart)), me.formatdecimal(set.recordinterval / 5), me.formatdecimal(last5 / 5)))

end

--[[
me.formatdecimal(value)
Returns a string representation of <value>, including up the the first place after the decimal, if it exists.
]]
me.formatdecimal = function(value)

        if floor(value) == value then
                return string.format("%d", value)

        else
                local base = string.format("%f", value)
                local dotpoint = string.find(base, "%.")
                return string.sub(base, 1, dotpoint + 1)
        end
end