profile.lua 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. local M = {}
  2. local api, uv = vim.api, vim.loop
  3. local std_data = vim.fn.stdpath "data"
  4. local std_config = vim.fn.stdpath "config"
  5. local vimruntime = os.getenv "VIMRUNTIME"
  6. local lvim_runtime = get_runtime_dir()
  7. local lvim_config = get_config_dir()
  8. local function load_buffer(title, lines)
  9. local bufnr = api.nvim_create_buf(false, false)
  10. api.nvim_buf_set_lines(bufnr, 0, 0, false, lines)
  11. api.nvim_buf_set_option(bufnr, "bufhidden", "wipe")
  12. api.nvim_buf_set_option(bufnr, "buftype", "nofile")
  13. api.nvim_buf_set_option(bufnr, "swapfile", false)
  14. api.nvim_buf_set_option(bufnr, "modifiable", false)
  15. api.nvim_buf_set_name(bufnr, title)
  16. api.nvim_set_current_buf(bufnr)
  17. end
  18. local function mod_path(path)
  19. if not path then
  20. return "?"
  21. end
  22. path = path:gsub(std_data .. "/site/pack/packer/", "<PACKER>/")
  23. path = path:gsub(std_data .. "/", "<STD_DATA>/")
  24. path = path:gsub(std_config .. "/", "<STD_CONFIG>/")
  25. path = path:gsub(vimruntime .. "/", "<VIMRUNTIME>/")
  26. path = path:gsub(lvim_runtime .. "/", "<LVIM_RUNTIME>/")
  27. path = path:gsub(lvim_config .. "/", "<LVIM_CONFIG>/")
  28. return path
  29. end
  30. local function time_tostr(x)
  31. if x == 0 then
  32. return "?"
  33. end
  34. return string.format("%8.3fms", x)
  35. end
  36. local function mem_tostr(x)
  37. local unit = ""
  38. for _, u in ipairs { "K", "M", "G" } do
  39. if x < 1000 then
  40. break
  41. end
  42. x = x / 1000
  43. unit = u
  44. end
  45. return string.format("%1.1f%s", x, unit)
  46. end
  47. function M.print_profile(I)
  48. local mod_profile = I.modpaths.profile
  49. local chunk_profile = I.chunks.profile
  50. if not mod_profile and not chunk_profile then
  51. print "Error: profiling was not enabled"
  52. return
  53. end
  54. local total_resolve = 0
  55. local total_load = 0
  56. local modules = {}
  57. for path, m in pairs(chunk_profile) do
  58. m.load = m.load_end - m.load_start
  59. m.load = m.load / 1000000
  60. m.path = mod_path(path)
  61. end
  62. local module_content_width = 0
  63. for module, m in pairs(mod_profile) do
  64. m.resolve = 0
  65. if m.resolve_end then
  66. m.resolve = m.resolve_end - m.resolve_start
  67. m.resolve = m.resolve / 1000000
  68. end
  69. m.module = module:gsub("/", ".")
  70. m.loader = m.loader or m.loader_guess
  71. local path = I.modpaths.cache[module]
  72. local path_prof = chunk_profile[path]
  73. m.path = mod_path(path)
  74. if path_prof then
  75. chunk_profile[path] = nil
  76. m.load = path_prof.load
  77. m.ploader = path_prof.loader
  78. else
  79. m.load = 0
  80. m.ploader = "NA"
  81. end
  82. total_resolve = total_resolve + m.resolve
  83. total_load = total_load + m.load
  84. if #module > module_content_width then
  85. module_content_width = #module
  86. end
  87. modules[#modules + 1] = m
  88. end
  89. table.sort(modules, function(a, b)
  90. return (a.resolve + a.load) > (b.resolve + b.load)
  91. end)
  92. local paths = {}
  93. local total_paths_load = 0
  94. for _, m in pairs(chunk_profile) do
  95. paths[#paths + 1] = m
  96. total_paths_load = total_paths_load + m.load
  97. end
  98. table.sort(paths, function(a, b)
  99. return a.load > b.load
  100. end)
  101. local lines = {}
  102. local function add(fmt, ...)
  103. local args = { ... }
  104. for i, a in ipairs(args) do
  105. if type(a) == "number" then
  106. args[i] = time_tostr(a)
  107. end
  108. end
  109. lines[#lines + 1] = string.format(fmt, unpack(args))
  110. end
  111. local time_cell_width = 12
  112. local loader_cell_width = 11
  113. local time_content_width = time_cell_width - 2
  114. local loader_content_width = loader_cell_width - 2
  115. local module_cell_width = module_content_width + 2
  116. local tcwl = string.rep("─", time_cell_width)
  117. local lcwl = string.rep("─", loader_cell_width)
  118. local mcwl = string.rep("─", module_cell_width + 2)
  119. local n = string.rep("─", 200)
  120. local module_cell_format = "%-" .. module_cell_width .. "s"
  121. local loader_format = "%-" .. loader_content_width .. "s"
  122. local line_format = "%s │ %s │ %s │ %s │ %s │ %s"
  123. local row_fmt = line_format:format(
  124. " %" .. time_content_width .. "s",
  125. loader_format,
  126. "%" .. time_content_width .. "s",
  127. loader_format,
  128. module_cell_format,
  129. "%s"
  130. )
  131. local title_fmt = line_format:format(
  132. " %-" .. time_content_width .. "s",
  133. loader_format,
  134. "%-" .. time_content_width .. "s",
  135. loader_format,
  136. module_cell_format,
  137. "%s"
  138. )
  139. local title1_width = time_cell_width + loader_cell_width - 1
  140. local title1_fmt = ("%s │ %s │"):format(" %-" .. title1_width .. "s", "%-" .. title1_width .. "s")
  141. add "Note: this report is not a measure of startup time. Only use this for comparing"
  142. add "between cached and uncached loads of Lua modules"
  143. add ""
  144. add "Cache files:"
  145. for _, f in ipairs { I.chunks.path, I.modpaths.path } do
  146. local size = vim.loop.fs_stat(f).size
  147. add(" %s %s", f, mem_tostr(size))
  148. end
  149. add ""
  150. add("%s─%s┬%s─%s┐", tcwl, lcwl, tcwl, lcwl)
  151. add(title1_fmt, "Resolve", "Load")
  152. add("%s┬%s┼%s┬%s┼%s┬%s", tcwl, lcwl, tcwl, lcwl, mcwl, n)
  153. add(title_fmt, "Time", "Method", "Time", "Method", "Module", "Path")
  154. add("%s┼%s┼%s┼%s┼%s┼%s", tcwl, lcwl, tcwl, lcwl, mcwl, n)
  155. add(row_fmt, total_resolve, "", total_load, "", "Total", "")
  156. add("%s┼%s┼%s┼%s┼%s┼%s", tcwl, lcwl, tcwl, lcwl, mcwl, n)
  157. for _, p in ipairs(modules) do
  158. add(row_fmt, p.resolve, p.loader, p.load, p.ploader, p.module, p.path)
  159. end
  160. add("%s┴%s┴%s┴%s┴%s┴%s", tcwl, lcwl, tcwl, lcwl, mcwl, n)
  161. if #paths > 0 then
  162. add ""
  163. add(n)
  164. local f3 = " %" .. time_content_width .. "s │ %" .. loader_content_width .. "s │ %s"
  165. add "Files loaded with no associated module"
  166. add("%s┬%s┬%s", tcwl, lcwl, n)
  167. add(f3, "Time", "Loader", "Path")
  168. add("%s┼%s┼%s", tcwl, lcwl, n)
  169. add(f3, total_paths_load, "", "Total")
  170. add("%s┼%s┼%s", tcwl, lcwl, n)
  171. for _, p in ipairs(paths) do
  172. add(f3, p.load, p.loader, p.path)
  173. end
  174. add("%s┴%s┴%s", tcwl, lcwl, n)
  175. add ""
  176. end
  177. load_buffer("Impatient Profile Report", lines)
  178. end
  179. M.setup = function(profile)
  180. local _require = require
  181. -- luacheck: ignore 121
  182. require = function(mod)
  183. local basename = mod:gsub("%.", "/")
  184. if not profile[basename] then
  185. profile[basename] = {}
  186. profile[basename].resolve_start = uv.hrtime()
  187. profile[basename].loader_guess = "C"
  188. end
  189. return _require(mod)
  190. end
  191. -- Add profiling around all the loaders
  192. local pl = package.loaders
  193. for i = 1, #pl do
  194. local l = pl[i]
  195. pl[i] = function(mod)
  196. local basename = mod:gsub("%.", "/")
  197. profile[basename].loader_guess = i == 1 and "preloader" or "loader #" .. i
  198. return l(mod)
  199. end
  200. end
  201. end
  202. return M