profiler.lua 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. ----------------------------------------------------------------------------
  2. -- LuaJIT profiler.
  3. --
  4. -- Copyright (C) 2005-2020 Mike Pall. All rights reserved.
  5. -- Released under the MIT license. See Copyright Notice in luajit.h
  6. ----------------------------------------------------------------------------
  7. -- Modified for inDECK to return string instead of writing to file.
  8. -- Usage:
  9. -- p = require('profiler')
  10. -- p.start('p=-s')
  11. -- --[[ hot code ]]
  12. -- profile_report = p.stop()
  13. --
  14. ----------------------------------------------------------------------------
  15. --
  16. -- This module is a simple command line interface to the built-in
  17. -- low-overhead profiler of LuaJIT.
  18. --
  19. -- The lower-level API of the profiler is accessible via the "jit.profile"
  20. -- module or the luaJIT_profile_* C API.
  21. --
  22. -- Example usage:
  23. --
  24. -- luajit -jp myapp.lua
  25. -- luajit -jp=s myapp.lua
  26. -- luajit -jp=-s myapp.lua
  27. -- luajit -jp=vl myapp.lua
  28. -- luajit -jp=G,profile.txt myapp.lua
  29. --
  30. -- The following dump features are available:
  31. --
  32. -- f Stack dump: function name, otherwise module:line. Default mode.
  33. -- F Stack dump: ditto, but always prepend module.
  34. -- l Stack dump: module:line.
  35. -- <number> stack dump depth (callee < caller). Default: 1.
  36. -- -<number> Inverse stack dump depth (caller > callee).
  37. -- s Split stack dump after first stack level. Implies abs(depth) >= 2.
  38. -- p Show full path for module names.
  39. -- v Show VM states. Can be combined with stack dumps, e.g. vf or fv.
  40. -- z Show zones. Can be combined with stack dumps, e.g. zf or fz.
  41. -- r Show raw sample counts. Default: show percentages.
  42. -- a Annotate excerpts from source code files.
  43. -- A Annotate complete source code files.
  44. -- G Produce raw output suitable for graphical tools (e.g. flame graphs).
  45. -- m<number> Minimum sample percentage to be shown. Default: 3.
  46. -- i<number> Sampling interval in milliseconds. Default: 10.
  47. --
  48. ----------------------------------------------------------------------------
  49. -- Cache some library functions and objects.
  50. local jit = require("jit")
  51. assert(jit.version_num == 20100, "LuaJIT core/library version mismatch")
  52. local profile = require("jit.profile")
  53. local vmdef = require("vmdef")
  54. local math = math
  55. local pairs, ipairs, tonumber, floor = pairs, ipairs, tonumber, math.floor
  56. local sort, format = table.sort, string.format
  57. local stdout = io.stdout
  58. local zone -- Load jit.zone module on demand.
  59. local iostring = setmetatable({}, {
  60. write = function(self, str) table.insert(self, str) end,
  61. readAll = function(self)
  62. local report = table.concat(self)
  63. for i,_ in ipairs(self) do self[i] = nil end -- reset
  64. return report
  65. end,
  66. })
  67. getmetatable(iostring).__index = getmetatable(iostring)
  68. -- Output file handle.
  69. local out = iostring
  70. ------------------------------------------------------------------------------
  71. local prof_ud
  72. local prof_states, prof_split, prof_min, prof_raw, prof_fmt, prof_depth
  73. local prof_ann, prof_count1, prof_count2, prof_samples
  74. local map_vmmode = {
  75. N = "Compiled",
  76. I = "Interpreted",
  77. C = "C code",
  78. G = "Garbage Collector",
  79. J = "JIT Compiler",
  80. }
  81. -- Profiler callback.
  82. local function prof_cb(th, samples, vmmode)
  83. prof_samples = prof_samples + samples
  84. local key_stack, key_stack2, key_state
  85. -- Collect keys for sample.
  86. if prof_states then
  87. if prof_states == "v" then
  88. key_state = map_vmmode[vmmode] or vmmode
  89. else
  90. key_state = zone:get() or "(none)"
  91. end
  92. end
  93. if prof_fmt then
  94. key_stack = profile.dumpstack(th, prof_fmt, prof_depth)
  95. key_stack = key_stack:gsub("%[builtin#(%d+)%]", function(x)
  96. return vmdef.ffnames[tonumber(x)]
  97. end)
  98. if prof_split == 2 then
  99. local k1, k2 = key_stack:match("(.-) [<>] (.*)")
  100. if k2 then key_stack, key_stack2 = k1, k2 end
  101. elseif prof_split == 3 then
  102. key_stack2 = profile.dumpstack(th, "l", 1)
  103. end
  104. end
  105. -- Order keys.
  106. local k1, k2
  107. if prof_split == 1 then
  108. if key_state then
  109. k1 = key_state
  110. if key_stack then k2 = key_stack end
  111. end
  112. elseif key_stack then
  113. k1 = key_stack
  114. if key_stack2 then k2 = key_stack2 elseif key_state then k2 = key_state end
  115. end
  116. -- Coalesce samples in one or two levels.
  117. if k1 then
  118. local t1 = prof_count1
  119. t1[k1] = (t1[k1] or 0) + samples
  120. if k2 then
  121. local t2 = prof_count2
  122. local t3 = t2[k1]
  123. if not t3 then t3 = {}; t2[k1] = t3 end
  124. t3[k2] = (t3[k2] or 0) + samples
  125. end
  126. end
  127. end
  128. ------------------------------------------------------------------------------
  129. -- Show top N list.
  130. local function prof_top(count1, count2, samples, indent)
  131. local t, n = {}, 0
  132. for k in pairs(count1) do
  133. n = n + 1
  134. t[n] = k
  135. end
  136. sort(t, function(a, b) return count1[a] > count1[b] end)
  137. for i=1,n do
  138. local k = t[i]
  139. local v = count1[k]
  140. local pct = floor(v*100/samples + 0.5)
  141. if pct < prof_min then break end
  142. if not prof_raw then
  143. out:write(format("%s%2d%% %s\n", indent, pct, k))
  144. elseif prof_raw == "r" then
  145. out:write(format("%s%5d %s\n", indent, v, k))
  146. else
  147. out:write(format("%s %d\n", k, v))
  148. end
  149. if count2 then
  150. local r = count2[k]
  151. if r then
  152. prof_top(r, nil, v, (prof_split == 3 or prof_split == 1) and " -- " or
  153. (prof_depth < 0 and " -> " or " <- "))
  154. end
  155. end
  156. end
  157. end
  158. -- Annotate source code
  159. local function prof_annotate(count1, samples)
  160. local files = {}
  161. local ms = 0
  162. for k, v in pairs(count1) do
  163. local pct = floor(v*100/samples + 0.5)
  164. ms = math.max(ms, v)
  165. if pct >= prof_min then
  166. local file, line = k:match("^(.*):(%d+)$")
  167. if not file then file = k; line = 0 end
  168. local fl = files[file]
  169. if not fl then fl = {}; files[file] = fl; files[#files+1] = file end
  170. line = tonumber(line)
  171. fl[line] = prof_raw and v or pct
  172. end
  173. end
  174. sort(files)
  175. local fmtv, fmtn = " %3d%% | %s\n", " | %s\n"
  176. if prof_raw then
  177. local n = math.max(5, math.ceil(math.log10(ms)))
  178. fmtv = "%"..n.."d | %s\n"
  179. fmtn = (" "):rep(n).." | %s\n"
  180. end
  181. local ann = prof_ann
  182. for _, file in ipairs(files) do
  183. local f0 = file:byte()
  184. if f0 == 40 or f0 == 91 then
  185. out:write(format("\n====== %s ======\n[Cannot annotate non-file]\n", file))
  186. break
  187. end
  188. local fp, err = io.open(file)
  189. if not fp then
  190. out:write(format("====== ERROR: %s: %s\n", file, err))
  191. break
  192. end
  193. out:write(format("\n====== %s ======\n", file))
  194. local fl = files[file]
  195. local n, show = 1, false
  196. if ann ~= 0 then
  197. for i=1,ann do
  198. if fl[i] then show = true; out:write("@@ 1 @@\n"); break end
  199. end
  200. end
  201. for line in fp:lines() do
  202. if line:byte() == 27 then
  203. out:write("[Cannot annotate bytecode file]\n")
  204. break
  205. end
  206. local v = fl[n]
  207. if ann ~= 0 then
  208. local v2 = fl[n+ann]
  209. if show then
  210. if v2 then show = n+ann elseif v then show = n
  211. elseif show+ann < n then show = false end
  212. elseif v2 then
  213. show = n+ann
  214. out:write(format("@@ %d @@\n", n))
  215. end
  216. if not show then goto next end
  217. end
  218. if v then
  219. out:write(format(fmtv, v, line))
  220. else
  221. out:write(format(fmtn, line))
  222. end
  223. ::next::
  224. n = n + 1
  225. end
  226. fp:close()
  227. end
  228. end
  229. ------------------------------------------------------------------------------
  230. -- Finish profiling and dump result.
  231. local function prof_finish()
  232. if prof_ud then
  233. profile.stop()
  234. local samples = prof_samples
  235. if samples == 0 then
  236. if prof_raw ~= true then out:write("[No samples collected]\n") end
  237. return
  238. end
  239. if prof_ann then
  240. prof_annotate(prof_count1, samples)
  241. else
  242. prof_top(prof_count1, prof_count2, samples, "")
  243. end
  244. prof_count1 = nil
  245. prof_count2 = nil
  246. prof_ud = nil
  247. end
  248. return out:readAll()
  249. end
  250. -- Start profiling.
  251. local function prof_start(mode)
  252. local interval = ""
  253. mode = mode:gsub("i%d*", function(s) interval = s; return "" end)
  254. prof_min = 3
  255. mode = mode:gsub("m(%d+)", function(s) prof_min = tonumber(s); return "" end)
  256. prof_depth = 1
  257. mode = mode:gsub("%-?%d+", function(s) prof_depth = tonumber(s); return "" end)
  258. local m = {}
  259. for c in mode:gmatch(".") do m[c] = c end
  260. prof_states = m.z or m.v
  261. if prof_states == "z" then zone = require("jit.zone") end
  262. local scope = m.l or m.f or m.F or (prof_states and "" or "f")
  263. local flags = (m.p or "")
  264. prof_raw = m.r
  265. if m.s then
  266. prof_split = 2
  267. if prof_depth == -1 or m["-"] then prof_depth = -2
  268. elseif prof_depth == 1 then prof_depth = 2 end
  269. elseif mode:find("[fF].*l") then
  270. scope = "l"
  271. prof_split = 3
  272. else
  273. prof_split = (scope == "" or mode:find("[zv].*[lfF]")) and 1 or 0
  274. end
  275. prof_ann = m.A and 0 or (m.a and 3)
  276. if prof_ann then
  277. scope = "l"
  278. prof_fmt = "pl"
  279. prof_split = 0
  280. prof_depth = 1
  281. elseif m.G and scope ~= "" then
  282. prof_fmt = flags..scope.."Z;"
  283. prof_depth = -100
  284. prof_raw = true
  285. prof_min = 0
  286. elseif scope == "" then
  287. prof_fmt = false
  288. else
  289. local sc = prof_split == 3 and m.f or m.F or scope
  290. prof_fmt = flags..sc..(prof_depth >= 0 and "Z < " or "Z > ")
  291. end
  292. prof_count1 = {}
  293. prof_count2 = {}
  294. prof_samples = 0
  295. profile.start(scope:lower()..interval, prof_cb)
  296. prof_ud = newproxy(true)
  297. getmetatable(prof_ud).__gc = prof_finish
  298. end
  299. ------------------------------------------------------------------------------
  300. local function start(mode, outfile)
  301. if not out then
  302. if not outfile then outfile = os.getenv("LUAJIT_PROFILEFILE") end
  303. if outfile then
  304. out = outfile == "-" and stdout or assert(io.open(outfile, "w"))
  305. else
  306. out = stdout
  307. end
  308. end
  309. prof_start(mode or "f")
  310. end
  311. -- Public module functions.
  312. return {
  313. start = start, -- For -j command line option.
  314. stop = prof_finish
  315. }