diff --git a/frontend/lulip.lua b/frontend/lulip.lua new file mode 100644 index 000000000..aee1c65a5 --- /dev/null +++ b/frontend/lulip.lua @@ -0,0 +1,190 @@ +-- lulip: LuaJIT line level profiler +-- +-- Copyright (c) 2013 John Graham-Cumming +-- +-- License: http://opensource.org/licenses/MIT + +local io_lines = io.lines +local io_open = io.open +local pairs = pairs +local print = print +local debug = debug +local tonumber = tonumber +local setmetatable = setmetatable +local table_sort = table.sort +local table_insert = table.insert +local string_find = string.find +local string_sub = string.sub +local string_gsub = string.gsub +local string_format = string.format +local ffi = require("ffi") + +ffi.cdef[[ + typedef long time_t; + + typedef struct timeval { + time_t tv_sec; + time_t tv_usec; + } timeval; + + int gettimeofday(struct timeval* t, void* tzp); +]] + +module(...) + +local gettimeofday_struct = ffi.new("timeval") +local function gettimeofday() + ffi.C.gettimeofday(gettimeofday_struct, nil) + return tonumber(gettimeofday_struct.tv_sec) * 1000000 + tonumber(gettimeofday_struct.tv_usec) +end + +local mt = { __index = _M } + +-- new: create new profiler object +function new(self) + return setmetatable({ + + -- Time when start() and stop() were called in microseconds + + start_time = 0, + stop_time = 0, + + -- Per line timing information + + lines = {}, + + -- The current line being processed and when it was startd + + current_line = nil, + current_start = 0, + + -- List of files to ignore. Set patterns using dont() + + ignore = {}, + + -- List of short file names used as a cache + + short = {}, + + -- Maximum number of rows of output data, set using maxrows() + + rows = 20, + }, mt) +end + +-- event: called when a line is executed +function event(self, event, line) + local now = gettimeofday() + + local f = string_sub(debug.getinfo(3).source,2) + for i=1,#self.ignore do + if string_find(f, self.ignore[i], 1, true) then + return + end + end + + local short = self.short[f] + if not short then + local start = string_find(f, "[^/]+$") + self.short[f] = string_sub(f, start) + short = self.short[f] + end + + if self.current_line ~= nil then + self.lines[self.current_line][1] = + self.lines[self.current_line][1] + 1 + self.lines[self.current_line][2] = + self.lines[self.current_line][2] + (now - self.current_start) + end + + self.current_line = short .. ':' .. line + + if self.lines[self.current_line] == nil then + self.lines[self.current_line] = {0, 0.0, f} + end + + self.current_start = gettimeofday() +end + +-- dont: tell the profiler to ignore files that match these patterns +function dont(self, file) + table_insert(self.ignore, file) +end + +-- maxrows: set the maximum number of rows of output +function maxrows(self, max) + self.rows = max +end + +-- start: begin profiling +function start(self) + self:dont('lulip.lua') + self.start_time = gettimeofday() + self.current_line = nil + self.current_start = 0 + debug.sethook(function(e,l) self:event(e, l) end, "l") +end + +-- stop: end profiling +function stop(self) + self.stop_time = gettimeofday() + debug.sethook() +end + +-- readfile: turn a file into an array for line-level access +local function readfile(file) + local lines = {} + local ln = 1 + for line in io_lines(file) do + lines[ln] = string_gsub(line, "^%s*(.-)%s*$", "%1") + ln = ln + 1 + end + return lines +end + +-- dump: dump profile information to the named file +function dump(self, file) + local t = {} + for l,d in pairs(self.lines) do + table_insert(t, {line=l, data=d}) + end + table_sort(t, function(a,b) return a["data"][2] > b["data"][2] end) + + local files = {} + + local f = io_open(file, "w") + if not f then + print("Failed to open output file " .. file) + return + end + f:write([[ + +
+ + + + +file:line | count | +elapsed (ms) | line | +
---|---|---|---|
%s | %i | %.3f | +%s |