1 ----------------------------------------------------------------------------
4 -- Copyright (C) 2005-2023 Mike Pall. All rights reserved.
5 -- Released under the MIT license. See Copyright Notice in luajit.h
6 ----------------------------------------------------------------------------
8 -- This module is a simple command line interface to the built-in
9 -- low-overhead profiler of LuaJIT.
11 -- The lower-level API of the profiler is accessible via the "jit.profile"
12 -- module or the luaJIT_profile_* C API.
16 -- luajit -jp myapp.lua
17 -- luajit -jp=s myapp.lua
18 -- luajit -jp=-s myapp.lua
19 -- luajit -jp=vl myapp.lua
20 -- luajit -jp=G,profile.txt myapp.lua
22 -- The following dump features are available:
24 -- f Stack dump: function name, otherwise module:line. Default mode.
25 -- F Stack dump: ditto, but always prepend module.
26 -- l Stack dump: module:line.
27 -- <number> stack dump depth (callee < caller). Default: 1.
28 -- -<number> Inverse stack dump depth (caller > callee).
29 -- s Split stack dump after first stack level. Implies abs(depth) >= 2.
30 -- p Show full path for module names.
31 -- v Show VM states. Can be combined with stack dumps, e.g. vf or fv.
32 -- z Show zones. Can be combined with stack dumps, e.g. zf or fz.
33 -- r Show raw sample counts. Default: show percentages.
34 -- a Annotate excerpts from source code files.
35 -- A Annotate complete source code files.
36 -- G Produce raw output suitable for graphical tools (e.g. flame graphs).
37 -- m<number> Minimum sample percentage to be shown. Default: 3.
38 -- i<number> Sampling interval in milliseconds. Default: 10.
40 ----------------------------------------------------------------------------
42 -- Cache some library functions and objects.
43 local jit
= require("jit")
44 local profile
= require("jit.profile")
45 local vmdef
= require("jit.vmdef")
47 local pairs
, ipairs
, tonumber, floor = pairs
, ipairs
, tonumber, math
.floor
48 local sort, format = table.sort, string.format
49 local stdout
= io
.stdout
50 local zone
-- Load jit.zone module on demand.
52 -- Output file handle.
55 ------------------------------------------------------------------------------
58 local prof_states
, prof_split
, prof_min
, prof_raw
, prof_fmt
, prof_depth
59 local prof_ann
, prof_count1
, prof_count2
, prof_samples
65 G
= "Garbage Collector",
70 local function prof_cb(th
, samples
, vmmode
)
71 prof_samples
= prof_samples
+ samples
72 local key_stack
, key_stack2
, key_state
73 -- Collect keys for sample.
75 if prof_states
== "v" then
76 key_state
= map_vmmode
[vmmode
] or vmmode
78 key_state
= zone
:get() or "(none)"
82 key_stack
= profile
.dumpstack(th
, prof_fmt
, prof_depth
)
83 key_stack
= key_stack
:gsub("%[builtin#(%d+)%]", function(x
)
84 return vmdef
.ffnames
[tonumber(x
)]
86 if prof_split
== 2 then
87 local k1
, k2
= key_stack
:match("(.-) [<>] (.*)")
88 if k2
then key_stack
, key_stack2
= k1
, k2
end
89 elseif prof_split
== 3 then
90 key_stack2
= profile
.dumpstack(th
, "l", 1)
95 if prof_split
== 1 then
98 if key_stack
then k2
= key_stack
end
100 elseif key_stack
then
102 if key_stack2
then k2
= key_stack2
elseif key_state
then k2
= key_state
end
104 -- Coalesce samples in one or two levels.
106 local t1
= prof_count1
107 t1
[k1
] = (t1
[k1
] or 0) + samples
109 local t2
= prof_count2
111 if not t3
then t3
= {}; t2
[k1
] = t3
end
112 t3
[k2
] = (t3
[k2
] or 0) + samples
117 ------------------------------------------------------------------------------
120 local function prof_top(count1
, count2
, samples
, indent
)
122 for k
in pairs(count1
) do
126 sort(t
, function(a
, b
) return count1
[a
] > count1
[b
] end)
130 local pct
= floor(v
*100/samples
+ 0.5)
131 if pct
< prof_min
then break end
133 out
:write(format("%s%2d%% %s\n", indent
, pct
, k
))
134 elseif prof_raw
== "r" then
135 out
:write(format("%s%5d %s\n", indent
, v
, k
))
137 out
:write(format("%s %d\n", k
, v
))
142 prof_top(r
, nil, v
, (prof_split
== 3 or prof_split
== 1) and " -- " or
143 (prof_depth
< 0 and " -> " or " <- "))
149 -- Annotate source code
150 local function prof_annotate(count1
, samples
)
153 for k
, v
in pairs(count1
) do
154 local pct
= floor(v
*100/samples
+ 0.5)
156 if pct
>= prof_min
then
157 local file
, line
= k
:match("^(.*):(%d+)$")
158 if not file
then file
= k
; line
= 0 end
159 local fl
= files
[file
]
160 if not fl
then fl
= {}; files
[file
] = fl
; files
[#files
+1] = file
end
161 line
= tonumber(line
)
162 fl
[line
] = prof_raw
and v
or pct
166 local fmtv
, fmtn
= " %3d%% | %s\n", " | %s\n"
168 local n
= math
.max(5, math
.ceil(math
.log10(ms
)))
169 fmtv
= "%"..n
.."d | %s\n"
170 fmtn
= (" "):rep(n
).." | %s\n"
173 for _
, file
in ipairs(files
) do
174 local f0
= file
:byte()
175 if f0
== 40 or f0
== 91 then
176 out
:write(format("\n====== %s ======\n[Cannot annotate non-file]\n", file
))
179 local fp
, err
= io
.open(file
)
181 out
:write(format("====== ERROR: %s: %s\n", file
, err
))
184 out
:write(format("\n====== %s ======\n", file
))
185 local fl
= files
[file
]
186 local n
, show
= 1, false
189 if fl
[i
] then show
= true; out
:write("@@ 1 @@\n"); break end
192 for line
in fp
:lines() do
193 if line
:byte() == 27 then
194 out
:write("[Cannot annotate bytecode file]\n")
201 if v2
then show
= n
+ann
elseif v
then show
= n
202 elseif show
+ann
< n
then show
= false end
205 out
:write(format("@@ %d @@\n", n
))
207 if not show
then goto
next end
210 out
:write(format(fmtv
, v
, line
))
212 out
:write(format(fmtn
, line
))
221 ------------------------------------------------------------------------------
223 -- Finish profiling and dump result.
224 local function prof_finish()
227 local samples
= prof_samples
229 if prof_raw
~= true then out
:write("[No samples collected]\n") end
233 prof_annotate(prof_count1
, samples
)
235 prof_top(prof_count1
, prof_count2
, samples
, "")
240 if out
~= stdout
then out
:close() end
245 local function prof_start(mode
)
247 mode
= mode
:gsub("i%d*", function(s
) interval
= s
; return "" end)
249 mode
= mode
:gsub("m(%d+)", function(s
) prof_min
= tonumber(s
); return "" end)
251 mode
= mode
:gsub("%-?%d+", function(s
) prof_depth
= tonumber(s
); return "" end)
253 for c
in mode
:gmatch(".") do m
[c
] = c
end
254 prof_states
= m
.z
or m
.v
255 if prof_states
== "z" then zone
= require("jit.zone") end
256 local scope
= m
.l
or m
.f
or m
.F
or (prof_states
and "" or "f")
257 local flags
= (m
.p
or "")
261 if prof_depth
== -1 or m
["-"] then prof_depth
= -2
262 elseif prof_depth
== 1 then prof_depth
= 2 end
263 elseif mode
:find("[fF].*l") then
267 prof_split
= (scope
== "" or mode
:find("[zv].*[lfF]")) and 1 or 0
269 prof_ann
= m
.A
and 0 or (m
.a
and 3)
275 elseif m
.G
and scope
~= "" then
276 prof_fmt
= flags
..scope
.."Z;"
280 elseif scope
== "" then
283 local sc
= prof_split
== 3 and m
.f
or m
.F
or scope
284 prof_fmt
= flags
..sc
..(prof_depth
>= 0 and "Z < " or "Z > ")
289 profile
.start(scope
:lower()..interval
, prof_cb
)
290 prof_ud
= newproxy(true)
291 getmetatable(prof_ud
).__gc
= prof_finish
294 ------------------------------------------------------------------------------
296 local function start(mode
, outfile
)
297 if not outfile
then outfile
= os
.getenv("LUAJIT_PROFILEFILE") end
299 out
= outfile
== "-" and stdout
or assert(io
.open(outfile
, "w"))
303 prof_start(mode
or "f")
306 -- Public module functions.
308 start
= start
, -- For -j command line option.