dobrograd-13-06-2022/garrysmod/addons/util-fprofiler/lua/fprofiler/gather.lua
Jonny_Bro (Nikita) e4d5311906 first commit
2023-11-16 15:01:19 +05:00

290 lines
8.8 KiB
Lua

local timeMeasurementFunc = SysTime
-- Helper function, created by some ancient Lua dev
-- Retrieves the local variables and their values of a function
local function getupvalues(f)
local t, i, k, v = {}, 1, debug.getupvalue(f, 1)
while k do
t[k] = v
i = i + 1
k,v = debug.getupvalue(f, i)
end
return t
end
-- Helper function
-- Get all local variables
local NIL = {}
setmetatable(NIL, {__tostring = function() return "nil" end})
local function getlocals(level)
local i = 1
local name, value
local vars = {}
while true do
name, value = debug.getlocal(level, i)
if not name then break end
value = value == nil and NIL or value
vars[name] = value
i = i + 1
end
return vars
end
--[[-------------------------------------------------------------------------
Call counts:
registers how often function have been called
---------------------------------------------------------------------------]]
local callcounts = {}
-- Gets the call counts
FProfiler.Internal.getCallCounts = function() return callcounts end
-- Resets the call counts
function FProfiler.Internal.resetCallCounts()
callcounts = {}
end
--[[-------------------------------------------------------------------------
Inclusive function times
Keeps track of how long functions take in total
i.e. average time between the start and return of a function * times called
This includes the time that any function called by this function takes
(that's what the "inclusive" refers to).
Note: recursive calls are not counted double
---------------------------------------------------------------------------]]
local inclusiveTimes = {}
-- Gets the inclusive times
FProfiler.Internal.getInclusiveTimes = function() return inclusiveTimes end
-- Resets the inclusive times
function FProfiler.Internal.resetInclusiveTimes()
inclusiveTimes = {}
end
--[[-------------------------------------------------------------------------
Top n most expensive single function calls
Keeps track of the functions that took the longest time to run
Note: functions can appear in this list at most once
---------------------------------------------------------------------------]]
local mostExpensiveSingleCalls = {}
-- Gets most expensive single calls
FProfiler.Internal.getMostExpensiveSingleCalls = function() return mostExpensiveSingleCalls end
-- Dictionary to make sure the same function doesn't appear multiple times
-- in the top n
local mostExpensiveSingleDict = {}
function FProfiler.Internal.resetMostExpensiveSingleCalls()
for i = 1, 50 do mostExpensiveSingleCalls[i] = {runtime = 0} end
mostExpensiveSingleDict = {}
end
-- Initial empty list
FProfiler.Internal.resetMostExpensiveSingleCalls()
--[[-------------------------------------------------------------------------
Function information
Using debug.getinfo on a function object won't give you any function names
that's because functions can have multiple names.
Luckily, when the functions are called, debug.getinfo(level) gives the
function name and scope
---------------------------------------------------------------------------]]
local functionNames = {}
FProfiler.Internal.getFunctionNames = function() return functionNames end
--[[-------------------------------------------------------------------------
Recursion depth
Used internally to make sure recursive functions' times aren't counted
multiple times
---------------------------------------------------------------------------]]
local recursiveCount = {}
--[[-------------------------------------------------------------------------
Function start times
Used internally to keep track of when functions were called
---------------------------------------------------------------------------]]
local startTimes = {}
--[[-------------------------------------------------------------------------
Lua code event handlers
---------------------------------------------------------------------------]]
-- The recursion depth of the function that is in focus.
-- Only applies when profiling a specific function (i.e. laying focus upon)
local focusDepth = 0
-- Called when a function in the code is called
local function registerFunctionCall(funcInfo)
local func = funcInfo.func
-- Update call counts
callcounts[func] = (callcounts[func] or 0) + 1
-- Increase recursion depth for this function
recursiveCount[func] = (recursiveCount[func] or 0) + 1
-- Store function info
local funcname = funcInfo.name or ""
functionNames[func] = functionNames[func] or {}
functionNames[func][funcname] = functionNames[func][funcname] or
{ namewhat = funcInfo.namewhat,
nparams = funcInfo.nparams
}
local time = timeMeasurementFunc()
-- Update inclusive function times,
-- only when we're on the first recursive call
if recursiveCount[func] == 1 then
startTimes[func] = time
end
end
-- Called when a function returns
local function registerReturn(funcInfo)
local time = timeMeasurementFunc()
local func = funcInfo.func
local runtime = time - startTimes[func]
-- Update inclusive function time
-- Only update on the topmost call, to prevent recursive
-- calls for being counted multiple times.
if recursiveCount[func] == 1 then
inclusiveTimes[func] = (inclusiveTimes[func] or 0) + runtime
end
-- Maintain recursion depth
recursiveCount[func] = recursiveCount[func] - 1
-- Update top n list
-- This path will be taken most often: the function isn't special
-- Also only counts the top recursion
if runtime <= mostExpensiveSingleCalls[50].runtime or recursiveCount[func] > 1 then return end
-- If the function already appears in the top 10, replace it or discard the result
if mostExpensiveSingleDict[func] then
local i = mostExpensiveSingleDict[func]
-- Discard this info
if runtime < mostExpensiveSingleCalls[i].runtime then return end
-- Update the entry
mostExpensiveSingleCalls[i].runtime = runtime
mostExpensiveSingleCalls[i].upvalues = getupvalues(func)
mostExpensiveSingleCalls[i].locals = getlocals(5)
mostExpensiveSingleCalls[i].info = funcInfo
mostExpensiveSingleCalls[i].func = func
-- Move the updated entry up the top 10 list if applicable
while i > 1 and runtime > mostExpensiveSingleCalls[i - 1].runtime do
mostExpensiveSingleDict[mostExpensiveSingleCalls[i - 1].func] = i
mostExpensiveSingleCalls[i - 1], mostExpensiveSingleCalls[i] = mostExpensiveSingleCalls[i], mostExpensiveSingleCalls[i - 1]
i = i - 1
end
mostExpensiveSingleDict[func] = i
return
end
-- Knowing that the function belongs in the top n, find its position
local i = 50
while i >= 1 and runtime > mostExpensiveSingleCalls[i].runtime do
-- Update the dictionary
-- All functions faster than the current one move down the list
if not mostExpensiveSingleCalls[i].func then i = i - 1 continue end
mostExpensiveSingleDict[mostExpensiveSingleCalls[i].func] = i + 1
i = i - 1
end
-- Insert the expensive call in the top n
mostExpensiveSingleDict[func] = i + 1
table.insert(mostExpensiveSingleCalls, i + 1,
{
func = func,
runtime = runtime,
info = funcInfo,
upvalues = getupvalues(func),
locals = getlocals(5)
})
-- What was previously the 50th most expensive function
-- is now kicked out of the top 10
if mostExpensiveSingleCalls[51].func then
mostExpensiveSingleDict[mostExpensiveSingleCalls[51].func] = nil
end
mostExpensiveSingleCalls[51] = nil
end
-- Called on any Lua event
local function onLuaEvent(event, focus)
local info = debug.getinfo(3)
local func = info.func
if event == "call" or event == "tail call" then
-- Only track the focussed function and the functions
-- called by the focussed function
if focus == func then focusDepth = focusDepth + 1 end
if focus and focusDepth == 0 then return end
registerFunctionCall(info)
else
-- Functions that return right after the call to FProfiler.Internal.start()
-- are not to be counted
if not recursiveCount[func] or recursiveCount[func] == 0 then return end
if focus == func then focusDepth = focusDepth - 1 end
if focus and focusDepth == 0 then return end
registerReturn(info)
end
end
--[[-------------------------------------------------------------------------
Profiling control
---------------------------------------------------------------------------]]
-- Start profiling
-- focus: only measure data of everything that happens within a certain function
function FProfiler.Internal.start(focus)
-- Empty start times, so unfinished functions aren't
-- registered as returns on a second profiling session
-- local time = SysTime()
-- for k,v in pairs(startTimes) do startTimes[k] = time end
table.Empty(startTimes)
table.Empty(recursiveCount)
debug.sethook(function(event) onLuaEvent(event, focus) end, "cr")
end
-- Stop profiling
function FProfiler.Internal.stop()
debug.sethook()
end
-- Reset all profiling data
function FProfiler.Internal.reset()
FProfiler.Internal.resetCallCounts()
FProfiler.Internal.resetInclusiveTimes()
FProfiler.Internal.resetMostExpensiveSingleCalls()
end