├── .gitignore ├── README.md └── lulip.lua /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | x 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lulip 2 | ===== 3 | 4 | Line-level profiler for code running in LuaJIT 5 | 6 | Usage 7 | ----- 8 | 9 | ```Lua 10 | local profiler = require 'lulip' 11 | local p = profiler:new() 12 | p:dont('some-module') 13 | p:maxrows(25) 14 | p:start() 15 | 16 | -- execute code here 17 | 18 | p:stop() 19 | p:dump(output_file_name) 20 | ``` 21 | -------------------------------------------------------------------------------- /lulip.lua: -------------------------------------------------------------------------------- 1 | -- lulip: LuaJIT line level profiler 2 | -- 3 | -- Copyright (c) 2013 John Graham-Cumming 4 | -- 5 | -- License: http://opensource.org/licenses/MIT 6 | 7 | local io_lines = io.lines 8 | local io_open = io.open 9 | local pairs = pairs 10 | local print = print 11 | local debug = debug 12 | local tonumber = tonumber 13 | local setmetatable = setmetatable 14 | local table_sort = table.sort 15 | local table_insert = table.insert 16 | local string_find = string.find 17 | local string_sub = string.sub 18 | local string_gsub = string.gsub 19 | local string_format = string.format 20 | local ffi = require("ffi") 21 | 22 | ffi.cdef[[ 23 | typedef long time_t; 24 | 25 | typedef struct timeval { 26 | time_t tv_sec; 27 | time_t tv_usec; 28 | } timeval; 29 | 30 | int gettimeofday(struct timeval* t, void* tzp); 31 | ]] 32 | 33 | module(...) 34 | 35 | local gettimeofday_struct = ffi.new("timeval") 36 | local function gettimeofday() 37 | ffi.C.gettimeofday(gettimeofday_struct, nil) 38 | return tonumber(gettimeofday_struct.tv_sec) * 1000000 + tonumber(gettimeofday_struct.tv_usec) 39 | end 40 | 41 | local mt = { __index = _M } 42 | 43 | -- new: create new profiler object 44 | function new(self) 45 | return setmetatable({ 46 | 47 | -- Time when start() and stop() were called in microseconds 48 | 49 | start_time = 0, 50 | stop_time = 0, 51 | 52 | -- Per line timing information 53 | 54 | lines = {}, 55 | 56 | -- The current line being processed and when it was startd 57 | 58 | current_line = nil, 59 | current_start = 0, 60 | 61 | -- List of files to ignore. Set patterns using dont() 62 | 63 | ignore = {}, 64 | 65 | -- List of short file names used as a cache 66 | 67 | short = {}, 68 | 69 | -- Maximum number of rows of output data, set using maxrows() 70 | 71 | rows = 20, 72 | }, mt) 73 | end 74 | 75 | -- event: called when a line is executed 76 | function event(self, event, line) 77 | local now = gettimeofday() 78 | 79 | local f = string_sub(debug.getinfo(3).source,2) 80 | for i=1,#self.ignore do 81 | if string_find(f, self.ignore[i], 1, true) then 82 | return 83 | end 84 | end 85 | 86 | local short = self.short[f] 87 | if not short then 88 | local start = string_find(f, "[^/]+$") 89 | self.short[f] = string_sub(f, start) 90 | short = self.short[f] 91 | end 92 | 93 | if self.current_line ~= nil then 94 | self.lines[self.current_line][1] = 95 | self.lines[self.current_line][1] + 1 96 | self.lines[self.current_line][2] = 97 | self.lines[self.current_line][2] + (now - self.current_start) 98 | end 99 | 100 | self.current_line = short .. ':' .. line 101 | 102 | if self.lines[self.current_line] == nil then 103 | self.lines[self.current_line] = {0, 0.0, f} 104 | end 105 | 106 | self.current_start = gettimeofday() 107 | end 108 | 109 | -- dont: tell the profiler to ignore files that match these patterns 110 | function dont(self, file) 111 | table_insert(self.ignore, file) 112 | end 113 | 114 | -- maxrows: set the maximum number of rows of output 115 | function maxrows(self, max) 116 | self.rows = max 117 | end 118 | 119 | -- start: begin profiling 120 | function start(self) 121 | self:dont('lulip.lua') 122 | self.start_time = gettimeofday() 123 | self.current_line = nil 124 | self.current_start = 0 125 | debug.sethook(function(e,l) self:event(e, l) end, "l") 126 | end 127 | 128 | -- stop: end profiling 129 | function stop(self) 130 | self.stop_time = gettimeofday() 131 | debug.sethook() 132 | end 133 | 134 | -- readfile: turn a file into an array for line-level access 135 | local function readfile(file) 136 | local lines = {} 137 | local ln = 1 138 | for line in io_lines(file) do 139 | lines[ln] = string_gsub(line, "^%s*(.-)%s*$", "%1") 140 | ln = ln + 1 141 | end 142 | return lines 143 | end 144 | 145 | -- dump: dump profile information to the named file 146 | function dump(self, file) 147 | local t = {} 148 | for l,d in pairs(self.lines) do 149 | table_insert(t, {line=l, data=d}) 150 | end 151 | table_sort(t, function(a,b) return a["data"][2] > b["data"][2] end) 152 | 153 | local files = {} 154 | 155 | local f = io_open(file, "w") 156 | if not f then 157 | print("Failed to open output file " .. file) 158 | return 159 | end 160 | f:write([[ 161 | 162 |
163 | 165 | 166 | 167 | 168 |file:line | count | 170 |elapsed (ms) | line | 171 |
---|---|---|---|
%s | %i | %.3f | 185 |%s |