local clock = os.clock --- Simple profiler written in Lua. -- @module profile -- @alias profile local profile = {} -- function labels local _labeled = {} -- function definitions local _defined = {} -- time of last call local _tcalled = {} -- total execution time local _telapsed = {} -- number of calls local _ncalls = {} -- list of internal profiler functions local _internal = {} --- This is an internal function. -- @tparam string event Event type -- @tparam number line Line number -- @tparam[opt] table info Debug info table function profile.hooker(event, line, info) info = info or debug.getinfo(2, "fnS") local f = info.func -- ignore the profiler itself if _internal[f] or info.what ~= "Lua" then return end -- get the function name if available if info.name then _labeled[f] = info.name end -- find the line definition if not _defined[f] then _defined[f] = info.short_src .. ":" .. info.linedefined _ncalls[f] = 0 _telapsed[f] = 0 end if _tcalled[f] then local dt = clock() - _tcalled[f] _telapsed[f] = _telapsed[f] + dt _tcalled[f] = nil end if event == "tail call" then local prev = debug.getinfo(3, "fnS") profile.hooker("return", line, prev) profile.hooker("call", line, info) elseif event == "call" then _tcalled[f] = clock() else _ncalls[f] = _ncalls[f] + 1 end end --- Sets a clock function to be used by the profiler. -- @tparam function func Clock function that returns a number function profile.setclock(f) assert(type(f) == "function", "clock must be a function") clock = f end --- Starts collecting data. function profile.start() if rawget(_G, "jit") then jit.off() jit.flush() end debug.sethook(profile.hooker, "cr") end --- Stops collecting data. function profile.stop() debug.sethook() for f in pairs(_tcalled) do local dt = clock() - _tcalled[f] _telapsed[f] = _telapsed[f] + dt _tcalled[f] = nil end -- merge closures local lookup = {} for f, d in pairs(_defined) do local id = (_labeled[f] or "?") .. d local f2 = lookup[id] if f2 then _ncalls[f2] = _ncalls[f2] + (_ncalls[f] or 0) _telapsed[f2] = _telapsed[f2] + (_telapsed[f] or 0) _defined[f], _labeled[f] = nil, nil _ncalls[f], _telapsed[f] = nil, nil else lookup[id] = f end end collectgarbage("collect") end --- Resets all collected data. function profile.reset() for f in pairs(_ncalls) do _ncalls[f] = 0 end for f in pairs(_telapsed) do _telapsed[f] = 0 end for f in pairs(_tcalled) do _tcalled[f] = nil end collectgarbage("collect") end --- This is an internal function. -- @tparam function a First function -- @tparam function b Second function -- @treturn boolean True if "a" should rank higher than "b" function profile.comp(a, b) local dt = _telapsed[b] - _telapsed[a] if dt == 0 then return _ncalls[b] < _ncalls[a] end return dt < 0 end --- Generates a report of functions that have been called since the profile was started. -- Returns the report as a numeric table of rows containing the rank, function label, number of calls, total execution time and source code line number. -- @tparam[opt] number limit Maximum number of rows -- @treturn table Table of rows function profile.query(limit) local t = {} for f, n in pairs(_ncalls) do if n > 0 then t[#t + 1] = f end end table.sort(t, profile.comp) if limit then while #t > limit do table.remove(t) end end for i, f in ipairs(t) do local dt = 0 if _tcalled[f] then dt = clock() - _tcalled[f] end t[i] = { i, _labeled[f] or "?", _ncalls[f], _telapsed[f] + dt, _defined[f] } end return t end local cols = { 3, 29, 11, 24, 32 } --- Generates a text report of functions that have been called since the profile was started. -- Returns the report as a string that can be printed to the console. -- @tparam[opt] number limit Maximum number of rows -- @treturn string Text-based profiling report function profile.report(n) local out = {} local report = profile.query(n) for i, row in ipairs(report) do for j = 1, 5 do local s = row[j] local l2 = cols[j] s = tostring(s) local l1 = s:len() if l1 < l2 then s = s .. (" "):rep(l2 - l1) elseif l1 > l2 then s = s:sub(l1 - l2 + 1, l1) end row[j] = s end out[i] = table.concat(row, " | ") end local row = " +-----+-------------------------------+-------------+--------------------------+----------------------------------+ \n" local col = " | # | Function | Calls | Time | Code | \n" local sz = row .. col .. row if #out > 0 then sz = sz .. " | " .. table.concat(out, " | \n | ") .. " | \n" end return "\n" .. sz .. row end -- store all internal profiler functions for _, v in pairs(profile) do if type(v) == "function" then _internal[v] = true end end return profile