-- Counts calls and measures time per function through a call/return hook.
local os = require("@neyuki/os")
local table = require("@neyuki/table")
local profile = {}
local stats = {}
local stack = {}
local running = false
local function tracer(event: string)
local info = debug.getinfo(2)
if info == nil then
return
end
if info.func == profile.start or info.func == profile.stop or info.func == profile.isRunning then
return
end
if event == "call" or event == "tail call" then
local name = info.name ?? "main chunk"
local entry = stats[name]
if entry == nil then
entry = { calls = 0, time = 0 }
stats[name] = entry
end
entry.calls = entry.calls + 1
stack[#stack + 1] = { name = name, started = os.clock() }
elseif event == "return" and #stack > 0 then
local top = table.remove(stack)
local entry = stats[top.name]
entry.time = entry.time + (os.clock() - top.started)
end
end
-- Starts profiling, discarding earlier results.
function profile.start()
stats = {}
stack = {}
running = true
debug.sethook(tracer, "cr")
end
-- Stops profiling and returns one `name: calls=N, time=Ts` line per
-- function, slowest first.
function profile.stop(): string
debug.sethook()
running = false
stack = {}
local names = {}
for name, _ in stats do
names[#names + 1] = name
end
table.sort(names, function(a, b) return stats[a].time > stats[b].time end)
local lines = {}
for _, name in names do
local entry = stats[name]
lines[#lines + 1] = name .. ": calls=" .. tostring(entry.calls) .. ", time=" .. tostring(entry.time) .. "s"
end
return table.concat(lines, "\n")
end
function profile.isRunning(): boolean
return running
end
return table.freeze(profile)