vanilla-wow-addons – Rev 1
?pathlinks?

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