1
0
mirror of https://github.com/ComfyFactory/ComfyFactorio.git synced 2024-12-28 23:06:38 +02:00
ComfyFactorio/utils/profiler.lua
2024-06-01 22:10:03 +02:00

268 lines
8.3 KiB
Lua

if (debug.sethook) then
local table_sort = table.sort
local string_rep = string.rep
local string_format = string.format
local debug_getinfo = debug.getinfo
local Color = require 'utils.color_presets'
local Task = require 'utils.task'
local Token = require 'utils.token'
local Event = require 'utils.event'
local Public = {
call_tree = nil,
is_running = false
}
local stop_profiler_token =
Token.register(
function()
Public.stop()
game.print('[PROFILER] Stopped!')
log('[PROFILER] Stopped!')
end
)
-- we can have this on runtime,
-- but never ever can a player run this without notifying us.
local allowed = {
['Gerkiz'] = true,
['mewmew'] = true
}
local ignored_functions = {
[debug.sethook] = true
}
local named_sources = {
['[string "local n, v = "serpent", "0.30" -- (C) 2012-17..."]'] = 'serpent'
}
local function start_command(command)
local player = game.player
if player then
if player ~= nil then
if not player.admin then
local p = player.print
p('[ERROR] Only admins are allowed to run this command!', Color.fail)
return
else
if allowed[player.name] then
Public.start(command.parameter ~= nil)
elseif _DEBUG then
Public.start(command.parameter ~= nil)
end
end
end
end
end
local function stop_command(command)
local player = game.player
if player then
if player ~= nil then
if not player.admin then
local p = player.print
p('[ERROR] Only admins are allowed to run this command!', Color.fail)
return
else
if allowed[player.name] then
Public.stop(command.parameter ~= nil, nil)
elseif _DEBUG then
Public.stop(command.parameter ~= nil, nil)
end
end
end
end
end
ignored_functions[start_command] = true
ignored_functions[stop_command] = true
commands.add_command('start_profiler', 'Starts profiling', start_command)
commands.add_command('stop_profiler', 'Stops profiling', stop_command)
--local assert_raw = assert
--function assert(expr, ...)
-- if not expr then
-- Public.stop(false, "Assertion failed")
-- end
-- assert_raw(expr, ...)
--end
local error_raw = error
--luacheck: ignore error
function error(...)
Public.stop(false, 'Error raised')
error_raw(...)
end
function Public.start(exclude_called_ms)
if Public.is_running then
return
end
local create_profiler = game.create_profiler
Public.is_running = true
Public.call_tree = {
name = 'root',
calls = 0,
profiler = create_profiler(),
next = {}
}
-- Array of Call
local stack = {[0] = Public.call_tree}
local stack_count = 0
debug.sethook(
function(event)
local info = debug_getinfo(2, 'nSf')
if ignored_functions[info.func] then
return
end
if event == 'call' or event == 'tail call' then
local prev_call = stack[stack_count]
if exclude_called_ms and prev_call then
prev_call.profiler.stop()
end
local what = info.what
local name
if what == 'C' then
name = string_format('C function %q', info.name or 'anonymous')
else
local source = info.short_src
local namedSource = named_sources[source]
if namedSource ~= nil then
source = namedSource
elseif string.sub(source, 1, 1) == '@' then
source = string.sub(source, 1)
end
name = string_format('%q in %q, line %d', info.name or 'anonymous', source, info.linedefined)
end
local prev_call_next = prev_call.next
if prev_call_next == nil then
prev_call_next = {}
prev_call.next = prev_call_next
end
local currCall = prev_call_next[name]
local profilerStartFunc
if currCall == nil then
local prof = create_profiler()
currCall = {
name = name,
calls = 1,
profiler = prof
}
prev_call_next[name] = currCall
profilerStartFunc = prof.reset
else
currCall.calls = currCall.calls + 1
profilerStartFunc = currCall.profiler.restart
end
stack_count = stack_count + 1
stack[stack_count] = currCall
profilerStartFunc()
end
if event == 'return' or event == 'tail call' then
if stack_count > 0 then
stack[stack_count].profiler.stop()
stack[stack_count] = nil
stack_count = stack_count - 1
if exclude_called_ms then
stack[stack_count].profiler.restart()
end
end
end
end,
'cr'
)
end
ignored_functions[Public.start] = true
local function dump_tree(averageMs)
local function sort_Call(a, b)
return a.calls > b.calls
end
local fullStr = {''}
local str = fullStr
local line = 1
local function recurse(curr, depth)
local sort = {}
local i = 1
for k, v in pairs(curr) do
sort[i] = v
i = i + 1
end
table_sort(sort, sort_Call)
for ii = 1, #sort do
local call = sort[ii]
if line >= 19 then --Localised string can only have up to 20 parameters
local newStr = {''} --So nest them!
str[line + 1] = newStr
str = newStr
line = 1
end
if averageMs then
call.profiler.divide(call.calls)
end
str[line + 1] = string_format('\n%s%dx %s. %s ', string_rep('\t', depth), call.calls, call.name, averageMs and 'Average' or 'Total')
str[line + 2] = call.profiler
line = line + 2
local next = call.next
if next ~= nil then
recurse(next, depth + 1)
end
end
end
if Public.call_tree.next ~= nil then
recurse(Public.call_tree.next, 0)
return fullStr
end
return 'No calls'
end
function Public.stop(averageMs, message)
if not Public.is_running then
return
end
debug.sethook()
local text = {'', '\n\n----------PROFILER DUMP----------\n', dump_tree(averageMs), '\n\n----------PROFILER STOPPED----------\n'}
if message ~= nil then
text[#text + 1] = string.format('Reason: %s\n', message)
end
log(text)
Public.call_tree = nil
Public.is_running = false
end
ignored_functions[Public.stop] = true
if _PROFILE then
Event.on_init(
function()
game.print('[PROFILER] Started!')
log('[PROFILER] Started!')
Public.start()
Task.set_timeout_in_ticks(3600, stop_profiler_token)
end
)
end
return Public
end