Skip to content

Instantly share code, notes, and snippets.

@josefnpat
Last active February 27, 2019 23:37
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save josefnpat/72b4c0985964ef6505b0b92534a8bb0f to your computer and use it in GitHub Desktop.
Save josefnpat/72b4c0985964ef6505b0b92534a8bb0f to your computer and use it in GitHub Desktop.
Callgrind module for Lua 5.1 / LuaJIT
--[[
Copyright 2007 Jan Kneschke (jan@kneschke.de)
Copyright 2019 Josef Patoprsty (josefnpat@gmail.com)
Licensed under the same license as Lua 5.1
This is an alteration of the original lua-callgrind by Jan Kneschke.
Features:
* Module like system (.start(),.stop()) which is better suited update loops
* Buffered IO object for smaller projects
* Custom filenames
* Overwriting prevention by appending an index to the file name
* Stop start/stop error prevention
Usage example:
-- This will write a callgrind stack file to `callgrind.update` so you can
-- figure out how badly foo is running.
local callgrind = require"callgrind"
functionn love.draw()
callgrind.start("callgrind.update")
foo()
callgrind.stop()
end
--]]
local callstack
local instr_count
local last_line_instr_count
local current_file
local mainfunc
local functions
local methods
local method_id
local call_indent
local tracefile
local started
local function getfuncname(f)
return ("%s"):format(tostring(f.func))
end
local function trace(class)
-- print("calling tracer: "..class)
if class == "count" then
instr_count = instr_count + 1
elseif class == "line" then
-- check if we know this function already
local f = debug.getinfo(2, "lSf")
if not functions[f.func] then
functions[f.func] = {
meta = f,
lines = { }
}
end
local lines = functions[f.func].lines
lines[#lines + 1] =("%d %d"):format(f.currentline, instr_count - last_line_instr_count)
functions[f.func].last_line = f.currentline
if not mainfunc then mainfunc = f.func end
last_line_instr_count = instr_count
elseif class == "call" then
-- add the function info to the stack
--
local f = debug.getinfo(2, "lSfn")
callstack[#callstack + 1] = {
short_src = f.short_src,
func = f.func,
linedefined = f.linedefined,
name = f.name,
instr_count = instr_count
}
if not functions[f.func] then
functions[f.func] = {
meta = f,
lines = { }
}
end
if not functions[f.func].meta.name then
functions[f.func].meta.name = f.name
end
-- is this method already known ?
if f.name then
methods[tostring(f.func)] = { name = f.name }
end
-- print((" "):rep(call_indent)..">>"..tostring(f.func).." (".. tostring(f.name)..")")
call_indent = call_indent + 1
elseif class == "return" then
if #callstack > 0 then
-- pop the function from the stack and
-- add the instr-count to the its caller
local ret = table.remove(callstack)
local f = debug.getinfo(2, "lSfn")
-- if lua wants to return from a pcall() after a assert(),
-- error() or runtime-error we have to cleanup our stack
if ret.func ~= f.func then
-- print("handling error()")
-- the error() is already removed
-- removed every thing up to pcall()
-- print("looking for:",f.func)
-- print("stack:")
-- for i,v in pairs(callstack) do
-- print(i,v.func)
-- end
while callstack[#callstack].func ~= f.func do
table.remove(callstack)
call_indent = call_indent - 1
end
-- remove the pcall() too
ret = table.remove(callstack)
call_indent = call_indent - 1
end
local prev
if #callstack > 0 then
prev = callstack[#callstack].func
else
prev = mainfunc
end
local lines = functions[prev].lines
local last_line = functions[prev].last_line
call_indent = call_indent - 1
-- in case the assert below fails, enable this print and the one in the "call" handling
-- print((" "):rep(call_indent).."<<"..tostring(ret.func).." "..tostring(f.func).. " =? " .. tostring(f.func == ret.func))
assert(ret.func == f.func)
lines[#lines + 1] = ("cfl=%s"):format(ret.short_src)
lines[#lines + 1] = ("cfn=%s"):format(tostring(ret.func))
lines[#lines + 1] = ("calls=1 %d"):format(ret.linedefined)
lines[#lines + 1] = ("%d %d"):format(last_line and last_line or -1, instr_count - ret.instr_count)
end
-- tracefile:write("# --callstack: " .. #callstack .. "\n")
else
-- print("class = " .. class)
end
end
-- build a fake io object so that we get buffered writes instead of stream
-- This is ram intensive, and should only be used if you're not going to hit
-- the 2GB limit love is compiled with.
local bufferedio = {
open = function(filename,mode)
return {
_data = "",
_mode = mode,
_filename = filename,
write = function(self,val)
self._data = self._data .. val
end,
close = function(self)
local file = io.open(self._filename, self._mode)
file:write(self._data)
file:close()
end,
}
end
}
local function fileexists(name)
local f=io.open(name,"r")
if f~=nil then io.close(f) return true else return false end
end
local function start(ifilename)
if started then
error("callgrind cannot start as it has already started.")
end
started = true
callstack = { }
instr_count = 0
last_line_instr_count = 0
current_file = nil
mainfunc = nil
functions = { }
methods = { }
method_id = 1
call_indent = 0
local filename = ifilename or "callgrind.dat"
if fileexists(filename) then
local offset = 1
while fileexists(filename..offset) do
offset = offset + 1
end
filename = filename..offset
end
tracefile = io.open(filename, "w+")
tracefile:write("events: Instructions\n")
debug.sethook(trace, "crl", 1)
end
-- try to build a reverse mapping of all functions pointers
-- string.sub() should not just be sub(), but the full name
--
-- scan all tables in _G for functions
local function func2name(m, tbl, prefix)
prefix = prefix and prefix .. "." or ""
-- print(prefix)
for name, func in pairs(tbl) do
if func == _G then
-- ignore
elseif m[tostring(func)] and type(m[tostring(func)]) == "table" and m[tostring(func)].id then
-- already mapped
elseif type(func) == "function" then
-- remove the package.loaded. prefix from the loaded methods
m[tostring(func)] = { name = (prefix..name):gsub("^package\\.loaded\\.", ""), id = method_id }
method_id = method_id + 1
elseif type(func) == "table" and type(name) == "string" then
-- a package, class, ...
--
-- make sure we don't look endlessly
if m[tostring(func)] ~= "*stop*" then
m[tostring(func)] = "*stop*"
func2name(m, func, prefix..name)
end
end
end
end
local function stop()
if not started then
error("callgrind cannot stop as it has not started.")
end
started = nil
debug.sethook()
-- resolve the function pointers
func2name(methods, _G)
for key, func in pairs(functions) do
local f = func.meta
if (not f.name) and f.linedefined == 0 then
f.name = "(test-wrapper)"
end
local func_name = getfuncname(f)
if methods[tostring(f.func)] then
func_name = methods[tostring(f.func)].name
end
tracefile:write("fl="..f.short_src.."\n")
tracefile:write("fn="..func_name.."\n")
for i, line in ipairs(func.lines) do
if line:sub(1, 4) == "cfn=" and methods[line:sub(5)] then
tracefile:write("cfn="..(methods[line:sub(5)].name).."\n")
else
tracefile:write(line.."\n")
end
end
tracefile:write("\n")
end
tracefile:close()
end
return {start=start,stop=stop}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment