mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-04 18:58:12 +00:00
feat(snacks): added Snacks.profiler
This commit is contained in:
parent
8f6719a368
commit
808879951f
7 changed files with 1503 additions and 0 deletions
236
lua/snacks/profiler/core.lua
Normal file
236
lua/snacks/profiler/core.lua
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
---@class snacks.profiler.core
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local hrtime = (vim.uv or vim.loop).hrtime
|
||||||
|
local nvim_create_autocmd = vim.api.nvim_create_autocmd
|
||||||
|
|
||||||
|
M._require = _G.require
|
||||||
|
M.attached = {} ---@type table<unknown, boolean>
|
||||||
|
M.events = {} ---@type snacks.profiler.Event[]
|
||||||
|
M.filter_fn = error ---@type fun(str:string):boolean
|
||||||
|
M.filter_mod = error ---@type fun(str:string):boolean
|
||||||
|
M.id = 0
|
||||||
|
M.me = debug.getinfo(1, "S").source:sub(2)
|
||||||
|
M.pids = {} ---@type table<string, number>
|
||||||
|
M.running = false
|
||||||
|
M.skips = { -- these modules are always be skipped
|
||||||
|
["_G"] = true,
|
||||||
|
["bit"] = true,
|
||||||
|
["coroutine"] = true,
|
||||||
|
["debug"] = true,
|
||||||
|
["ffi"] = true,
|
||||||
|
["io"] = true,
|
||||||
|
["jit"] = true,
|
||||||
|
["jit.opt"] = true,
|
||||||
|
["jit.profile"] = true,
|
||||||
|
["lpeg"] = true,
|
||||||
|
["luv"] = true,
|
||||||
|
["math"] = true,
|
||||||
|
["mpack"] = true,
|
||||||
|
["os"] = true,
|
||||||
|
["package"] = true,
|
||||||
|
["snacks.debug"] = true,
|
||||||
|
["snacks.profiler"] = true,
|
||||||
|
["snacks.profiler.core"] = true,
|
||||||
|
["snacks.profiler.loc"] = true,
|
||||||
|
["snacks.profiler.picker"] = true,
|
||||||
|
["snacks.profiler.tracer"] = true,
|
||||||
|
["snacks.profiler.ui"] = true,
|
||||||
|
["string"] = true,
|
||||||
|
["table"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
function M.skip(it)
|
||||||
|
M.attached[it] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param spec table<string, boolean>
|
||||||
|
---@return fun(str:string):boolean
|
||||||
|
function M.filter(spec)
|
||||||
|
local filters = {} ---@type {pattern:string, want:boolean, exact:boolean}[]
|
||||||
|
local default = spec.default
|
||||||
|
default = default == nil and true or default
|
||||||
|
for pattern, want in pairs(spec) do
|
||||||
|
if pattern ~= "default" then
|
||||||
|
table.insert(filters, { pattern = pattern, want = want, exact = pattern:sub(1, 1) ~= "^" })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- sort by longest pattern first
|
||||||
|
table.sort(filters, function(a, b)
|
||||||
|
return #a.pattern > #b.pattern
|
||||||
|
end)
|
||||||
|
return function(str)
|
||||||
|
for _, filter in ipairs(filters) do
|
||||||
|
if filter.exact then
|
||||||
|
if str == filter.pattern then
|
||||||
|
return filter.want
|
||||||
|
end
|
||||||
|
elseif str:find(filter.pattern) then
|
||||||
|
return filter.want
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return default
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param opts snacks.profiler.Trace.opts
|
||||||
|
---@param caller? snacks.profiler.Loc
|
||||||
|
---@return ...
|
||||||
|
function M.trace(opts, caller, ...)
|
||||||
|
local start = hrtime()
|
||||||
|
local thread = tostring(coroutine.running() or "main")
|
||||||
|
local pid = M.pids[thread] or 0
|
||||||
|
M.id = M.id + 1
|
||||||
|
M.pids[thread] = M.id
|
||||||
|
---@type snacks.profiler.Event
|
||||||
|
local entry = { id = M.id, start = start, pid = pid, ref = caller, opts = opts }
|
||||||
|
M.events[#M.events + 1] = entry
|
||||||
|
local ret = { pcall(opts.fn, ...) }
|
||||||
|
M.pids[thread] = pid
|
||||||
|
entry.stop = hrtime()
|
||||||
|
if not ret[1] then
|
||||||
|
error(ret[2])
|
||||||
|
end
|
||||||
|
return select(2, unpack(ret))
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param depth? number
|
||||||
|
---@param max_depth? number
|
||||||
|
---@return snacks.profiler.Loc?
|
||||||
|
function M.caller(depth, max_depth)
|
||||||
|
for i = depth or 3, max_depth or 10 do
|
||||||
|
local info = debug.getinfo(i, "Sl")
|
||||||
|
if not info then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local source = info.source:sub(2)
|
||||||
|
if info.what ~= "C" and source ~= M.me then
|
||||||
|
return { file = source, line = info.currentline }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param opts snacks.profiler.Trace.opts
|
||||||
|
function M.attach_fn(opts)
|
||||||
|
if M.attached[opts.fn] then
|
||||||
|
return opts.fn
|
||||||
|
end
|
||||||
|
M.attached[opts.fn] = true
|
||||||
|
local ret = function(...)
|
||||||
|
if not M.running then
|
||||||
|
return opts.fn(...)
|
||||||
|
end
|
||||||
|
return M.trace(opts, M.caller() or nil, ...)
|
||||||
|
end
|
||||||
|
M.attached[ret] = true
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param modname string
|
||||||
|
---@param mod table<string, function>
|
||||||
|
---@param opts? {force?:boolean}
|
||||||
|
function M.attach_mod(modname, mod, opts)
|
||||||
|
if type(mod) ~= "table" or M.attached[mod] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
opts = opts or {}
|
||||||
|
if (M.skips[modname] or not M.filter_mod(modname)) and opts.force ~= true then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
M.attached[mod] = true
|
||||||
|
for k, v in pairs(mod) do
|
||||||
|
if type(k) == "string" and type(v) == "function" and not M.attached[v] then
|
||||||
|
local name = modname .. "." .. k
|
||||||
|
if M.filter_fn(name) then
|
||||||
|
mod[k] = M.attach_fn({ modname = modname, fname = k, name = name, fn = v })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.require(modname)
|
||||||
|
if not M.running or package.loaded[modname] or M.skips[modname] then
|
||||||
|
return M._require(modname)
|
||||||
|
end
|
||||||
|
local ret = {
|
||||||
|
M.trace({
|
||||||
|
fname = "require",
|
||||||
|
name = "require:" .. modname,
|
||||||
|
require = modname,
|
||||||
|
fn = M._require,
|
||||||
|
}, M.caller(), modname),
|
||||||
|
}
|
||||||
|
if type(ret[1]) == "table" then
|
||||||
|
M.attach_mod(modname, ret[1])
|
||||||
|
end
|
||||||
|
return unpack(ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param event any (string|array) Event(s) that will trigger the handler (`callback` or `command`).
|
||||||
|
---@param opts vim.api.keyset.create_autocmd Options dict:
|
||||||
|
function M.autocmd(event, opts)
|
||||||
|
if opts and type(opts.callback) == "function" then
|
||||||
|
local name = { type(event) == "string" and event or table.concat(event, "|") }
|
||||||
|
if opts.pattern then
|
||||||
|
name[#name + 1] = type(opts.pattern) == "string" and opts.pattern or table.concat(opts.pattern, "|")
|
||||||
|
end
|
||||||
|
local autocmd = table.concat(name, ":")
|
||||||
|
local trace = { name = "autocmd:" .. autocmd, fn = opts.callback, autocmd = autocmd }
|
||||||
|
opts.callback = function(...)
|
||||||
|
if not M.running then
|
||||||
|
return trace.fn(...)
|
||||||
|
end
|
||||||
|
return M.trace(trace, M.caller(), ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nvim_create_autocmd(event, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param opts snacks.profiler.Config
|
||||||
|
function M.start(opts)
|
||||||
|
assert(not M.running, "Profiler is already enabled")
|
||||||
|
|
||||||
|
-- Clear events
|
||||||
|
M.events = {}
|
||||||
|
|
||||||
|
-- Setup filters and include globals
|
||||||
|
local filter_mod = vim.deepcopy(opts.filter_mod)
|
||||||
|
for _, global in ipairs(opts.globals) do
|
||||||
|
filter_mod[global] = true
|
||||||
|
end
|
||||||
|
M.filter_mod = M.filter(filter_mod)
|
||||||
|
M.filter_fn = M.filter(opts.filter_fn)
|
||||||
|
|
||||||
|
-- Attach to require
|
||||||
|
_G.require = M.require
|
||||||
|
|
||||||
|
-- Attach to autocmds
|
||||||
|
if opts.autocmds then
|
||||||
|
vim.api.nvim_create_autocmd = M.autocmd
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Attach to globals
|
||||||
|
for _, name in ipairs(opts.globals) do
|
||||||
|
M.attach_mod(name, vim.tbl_get(_G, unpack(vim.split(name, ".", { plain = true }))))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Attach to loaded modules
|
||||||
|
---@diagnostic disable-next-line: no-unknown
|
||||||
|
for modname, mod in pairs(package.loaded) do
|
||||||
|
M.attach_mod(modname, mod)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Enable the profiler
|
||||||
|
M.running = true
|
||||||
|
vim.api.nvim_exec_autocmds("User", { pattern = "SnacksProfilerStarted", modeline = false })
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.stop()
|
||||||
|
assert(M.running, "Profiler is not enabled")
|
||||||
|
_G.require = M._require
|
||||||
|
vim.api.nvim_create_autocmd = nvim_create_autocmd
|
||||||
|
M.running = false
|
||||||
|
vim.api.nvim_exec_autocmds("User", { pattern = "SnacksProfilerStopped", modeline = false })
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
382
lua/snacks/profiler/init.lua
Normal file
382
lua/snacks/profiler/init.lua
Normal file
|
@ -0,0 +1,382 @@
|
||||||
|
require("snacks")
|
||||||
|
|
||||||
|
-- ### Traces
|
||||||
|
--
|
||||||
|
---@class snacks.profiler.Trace
|
||||||
|
---@field name string fully qualified name of the function
|
||||||
|
---@field time number time in nanoseconds
|
||||||
|
---@field depth number stack depth
|
||||||
|
---@field [number] snacks.profiler.Trace child traces
|
||||||
|
---@field fname string function name
|
||||||
|
---@field fn function function reference
|
||||||
|
---@field modname? string module name
|
||||||
|
---@field require? string special case for require
|
||||||
|
---@field autocmd? string special case for autocmd
|
||||||
|
---@field count? number number of calls
|
||||||
|
---@field def? snacks.profiler.Loc location of the definition
|
||||||
|
---@field ref? snacks.profiler.Loc location of the reference (caller)
|
||||||
|
---@field loc? snacks.profiler.Loc normalized location
|
||||||
|
|
||||||
|
---@class snacks.profiler.Loc
|
||||||
|
---@field file string path to the file
|
||||||
|
---@field line number line number
|
||||||
|
---@field loc? string normalized location
|
||||||
|
---@field modname? string module name
|
||||||
|
---@field plugin? string plugin name
|
||||||
|
|
||||||
|
-- ### Pick: grouping, filtering and sorting
|
||||||
|
--
|
||||||
|
---@class snacks.profiler.Find
|
||||||
|
---@field structure? boolean show traces as a tree or flat list
|
||||||
|
---@field sort? "time"|"count"|false sort by time or count, or keep original order
|
||||||
|
---@field loc? "def"|"ref" what location to show in the preview
|
||||||
|
---@field group? boolean|snacks.profiler.Field group traces by field
|
||||||
|
---@field filter? snacks.profiler.Filter filter traces by field(s)
|
||||||
|
---@field min_time? number only show grouped traces with `time >= min_time`
|
||||||
|
|
||||||
|
---@class snacks.profiler.Pick: snacks.profiler.Find
|
||||||
|
---@field picker? snacks.profiler.Picker
|
||||||
|
|
||||||
|
---@alias snacks.profiler.Picker "auto"|"fzf-lua"|"telescope"|"trouble"
|
||||||
|
---@alias snacks.profiler.Pick.spec snacks.profiler.Pick|{preset?:string}|fun():snacks.profiler.Pick
|
||||||
|
|
||||||
|
---@alias snacks.profiler.Field
|
||||||
|
---| "name" fully qualified name of the function
|
||||||
|
---| "def" definition
|
||||||
|
---| "ref" reference (caller)
|
||||||
|
---| "require" require
|
||||||
|
---| "autocmd" autocmd
|
||||||
|
---| "modname" module name of the called function
|
||||||
|
---| "def_file" file of the definition
|
||||||
|
---| "def_modname" module name of the definition
|
||||||
|
---| "def_plugin" plugin that defines the function
|
||||||
|
---| "ref_file" file of the reference
|
||||||
|
---| "ref_modname" module name of the reference
|
||||||
|
---| "ref_plugin" plugin that references the function
|
||||||
|
|
||||||
|
---@class snacks.profiler.Filter
|
||||||
|
---@field name? string|boolean fully qualified name of the function
|
||||||
|
---@field def? string|boolean location of the definition
|
||||||
|
---@field ref? string|boolean location of the reference (caller)
|
||||||
|
---@field require? string|boolean special case for require
|
||||||
|
---@field autocmd? string|boolean special case for autocmd
|
||||||
|
---@field modname? string|boolean module name
|
||||||
|
---@field def_file? string|boolean file of the definition
|
||||||
|
---@field def_modname? string|boolean module name of the definition
|
||||||
|
---@field def_plugin? string|boolean plugin that defines the function
|
||||||
|
---@field ref_file? string|boolean file of the reference
|
||||||
|
---@field ref_modname? string|boolean module name of the reference
|
||||||
|
---@field ref_plugin? string|boolean plugin that references the function
|
||||||
|
|
||||||
|
-- ### UI
|
||||||
|
--
|
||||||
|
---@alias snacks.profiler.Badge {icon:string, text:string, padding?:boolean, level?:string}
|
||||||
|
---@alias snacks.profiler.Badge.type "time"|"pct"|"count"|"name"|"trace"
|
||||||
|
|
||||||
|
---@class snacks.profiler.Highlights
|
||||||
|
---@field min_time? number only highlight entries with time >= min_time
|
||||||
|
---@field max_shade? number -- time in ms for the darkest shade
|
||||||
|
---@field badges? snacks.profiler.Badge.type[] badges to show
|
||||||
|
---@field align? "right"|"left"|number align the badges right, left or at a specific column
|
||||||
|
|
||||||
|
-- ### Other
|
||||||
|
--
|
||||||
|
---@class snacks.profiler.Startup
|
||||||
|
---@field event? string
|
||||||
|
---@field pattern? string|string[] pattern to match for the autocmd
|
||||||
|
|
||||||
|
---@alias snacks.profiler.GroupFn fun(entry:snacks.profiler.Trace):{key:string, name?:string}?
|
||||||
|
|
||||||
|
---@class snacks.profiler
|
||||||
|
---@field core snacks.profiler.core
|
||||||
|
---@field loc snacks.profiler.loc
|
||||||
|
---@field tracer snacks.profiler.tracer
|
||||||
|
---@field ui snacks.profiler.ui
|
||||||
|
---@field picker snacks.profiler.picker
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local mods = { core = true, loc = true, tracer = true, ui = true, picker = true }
|
||||||
|
setmetatable(M, {
|
||||||
|
__index = function(t, k)
|
||||||
|
if mods[k] then
|
||||||
|
---@diagnostic disable-next-line: no-unknown
|
||||||
|
t[k] = require("snacks.profiler." .. k)
|
||||||
|
end
|
||||||
|
return rawget(t, k)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
---@class snacks.profiler.Config
|
||||||
|
local defaults = {
|
||||||
|
autocmds = true,
|
||||||
|
runtime = vim.env.VIMRUNTIME, ---@type string
|
||||||
|
-- thresholds for buttons to be shown as info, warn or error
|
||||||
|
-- value is a tuple of [warn, error]
|
||||||
|
thresholds = {
|
||||||
|
time = { 2, 10 },
|
||||||
|
pct = { 10, 20 },
|
||||||
|
count = { 10, 100 },
|
||||||
|
},
|
||||||
|
on_stop = {
|
||||||
|
highlights = true, -- highlight entries after stopping the profiler
|
||||||
|
pick = true, -- show a picker after stopping the profiler (uses the `on_stop` preset)
|
||||||
|
},
|
||||||
|
---@type snacks.profiler.Highlights
|
||||||
|
highlights = {
|
||||||
|
min_time = 0, -- only highlight entries with time > min_time (in ms)
|
||||||
|
max_shade = 20, -- time in ms for the darkest shade
|
||||||
|
badges = { "time", "pct", "count", "trace" },
|
||||||
|
align = 80,
|
||||||
|
},
|
||||||
|
pick = {
|
||||||
|
picker = "auto", ---@type snacks.profiler.Picker
|
||||||
|
---@type snacks.profiler.Badge.type[]
|
||||||
|
badges = { "time", "count", "name" },
|
||||||
|
---@type snacks.profiler.Highlights
|
||||||
|
preview = {
|
||||||
|
badges = { "time", "pct", "count" },
|
||||||
|
align = "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
startup = {
|
||||||
|
event = "VimEnter", -- stop profiler on this event. Defaults to `VimEnter`
|
||||||
|
after = true, -- stop the profiler **after** the event. When false it stops **at** the event
|
||||||
|
pattern = nil, -- pattern to match for the autocmd
|
||||||
|
pick = true, -- show a picker after starting the profiler (uses the `startup` preset)
|
||||||
|
},
|
||||||
|
---@type table<string, snacks.profiler.Pick|fun():snacks.profiler.Pick>
|
||||||
|
presets = {
|
||||||
|
startup = { min_time = 1, sort = false },
|
||||||
|
on_stop = {},
|
||||||
|
filter_by_plugin = function()
|
||||||
|
return { filter = { def_plugin = vim.fn.input("Filter by plugin: ") } }
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
---@type string[]
|
||||||
|
globals = {
|
||||||
|
-- "vim",
|
||||||
|
-- "vim.api",
|
||||||
|
-- "vim.keymap",
|
||||||
|
-- "Snacks.dashboard.Dashboard",
|
||||||
|
},
|
||||||
|
-- filter modules by pattern.
|
||||||
|
-- longest patterns are matched first
|
||||||
|
filter_mod = {
|
||||||
|
default = true, -- default value for unmatched patterns
|
||||||
|
["^vim%."] = false,
|
||||||
|
["mason-core.functional"] = false,
|
||||||
|
["mason-core.functional.data"] = false,
|
||||||
|
["mason-core.optional"] = false,
|
||||||
|
["which-key.state"] = false,
|
||||||
|
},
|
||||||
|
filter_fn = {
|
||||||
|
default = true,
|
||||||
|
["^.*%._[^%.]*$"] = false,
|
||||||
|
["trouble.filter.is"] = false,
|
||||||
|
["trouble.item.__index"] = false,
|
||||||
|
["which-key.node.__index"] = false,
|
||||||
|
["smear_cursor.draw.wo"] = false,
|
||||||
|
["^ibl%.utils%."] = false,
|
||||||
|
},
|
||||||
|
-- stylua: ignore
|
||||||
|
icons = {
|
||||||
|
time = " ",
|
||||||
|
pct = " ",
|
||||||
|
count = " ",
|
||||||
|
require = " ",
|
||||||
|
modname = " ",
|
||||||
|
plugin = " ",
|
||||||
|
autocmd = "⚡",
|
||||||
|
file = " ",
|
||||||
|
fn = " ",
|
||||||
|
status = " ",
|
||||||
|
},
|
||||||
|
debug = false,
|
||||||
|
}
|
||||||
|
|
||||||
|
M.config = Snacks.config.get("profiler", defaults)
|
||||||
|
|
||||||
|
local attached_debug = false
|
||||||
|
local loaded = false
|
||||||
|
|
||||||
|
-- Toggle the profiler
|
||||||
|
function M.toggle()
|
||||||
|
if M.core.running then
|
||||||
|
M.stop()
|
||||||
|
else
|
||||||
|
M.start()
|
||||||
|
end
|
||||||
|
return M.core.running
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Statusline component
|
||||||
|
function M.status()
|
||||||
|
return {
|
||||||
|
function()
|
||||||
|
return ("%s %d events"):format(M.config.icons.status, #M.core.events)
|
||||||
|
end,
|
||||||
|
color = "DiagnosticError",
|
||||||
|
cond = function()
|
||||||
|
return M.core.running
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Start the profiler
|
||||||
|
---@param opts? snacks.profiler.Config
|
||||||
|
function M.start(opts)
|
||||||
|
if M.core.running then
|
||||||
|
return Snacks.notify.warn("Profiler is already enabled")
|
||||||
|
end
|
||||||
|
M.config = Snacks.config.get("profiler", defaults, opts)
|
||||||
|
|
||||||
|
M.highlight(false)
|
||||||
|
M.core.start(M.config)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function load()
|
||||||
|
if loaded then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
loaded = true
|
||||||
|
M.tracer.load() -- load traces
|
||||||
|
M.loc.load() -- add and normalize locations
|
||||||
|
M.ui.load() -- load highlights
|
||||||
|
vim.api.nvim_exec_autocmds("User", { pattern = "SnacksProfilerLoaded", modeline = false })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Stop the profiler
|
||||||
|
---@param opts? {highlights?:boolean, pick?:snacks.profiler.Pick.spec}
|
||||||
|
function M.stop(opts)
|
||||||
|
if not M.core.running then
|
||||||
|
return Snacks.notify.warn("Profiler is not enabled")
|
||||||
|
end
|
||||||
|
M.core.stop()
|
||||||
|
opts = vim.tbl_extend("force", {}, M.config.on_stop, opts or {})
|
||||||
|
if opts.pick == true then
|
||||||
|
opts.pick = M.config.presets.on_stop or {}
|
||||||
|
elseif opts.pick == false then
|
||||||
|
opts.pick = nil
|
||||||
|
end
|
||||||
|
loaded = false
|
||||||
|
vim.schedule(function()
|
||||||
|
load()
|
||||||
|
if opts.highlights then
|
||||||
|
M.highlight(true)
|
||||||
|
end
|
||||||
|
if opts.pick then
|
||||||
|
M.pick(opts.pick)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if the profiler is running
|
||||||
|
function M.running()
|
||||||
|
return M.core.running
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Profile the profiler
|
||||||
|
---@private
|
||||||
|
function M.debug()
|
||||||
|
if not M.core.running then
|
||||||
|
return Snacks.notify.warn("Profiler is not enabled")
|
||||||
|
end
|
||||||
|
if loaded then
|
||||||
|
return Snacks.notify.warn("Profiler is already loaded")
|
||||||
|
end
|
||||||
|
if not attached_debug then
|
||||||
|
attached_debug = true
|
||||||
|
M.core.skip(M.core.caller)
|
||||||
|
M.core.skip(M.core.trace)
|
||||||
|
M.core.skip(M.loc.loc)
|
||||||
|
M.core.skip(M.loc.norm)
|
||||||
|
M.core.skip(M.loc.realpath)
|
||||||
|
M.core.attach_mod("vim.fs", vim.fs, { force = true })
|
||||||
|
M.core.attach_mod("snacks.profiler", M, { force = true })
|
||||||
|
for mod in pairs(mods) do
|
||||||
|
M.core.attach_mod("snacks.profiler." .. mod, M[mod], { force = true })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local event_count = #M.core.events
|
||||||
|
local me = M.core.me
|
||||||
|
M.core.me = "__ignore__"
|
||||||
|
load()
|
||||||
|
M.pick({ picker = "foo", group = "name", structure = true })
|
||||||
|
M.core.events = vim.list_slice(M.core.events, event_count)
|
||||||
|
loaded = false
|
||||||
|
M.stop()
|
||||||
|
M.core.me = me
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Group and filter traces
|
||||||
|
---@param opts snacks.profiler.Find
|
||||||
|
function M.find(opts)
|
||||||
|
load()
|
||||||
|
return M.tracer.find(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Group and filter traces and open a picker
|
||||||
|
---@param opts? snacks.profiler.Pick.spec
|
||||||
|
function M.pick(opts)
|
||||||
|
load()
|
||||||
|
opts = type(opts) == "function" and opts() or opts or {}
|
||||||
|
if opts.preset then
|
||||||
|
local preset = M.config.presets[opts.preset]
|
||||||
|
preset = type(preset) == "function" and preset() or preset
|
||||||
|
opts = vim.tbl_deep_extend("force", {}, preset, opts)
|
||||||
|
end
|
||||||
|
---@cast opts snacks.profiler.Pick
|
||||||
|
return M.picker.open(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Open a scratch buffer with the profiler picker options
|
||||||
|
function M.scratch()
|
||||||
|
return Snacks.scratch({
|
||||||
|
ft = "lua",
|
||||||
|
icon = " ",
|
||||||
|
name = "Profiler Picker Options",
|
||||||
|
template = ("---@module 'snacks'\n\nSnacks.profiler.pick(%s)"):format(vim.inspect({
|
||||||
|
structure = true,
|
||||||
|
group = "name",
|
||||||
|
sort = "time",
|
||||||
|
min_time = 1,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Start the profiler on startup, and stop it after the event has been triggered.
|
||||||
|
---@param opts snacks.profiler.Config
|
||||||
|
function M.startup(opts)
|
||||||
|
local event, pattern = M.config.startup.event or "VimEnter", M.config.startup.pattern
|
||||||
|
if event == "VeryLazy" then
|
||||||
|
event, pattern = "User", event
|
||||||
|
end
|
||||||
|
local cb = function()
|
||||||
|
local pick = M.config.startup.pick and M.config.presets.startup
|
||||||
|
Snacks.profiler.stop({ pick = pick })
|
||||||
|
end
|
||||||
|
if M.config.startup.after then
|
||||||
|
cb = vim.schedule_wrap(cb)
|
||||||
|
end
|
||||||
|
vim.api.nvim_create_autocmd(event, { pattern = pattern, once = true, callback = cb })
|
||||||
|
M.start(opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Toggle the profiler highlights
|
||||||
|
---@param enable? boolean
|
||||||
|
function M.highlight(enable)
|
||||||
|
if enable == nil then
|
||||||
|
enable = not M.ui.enabled
|
||||||
|
end
|
||||||
|
if enable == M.ui.enabled then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if enable then
|
||||||
|
load()
|
||||||
|
M.ui.show()
|
||||||
|
else
|
||||||
|
M.ui.hide()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
139
lua/snacks/profiler/loc.lua
Normal file
139
lua/snacks/profiler/loc.lua
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
---@class snacks.profiler.loc
|
||||||
|
---@field vim_runtime string
|
||||||
|
---@field user_runtime string
|
||||||
|
---@field user_config string
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local fun_cache = {} ---@type table<function, snacks.profiler.Loc|false>
|
||||||
|
local norm_cache = {} ---@type table<string, table<number,snacks.profiler.Loc>>
|
||||||
|
local path_cache = {} ---@type table<string, string>
|
||||||
|
local ts_cache = {} ---@type table<string, table<string, snacks.profiler.Loc>>
|
||||||
|
local ts_query ---@type vim.treesitter.Query?
|
||||||
|
|
||||||
|
-- add and normalize locations
|
||||||
|
function M.load()
|
||||||
|
local opts = Snacks.profiler.config
|
||||||
|
M.vim_runtime = M.realpath(vim.env.VIMRUNTIME)
|
||||||
|
M.user_runtime = M.realpath(opts.runtime or M.vim_runtime)
|
||||||
|
M.user_config = M.realpath(vim.fn.stdpath("config") .. "")
|
||||||
|
|
||||||
|
Snacks.profiler.tracer.walk(function(entry)
|
||||||
|
entry.def = M.loc(entry)
|
||||||
|
entry.ref = entry.ref and M.norm(entry.ref) or nil
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get the location at the cursor
|
||||||
|
function M.current()
|
||||||
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||||
|
return M.norm({ file = vim.api.nvim_buf_get_name(0), line = cursor[1] })
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the real path of a file
|
||||||
|
---@param path string
|
||||||
|
function M.realpath(path)
|
||||||
|
if path_cache[path] then
|
||||||
|
return path_cache[path]
|
||||||
|
end
|
||||||
|
path = vim.fs.normalize(path, { expand_env = false })
|
||||||
|
path_cache[path] = vim.fs.normalize(vim.uv.fs_realpath(path) or path, { expand_env = false, _fast = true })
|
||||||
|
return path_cache[path]
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param loc snacks.profiler.Loc
|
||||||
|
function M.norm(loc)
|
||||||
|
local file, line = loc.file, loc.line
|
||||||
|
local ret = norm_cache[file] and norm_cache[file][line]
|
||||||
|
if not ret then
|
||||||
|
ret = M._norm(loc)
|
||||||
|
norm_cache[file] = norm_cache[file] or {}
|
||||||
|
norm_cache[file][line] = ret
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param loc snacks.profiler.Loc
|
||||||
|
function M._norm(loc)
|
||||||
|
if loc.file:sub(1, 4) == "vim/" then
|
||||||
|
loc.file = M.user_runtime .. "/lua/" .. loc.file
|
||||||
|
elseif loc.file:find("runtime", 1, true) then
|
||||||
|
if loc.file:sub(1, #M.vim_runtime) == M.vim_runtime then
|
||||||
|
loc.file = M.user_runtime .. "/" .. loc.file:sub(#M.vim_runtime + 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
loc.file = M.realpath(loc.file)
|
||||||
|
loc.line = loc.line == 0 and 1 or loc.line
|
||||||
|
loc.loc = ("%s:%d"):format(loc.file, loc.line)
|
||||||
|
if loc.file:find(M.user_config, 1, true) == 1 then
|
||||||
|
local relpath = loc.file:sub(#M.user_config + 2)
|
||||||
|
local modpath = relpath:match("^lua/(.*)%.lua$")
|
||||||
|
loc.modname = modpath and modpath:gsub("/", "."):gsub("%.init$", "") or "vimrc"
|
||||||
|
loc.plugin = "user"
|
||||||
|
else
|
||||||
|
local plugin, modpath = loc.file:match("/([^/]+)/lua/(.*)%.lua$")
|
||||||
|
if plugin and modpath then
|
||||||
|
plugin = plugin == "runtime" and "nvim" or plugin
|
||||||
|
loc.plugin = plugin
|
||||||
|
loc.modname = modpath:gsub("/", "."):gsub("%.init$", "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return loc
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param entry snacks.profiler.Trace
|
||||||
|
---@return snacks.profiler.Loc?
|
||||||
|
function M.loc(entry)
|
||||||
|
local ret = fun_cache[entry.fn]
|
||||||
|
if ret == nil then
|
||||||
|
local info = debug.getinfo(entry.fn, "S")
|
||||||
|
if info and info.what ~= "C" then
|
||||||
|
ret = { file = info.source:sub(2), line = info.linedefined }
|
||||||
|
if entry.fname and ret.file:sub(1, 4) == "vim/" then
|
||||||
|
ret.file = M.user_runtime .. "/lua/" .. ret.file
|
||||||
|
local ts_loc = M.ts_locs(ret.file)[entry.fname]
|
||||||
|
if ts_loc then
|
||||||
|
ret.file, ret.line = ts_loc.file, ts_loc.line
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ret = M.norm(ret)
|
||||||
|
end
|
||||||
|
fun_cache[entry.fn] = ret or false
|
||||||
|
end
|
||||||
|
return ret or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param file string
|
||||||
|
function M.ts_locs(file)
|
||||||
|
if ts_cache[file] then
|
||||||
|
return ts_cache[file]
|
||||||
|
end
|
||||||
|
ts_query = ts_query
|
||||||
|
or vim.treesitter.query.parse(
|
||||||
|
"lua",
|
||||||
|
[[((function_declaration name: (_) @fun_name) @fun
|
||||||
|
(#has-parent? @fun chunk))
|
||||||
|
((return_statement (expression_list (identifier) @ret_name)) @ret
|
||||||
|
(#has-parent? @ret chunk))]]
|
||||||
|
)
|
||||||
|
local source = table.concat(vim.fn.readfile(file), "\n")
|
||||||
|
local parser = vim.treesitter.get_string_parser(source, "lua")
|
||||||
|
parser:parse()
|
||||||
|
local ret, ret_name = {}, nil ---@type table<string, snacks.profiler.Loc>, string?
|
||||||
|
local funs = {} ---@type table<string, number>
|
||||||
|
for id, node in ts_query:iter_captures(parser:trees()[1]:root(), source) do
|
||||||
|
local name = ts_query.captures[id]
|
||||||
|
if name == "fun_name" then
|
||||||
|
funs[vim.treesitter.get_node_text(node, source)] = node:start() + 1
|
||||||
|
elseif name == "ret_name" then
|
||||||
|
ret_name = vim.treesitter.get_node_text(node, source)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for fname, line in pairs(funs) do
|
||||||
|
fname = ret_name and fname:gsub("^" .. ret_name .. "%.", "") or fname
|
||||||
|
ret[fname] = { file = file, line = line }
|
||||||
|
end
|
||||||
|
ts_cache[file] = ret
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
178
lua/snacks/profiler/picker.lua
Normal file
178
lua/snacks/profiler/picker.lua
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
---@class snacks.profiler.picker
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
---@param opts? snacks.profiler.Pick
|
||||||
|
function M.open(opts)
|
||||||
|
opts = opts or {}
|
||||||
|
|
||||||
|
local picker = opts and opts.picker or Snacks.profiler.config.pick.picker
|
||||||
|
if picker == "auto" then
|
||||||
|
if pcall(require, "fzf-lua") then
|
||||||
|
picker = "fzf-lua"
|
||||||
|
elseif pcall(require, "telescope") then
|
||||||
|
picker = "telescope"
|
||||||
|
elseif pcall(require, "trouble") then
|
||||||
|
picker = "trouble"
|
||||||
|
else
|
||||||
|
return Snacks.notify.error("No picker found")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- special case for trouble, since it does its own thing
|
||||||
|
if picker == "trouble" then
|
||||||
|
return require("trouble").open({ mode = "profiler", params = opts })
|
||||||
|
end
|
||||||
|
|
||||||
|
local traces, _, fopts = Snacks.profiler.tracer.find(opts)
|
||||||
|
|
||||||
|
---@alias snacks.profiler.Pick.entry {badges:snacks.profiler.Badge[], path:string, line:number, col:number, text:string[][]}|snacks.profiler.Trace
|
||||||
|
---@type snacks.profiler.Pick.entry[]
|
||||||
|
local entries = {}
|
||||||
|
local widths = {} ---@type number[]
|
||||||
|
for _, trace in ipairs(traces) do
|
||||||
|
local badges = Snacks.profiler.ui.badges(trace, {
|
||||||
|
badges = Snacks.profiler.config.pick.badges,
|
||||||
|
indent = fopts.group == false or fopts.structure,
|
||||||
|
})
|
||||||
|
for b, badge in ipairs(badges) do
|
||||||
|
widths[b] = math.max(widths[b] or 0, vim.api.nvim_strwidth(badge.text))
|
||||||
|
end
|
||||||
|
local loc = trace.loc
|
||||||
|
table.insert(
|
||||||
|
entries,
|
||||||
|
setmetatable({ badges = badges, path = loc and loc.file, line = loc and loc.line, col = 1 }, { __index = trace })
|
||||||
|
)
|
||||||
|
end
|
||||||
|
for _, entry in ipairs(entries) do
|
||||||
|
entry.text = Snacks.profiler.ui.format(entry.badges, { widths = widths })
|
||||||
|
for _, text in ipairs(entry.text) do
|
||||||
|
if text[2] == "Normal" or text[2] == "SnacksProfilerBadgeTrace" then
|
||||||
|
text[2] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #entries == 0 then
|
||||||
|
return Snacks.notify.warn("No traces found")
|
||||||
|
end
|
||||||
|
|
||||||
|
if picker == "telescope" then
|
||||||
|
M.telescope(entries)
|
||||||
|
elseif picker == "fzf-lua" then
|
||||||
|
M.fzf_lua(entries)
|
||||||
|
else
|
||||||
|
return Snacks.notify.error("Not a valid picker `" .. picker .. "`")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param entries snacks.profiler.Pick.entry[]
|
||||||
|
function M.telescope(entries)
|
||||||
|
local finder = require("telescope.finders").new_table({
|
||||||
|
results = entries,
|
||||||
|
---@param entry snacks.profiler.Pick.entry
|
||||||
|
entry_maker = function(entry)
|
||||||
|
local text, hl = {}, {} ---@type string[], string[][]
|
||||||
|
local col = 0
|
||||||
|
for _, t in ipairs(entry.text) do
|
||||||
|
text[#text + 1] = t[1]
|
||||||
|
if t[2] then
|
||||||
|
table.insert(hl, { { col, col + #t[1] }, t[2] })
|
||||||
|
end
|
||||||
|
col = col + #t[1]
|
||||||
|
end
|
||||||
|
return vim.tbl_extend("force", entry, {
|
||||||
|
lnum = entry.line,
|
||||||
|
ordinal = entry.name,
|
||||||
|
display = function()
|
||||||
|
return table.concat(text), hl
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
local conf = require("telescope.config").values
|
||||||
|
local topts = {}
|
||||||
|
local previewer = require("telescope.previewers").new_buffer_previewer({
|
||||||
|
title = "File Preview",
|
||||||
|
define_preview = function(self, entry, _status)
|
||||||
|
conf.buffer_previewer_maker(entry.path, self.state.bufnr, {
|
||||||
|
bufname = self.state.bufname,
|
||||||
|
winid = self.state.winid,
|
||||||
|
callback = function(bufnr)
|
||||||
|
Snacks.util.wo(self.state.winid, { cursorline = true })
|
||||||
|
Snacks.profiler.ui.highlight(
|
||||||
|
self.state.bufnr,
|
||||||
|
vim.tbl_extend("force", {}, Snacks.profiler.config.pick.preview, { file = entry.path })
|
||||||
|
)
|
||||||
|
pcall(vim.api.nvim_win_set_cursor, self.state.winid, { entry.lnum, 0 })
|
||||||
|
vim.api.nvim_buf_call(bufnr, function()
|
||||||
|
vim.cmd("norm! zz")
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
require("telescope.pickers")
|
||||||
|
.new(topts, {
|
||||||
|
results_title = "Snacks Profiler",
|
||||||
|
prompt_title = "Filter",
|
||||||
|
finder = finder,
|
||||||
|
previewer = previewer,
|
||||||
|
sorter = conf.generic_sorter(topts),
|
||||||
|
})
|
||||||
|
:find()
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param entries snacks.profiler.Pick.entry[]
|
||||||
|
function M.fzf_lua(entries)
|
||||||
|
local fzf = require("fzf-lua")
|
||||||
|
local builtin = require("fzf-lua.previewer.builtin")
|
||||||
|
local previewer = builtin.buffer_or_file:extend()
|
||||||
|
function previewer:new(o, fzf_opts, fzf_win)
|
||||||
|
previewer.super.new(self, o, fzf_opts, fzf_win)
|
||||||
|
setmetatable(self, previewer)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
function previewer:parse_entry(entry_str)
|
||||||
|
local id = tonumber(entry_str:match("^(%d+)") or "0")
|
||||||
|
return entries[id] or {}
|
||||||
|
end
|
||||||
|
function previewer:preview_buf_post(entry, min_winopts)
|
||||||
|
builtin.buffer_or_file.preview_buf_post(self, entry, min_winopts)
|
||||||
|
Snacks.profiler.ui.highlight(
|
||||||
|
self.preview_bufnr,
|
||||||
|
vim.tbl_extend("force", {}, Snacks.profiler.config.pick.preview, { file = entry.path })
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local contents = {} ---@type string[]
|
||||||
|
for e, entry in ipairs(entries) do
|
||||||
|
local display = { e .. " " } ---@type string[]
|
||||||
|
for _, text in ipairs(entry.text) do
|
||||||
|
display[#display + 1] = text[2] and fzf.utils.ansi_from_hl(text[2], text[1]) or text[1]
|
||||||
|
end
|
||||||
|
contents[#contents + 1] = table.concat(display)
|
||||||
|
end
|
||||||
|
|
||||||
|
require("fzf-lua").fzf_exec(contents, {
|
||||||
|
previewer = previewer,
|
||||||
|
-- multiline = true,
|
||||||
|
actions = {
|
||||||
|
-- Use fzf-lua builtin actions or your own handler
|
||||||
|
["default"] = function(selection, fzf_opts)
|
||||||
|
fzf.actions.file_edit(
|
||||||
|
vim.tbl_map(function(sel)
|
||||||
|
local id = tonumber(sel:match("^(%d+)") or "0")
|
||||||
|
return entries[id].path .. ":" .. entries[id].line
|
||||||
|
end, selection),
|
||||||
|
fzf_opts
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
fzf_opts = {
|
||||||
|
["--no-multi"] = "",
|
||||||
|
["--with-nth"] = "2..",
|
||||||
|
["--no-sort"] = true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return M
|
228
lua/snacks/profiler/tracer.lua
Normal file
228
lua/snacks/profiler/tracer.lua
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
---@alias snacks.profiler.Trace.opts snacks.profiler.Trace|{id?:number, pid?:number, time?:number, depth?:number}
|
||||||
|
---@alias snacks.profiler.Event {id:number, pid:number, start:number, stop:number, measurements:number, ref?:snacks.profiler.Loc, idx:number, opts:snacks.profiler.Trace.opts}
|
||||||
|
---@alias snacks.profiler.Node {group:string, trace:snacks.profiler.Trace, children:table<string|number,snacks.profiler.Node>, order:(string|number)[]}
|
||||||
|
|
||||||
|
---@class snacks.profiler.tracer
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
M.root = {} ---@type snacks.profiler.Trace[]
|
||||||
|
|
||||||
|
function M.load()
|
||||||
|
M.root = {}
|
||||||
|
local traces = {} ---@type snacks.profiler.Trace[]
|
||||||
|
for _, event in ipairs(Snacks.profiler.core.events) do
|
||||||
|
local trace = setmetatable({}, { __index = event.opts })
|
||||||
|
trace.id = event.id
|
||||||
|
trace.pid = event.pid
|
||||||
|
trace.ref = event.ref
|
||||||
|
trace.depth = 0
|
||||||
|
if event.stop then
|
||||||
|
trace.time = event.stop - event.start
|
||||||
|
traces[event.id] = trace
|
||||||
|
if traces[event.pid] then
|
||||||
|
trace.depth = traces[event.pid].depth + 1
|
||||||
|
table.insert(traces[event.pid], trace)
|
||||||
|
elseif trace.time then
|
||||||
|
table.insert(M.root, trace)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param on_start? fun(entry:snacks.profiler.Trace):any?
|
||||||
|
---@param on_end? fun(entry:snacks.profiler.Trace, start?:any)
|
||||||
|
function M.walk(on_start, on_end)
|
||||||
|
---@param entry snacks.profiler.Trace
|
||||||
|
local function walk(entry)
|
||||||
|
local start = on_start and on_start(entry)
|
||||||
|
for _, child in ipairs(entry) do
|
||||||
|
walk(child)
|
||||||
|
end
|
||||||
|
if on_end then
|
||||||
|
on_end(entry, start)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, child in ipairs(M.root) do
|
||||||
|
walk(child)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param fn snacks.profiler.GroupFn
|
||||||
|
---@param opts? {structure?:boolean, sort?:"time"|"count"}
|
||||||
|
function M.group(fn, opts)
|
||||||
|
opts = opts or {}
|
||||||
|
---@type snacks.profiler.Node[]
|
||||||
|
local nodes = { { children = {}, order = {} } } -- root node
|
||||||
|
|
||||||
|
---@param entry snacks.profiler.Trace
|
||||||
|
M.walk(function(entry)
|
||||||
|
local group = fn(entry)
|
||||||
|
if group then
|
||||||
|
local key, parent, recursive = group.key, nodes[1], false
|
||||||
|
for n = 2, #nodes do
|
||||||
|
local node = nodes[n]
|
||||||
|
if node.group == key then
|
||||||
|
recursive = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
parent = opts.structure and node or parent
|
||||||
|
end
|
||||||
|
local node = parent.children[key]
|
||||||
|
if not node then
|
||||||
|
local trace = vim.tbl_extend("force", { time = 0, count = 0, name = key, depth = #nodes - 1 }, group)
|
||||||
|
node = { group = key, trace = trace, children = {}, order = {} } ---@type snacks.profiler.Node
|
||||||
|
---@diagnostic disable-next-line: no-unknown
|
||||||
|
parent.children[key] = node
|
||||||
|
table.insert(parent.order, key)
|
||||||
|
end
|
||||||
|
if not recursive then
|
||||||
|
table.insert(nodes, node)
|
||||||
|
node.trace.time = node.trace.time + entry.time
|
||||||
|
end
|
||||||
|
node.trace.count = node.trace.count + 1
|
||||||
|
table.insert(node.trace, entry)
|
||||||
|
return not recursive
|
||||||
|
end
|
||||||
|
end, function(_, start)
|
||||||
|
if start then
|
||||||
|
table.remove(nodes)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert(#nodes == 1, "node stack not empty")
|
||||||
|
return nodes[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param node snacks.profiler.Node
|
||||||
|
---@param opts? snacks.profiler.Find
|
||||||
|
function M.flatten(node, opts)
|
||||||
|
opts = opts or {}
|
||||||
|
local ret = {} ---@type snacks.profiler.Trace[]
|
||||||
|
---@param n snacks.profiler.Node
|
||||||
|
local function walk(n)
|
||||||
|
if n.trace and (n.trace.time / 1e6 >= (opts.min_time or 0)) then
|
||||||
|
table.insert(ret, n.trace)
|
||||||
|
end
|
||||||
|
if opts.sort then
|
||||||
|
local children = vim.tbl_values(n.children) ---@type snacks.profiler.Node[]
|
||||||
|
if opts.sort == "time" then
|
||||||
|
table.sort(children, function(a, b)
|
||||||
|
return a.trace.time > b.trace.time
|
||||||
|
end)
|
||||||
|
elseif opts.sort == "count" then
|
||||||
|
table.sort(children, function(a, b)
|
||||||
|
return a.trace.count > b.trace.count
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
for _, child in ipairs(children) do
|
||||||
|
walk(child)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for _, key in ipairs(n.order) do
|
||||||
|
walk(n.children[key])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
walk(node)
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param opts snacks.profiler.Find
|
||||||
|
function M.find(opts)
|
||||||
|
opts = opts or {}
|
||||||
|
opts = vim.tbl_extend("force", {
|
||||||
|
group = "name",
|
||||||
|
structure = opts.group ~= false,
|
||||||
|
sort = (opts.group ~= false) and "time",
|
||||||
|
}, opts or {})
|
||||||
|
opts.group = opts.group == true and "name" or opts.group
|
||||||
|
opts.sort = opts.sort == true and "time" or opts.sort
|
||||||
|
---@cast opts snacks.profiler.Find
|
||||||
|
local key_parts = {} ---@type table<string, string[]>
|
||||||
|
local id = 0
|
||||||
|
|
||||||
|
---@param entry snacks.profiler.Trace
|
||||||
|
---@param key string|false
|
||||||
|
local function get(entry, key)
|
||||||
|
if key == false then
|
||||||
|
id = id + 1
|
||||||
|
return tostring(id), entry.name
|
||||||
|
end
|
||||||
|
local parts = key_parts[key]
|
||||||
|
if not parts then
|
||||||
|
parts = vim.split(key, "[_%.]")
|
||||||
|
if #parts == 1 and (parts[1] == "ref" or parts[1] == "def") then
|
||||||
|
parts[2] = "loc"
|
||||||
|
end
|
||||||
|
key_parts[key] = parts
|
||||||
|
end
|
||||||
|
local value = vim.tbl_get(entry, unpack(parts)) ---@type string?
|
||||||
|
if not value then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local name, loc = value, entry.def
|
||||||
|
if parts[1] == "ref" or parts[1] == "require" then
|
||||||
|
loc = entry.ref
|
||||||
|
elseif parts[1] == "name" and entry.require then
|
||||||
|
loc = entry.ref
|
||||||
|
end
|
||||||
|
if parts[2] == "def" or parts[1] == "name" then
|
||||||
|
name = entry.name
|
||||||
|
else
|
||||||
|
name = parts[#parts] .. ":" .. value
|
||||||
|
end
|
||||||
|
return value, name, loc
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Build the filter
|
||||||
|
local filter = {} ---@type table<string, string|boolean>
|
||||||
|
local current ---@type snacks.profiler.Trace?
|
||||||
|
for k, v in pairs(opts.filter or {}) do
|
||||||
|
if v == true then
|
||||||
|
-- If the value is true, then we want the current location
|
||||||
|
if k:find("[rd]ef") then
|
||||||
|
if not current then
|
||||||
|
local loc = Snacks.profiler.loc.current()
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
current = { def = loc, ref = loc }
|
||||||
|
end
|
||||||
|
v = get(current, k) or false
|
||||||
|
else -- match all
|
||||||
|
v = "^.*$"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
filter[k] = v
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param entry snacks.profiler.Trace
|
||||||
|
local function match(entry)
|
||||||
|
for key, m in pairs(filter) do
|
||||||
|
local value = get(entry, key) or false
|
||||||
|
if type(m) == "string" and m:sub(1, 1) == "^" then
|
||||||
|
if not (value and value:find(m)) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
elseif value ~= m then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type snacks.profiler.GroupFn
|
||||||
|
local group_fn = function(entry)
|
||||||
|
if opts.filter and not match(entry) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local key, name, loc = get(entry, opts.group --[[@as string|false]])
|
||||||
|
if key then
|
||||||
|
loc = opts.loc and entry[opts.loc] or loc or entry.def or entry.ref
|
||||||
|
return { key = key, name = name, loc = loc, ref = entry.ref, def = entry.def }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local node = M.group(group_fn, opts)
|
||||||
|
return M.flatten(node, opts), node, opts
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
262
lua/snacks/profiler/ui.lua
Normal file
262
lua/snacks/profiler/ui.lua
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
---@class snacks.profiler.ui
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
M.highlights = {} ---@type table<string, table<number, snacks.profiler.Trace>>
|
||||||
|
M.max_time = 0
|
||||||
|
M.ns = vim.api.nvim_create_namespace("snacks_profiler")
|
||||||
|
M.shades = 20
|
||||||
|
M.enabled = true
|
||||||
|
M.max_time = 0
|
||||||
|
|
||||||
|
---@type table<string, fun(entry:snacks.profiler.Trace):snacks.profiler.Badge>
|
||||||
|
M.badge_formats = {
|
||||||
|
time = function(entry)
|
||||||
|
local ms = entry.time / 1e6
|
||||||
|
return { icon = Snacks.profiler.config.icons.time, text = ("%.2f ms"):format(ms), level = M.get_level(ms, "time") }
|
||||||
|
end,
|
||||||
|
pct = function(entry)
|
||||||
|
local pct = entry.time / M.max_time * 100
|
||||||
|
return { icon = Snacks.profiler.config.icons.pct, text = ("%d%%"):format(pct), level = M.get_level(pct, "pct") }
|
||||||
|
end,
|
||||||
|
count = function(entry)
|
||||||
|
local count = entry.count or 1
|
||||||
|
return { icon = " ", text = ("%d"):format(count), level = M.get_level(count, "count") }
|
||||||
|
end,
|
||||||
|
trace = function(entry)
|
||||||
|
local field, value = entry.name:match("^(%w+):(.*)$") ---@type string?, string?
|
||||||
|
value = field == "file" and vim.fn.fnamemodify(value, ":~:.") or value
|
||||||
|
value = field == "require" and ("require(%q)"):format(value) or value
|
||||||
|
value = field == "autocmd" and ("autocmd %s"):format(value) or value
|
||||||
|
value = Snacks.profiler.config.icons[field] and value or entry.name
|
||||||
|
return {
|
||||||
|
icon = Snacks.profiler.config.icons[field] or Snacks.profiler.config.icons.fn,
|
||||||
|
text = value,
|
||||||
|
padding = false,
|
||||||
|
level = "Trace",
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
function M.toggle()
|
||||||
|
if M.enabled then
|
||||||
|
M.hide()
|
||||||
|
else
|
||||||
|
M.show()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.hide()
|
||||||
|
assert(M.enabled, "Highlights are not enabled")
|
||||||
|
M.enabled = false
|
||||||
|
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||||
|
if vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].buftype == "" then
|
||||||
|
vim.api.nvim_buf_clear_namespace(buf, M.ns, 0, -1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.show()
|
||||||
|
assert(not M.enabled, "Highlights are already enabled")
|
||||||
|
M.enabled = true
|
||||||
|
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||||
|
if vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].buftype == "" then
|
||||||
|
M.highlight(buf, Snacks.profiler.config.highlights)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
vim.api.nvim_create_autocmd("BufReadPost", {
|
||||||
|
group = vim.api.nvim_create_augroup("snacks_profiler_highlights", { clear = true }),
|
||||||
|
callback = function(ev)
|
||||||
|
if M.enabled then
|
||||||
|
M.highlight(ev.buf, Snacks.profiler.config.highlights)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param trace snacks.profiler.Trace
|
||||||
|
function M.dump(trace)
|
||||||
|
local ret = {}
|
||||||
|
---@diagnostic disable-next-line: no-unknown
|
||||||
|
for k, v in pairs(trace) do
|
||||||
|
if type(k) == "string" then
|
||||||
|
---@diagnostic disable-next-line: no-unknown
|
||||||
|
ret[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.load()
|
||||||
|
M.highlights = {}
|
||||||
|
M.max_time = 10 * 1e6
|
||||||
|
M.colors()
|
||||||
|
local groups = {
|
||||||
|
defs = Snacks.profiler.tracer.find({ group = "def", structure = false, sort = false }),
|
||||||
|
refs = Snacks.profiler.tracer.find({ group = "ref", structure = false, sort = false }),
|
||||||
|
}
|
||||||
|
for group, entries in pairs(groups) do
|
||||||
|
for _, entry in pairs(entries) do
|
||||||
|
local loc = entry.loc
|
||||||
|
if loc then
|
||||||
|
---@diagnostic disable-next-line: inject-field
|
||||||
|
entry._group = group
|
||||||
|
M.max_time = math.max(M.max_time, entry.time)
|
||||||
|
M.highlights[loc.file] = M.highlights[loc.file] or {}
|
||||||
|
if Snacks.profiler.config.debug and M.highlights[loc.file][loc.line] then
|
||||||
|
local old = M.highlights[loc.file][loc.line]
|
||||||
|
Snacks.debug.inspect({ group = group, old = M.dump(old), new = M.dump(entry) })
|
||||||
|
end
|
||||||
|
M.highlights[loc.file][loc.line] = entry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.get_level(value, t)
|
||||||
|
return value > Snacks.profiler.config.thresholds[t][2] and "Error"
|
||||||
|
or value > Snacks.profiler.config.thresholds[t][1] and "Warn"
|
||||||
|
or "Info"
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param entry snacks.profiler.Trace
|
||||||
|
---@param opts? { badges?: snacks.profiler.Badge.type[], indent?: boolean }
|
||||||
|
---@return snacks.profiler.Badge[]
|
||||||
|
function M.badges(entry, opts)
|
||||||
|
opts = opts or {}
|
||||||
|
opts.badges = opts.badges or { "time", "pct", "count", "name", "trace" }
|
||||||
|
local ret = {} ---@type snacks.profiler.Badge[]
|
||||||
|
local done = {} ---@type table<string, boolean>
|
||||||
|
local indented = false
|
||||||
|
for _, b in ipairs(opts.badges) do
|
||||||
|
if b == "trace" or b == "name" then
|
||||||
|
local entries = {} ---@type snacks.profiler.Trace[]
|
||||||
|
if b == "name" then
|
||||||
|
table.insert(entries, entry)
|
||||||
|
end
|
||||||
|
if b == "trace" then
|
||||||
|
vim.list_extend(entries, entry)
|
||||||
|
end
|
||||||
|
for _, e in ipairs(entries) do
|
||||||
|
if not done[e.name] then
|
||||||
|
done[e.name] = true
|
||||||
|
local badge = M.badge_formats.trace(e)
|
||||||
|
if opts.indent and not indented then
|
||||||
|
indented = true
|
||||||
|
badge.text = (" "):rep(e.depth) .. badge.text
|
||||||
|
end
|
||||||
|
table.insert(ret, badge)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(ret, M.badge_formats[b](entry))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param badges snacks.profiler.Badge[]
|
||||||
|
---@param opts? {widths?:number[]}
|
||||||
|
function M.format(badges, opts)
|
||||||
|
local text = {} ---@type string[][]
|
||||||
|
text[#text + 1] = { " ", "Normal" }
|
||||||
|
for b, badge in ipairs(badges) do
|
||||||
|
local level = badge.level or ""
|
||||||
|
local padding = badge.padding ~= false
|
||||||
|
and opts
|
||||||
|
and opts.widths
|
||||||
|
and (opts.widths[b] - vim.api.nvim_strwidth(badge.text))
|
||||||
|
or 0
|
||||||
|
text[#text + 1] = { badge.icon, "SnacksProfilerIcon" .. level }
|
||||||
|
text[#text + 1] = { " " .. (" "):rep(padding) .. badge.text .. " ", "SnacksProfilerBadge" .. level }
|
||||||
|
text[#text + 1] = { " ", "Normal" }
|
||||||
|
end
|
||||||
|
return text
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf number
|
||||||
|
---@param opts? snacks.profiler.Highlights|{file?:string}
|
||||||
|
function M.highlight(buf, opts)
|
||||||
|
opts = opts or {}
|
||||||
|
vim.api.nvim_buf_clear_namespace(buf, M.ns, 0, -1)
|
||||||
|
local file = Snacks.profiler.loc.norm({ file = opts.file or vim.api.nvim_buf_get_name(buf), line = 0 }).file
|
||||||
|
local highlights = M.highlights[file]
|
||||||
|
if not highlights then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local keep = {} ---@type table<number, snacks.profiler.Trace>
|
||||||
|
for l, entry in pairs(highlights) do
|
||||||
|
if entry.time >= (opts.min_time or 0) then
|
||||||
|
keep[l] = entry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
highlights = keep
|
||||||
|
|
||||||
|
local align = opts.align or 80
|
||||||
|
local buttons = {} ---@type table<number, snacks.profiler.Badge[]>
|
||||||
|
local widths = {} ---@type number[]
|
||||||
|
for line, entry in pairs(highlights) do
|
||||||
|
buttons[line] = M.badges(entry, opts --[[@as snacks.profiler.Highlights]])
|
||||||
|
for b, button in ipairs(buttons[line]) do
|
||||||
|
widths[b] = math.max(widths[b] or 0, vim.api.nvim_strwidth(button.text))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for line, entry in pairs(highlights) do
|
||||||
|
local text = M.format(buttons[line], { widths = widths })
|
||||||
|
if type(align) == "number" then
|
||||||
|
text[#text + 1] = { (" "):rep(vim.o.columns), "Normal" }
|
||||||
|
end
|
||||||
|
local mmax = math.min(M.max_time, 1e6 * Snacks.profiler.config.highlights.max_shade)
|
||||||
|
vim.api.nvim_buf_set_extmark(buf, M.ns, line - 1, 0, {
|
||||||
|
hl_mode = "combine",
|
||||||
|
virt_text = text,
|
||||||
|
virt_text_win_col = type(align) == "number" and align or nil,
|
||||||
|
virt_text_pos = align == "right" and "right_align" or align == "left" and "eol" or nil,
|
||||||
|
line_hl_group = ("SnacksProfilerHot%02d"):format(
|
||||||
|
math.max(math.min(math.floor(entry.time / mmax * M.shades), M.shades), 1)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param fg string foreground color
|
||||||
|
---@param bg string background color
|
||||||
|
---@param alpha number number between 0 and 1. 0 results in bg, 1 results in fg
|
||||||
|
function M.blend(fg, bg, alpha)
|
||||||
|
local bg_rgb = { tonumber(bg:sub(2, 3), 16), tonumber(bg:sub(4, 5), 16), tonumber(bg:sub(6, 7), 16) }
|
||||||
|
local fg_rgb = { tonumber(fg:sub(2, 3), 16), tonumber(fg:sub(4, 5), 16), tonumber(fg:sub(6, 7), 16) }
|
||||||
|
local blend = function(i)
|
||||||
|
local ret = (alpha * fg_rgb[i] + ((1 - alpha) * bg_rgb[i]))
|
||||||
|
return math.floor(math.min(math.max(0, ret), 255) + 0.5)
|
||||||
|
end
|
||||||
|
return string.format("#%02x%02x%02x", blend(1), blend(2), blend(3))
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.colors()
|
||||||
|
---@type snacks.util.hl
|
||||||
|
local hl_groups = {
|
||||||
|
Icon = "SnacksProfilerIconInfo",
|
||||||
|
Badge = "SnacksProfilerBadgeInfo",
|
||||||
|
IconTrace = "SnacksProfilerIconInfo",
|
||||||
|
BadgeTrace = "SnacksProfilerBadgeInfo",
|
||||||
|
}
|
||||||
|
local fallbacks = { Info = "#0ea5e9", Warn = "#f59e0b", Error = "#dc2626" }
|
||||||
|
local bg = Snacks.util.color("Normal", "bg") or "#000000"
|
||||||
|
local red = Snacks.util.color("DiagnosticError") or fallbacks.Error
|
||||||
|
for _, s in ipairs({ "Info", "Warn", "Error" }) do
|
||||||
|
local color = Snacks.util.color("Diagnostic" .. s) or fallbacks[s]
|
||||||
|
hl_groups["Icon" .. s] = { fg = color, bg = M.blend(color, bg, 0.3) }
|
||||||
|
hl_groups["Badge" .. s] = { fg = color, bg = M.blend(color, bg, 0.1) }
|
||||||
|
end
|
||||||
|
for i = 1, M.shades do
|
||||||
|
hl_groups[("Hot%02d"):format(i)] = { bg = M.blend(red, bg, i / (M.shades + 1)) }
|
||||||
|
end
|
||||||
|
Snacks.util.set_hl(hl_groups, { prefix = "SnacksProfiler", managed = false })
|
||||||
|
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||||
|
group = vim.api.nvim_create_augroup("snacks_profiler_colors", { clear = true }),
|
||||||
|
callback = M.colors,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
78
lua/trouble/sources/profiler.lua
Normal file
78
lua/trouble/sources/profiler.lua
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
---@module 'trouble'
|
||||||
|
---@diagnostic disable: inject-field
|
||||||
|
local Item = require("trouble.item")
|
||||||
|
|
||||||
|
---@type trouble.Source
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
---@diagnostic disable-next-line: missing-fields
|
||||||
|
M.config = {
|
||||||
|
formatters = {
|
||||||
|
badges = function(ctx)
|
||||||
|
local trace = ctx.item.item ---@type snacks.profiler.Trace
|
||||||
|
local badges = Snacks.profiler.ui.badges(trace, { badges = { "time", "count" } })
|
||||||
|
local text = Snacks.profiler.ui.format(badges)
|
||||||
|
return vim.tbl_map(function(t)
|
||||||
|
return { text = t[1], hl = t[2] }
|
||||||
|
end, text)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
modes = {
|
||||||
|
profiler = {
|
||||||
|
events = { { event = "User", pattern = "SnacksProfilerLoaded" } },
|
||||||
|
source = "profiler",
|
||||||
|
groups = {
|
||||||
|
-- { "tag", format = "{todo_icon} {tag}" },
|
||||||
|
-- { "directory" },
|
||||||
|
{ "loc.plugin", format = "{file_icon} {loc.plugin} {count}" },
|
||||||
|
},
|
||||||
|
-- sort = { { buf = 0 }, "filename", "pos", "name" },
|
||||||
|
sort = { "-time" },
|
||||||
|
format = "{name} {badges} {pos}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function M.preview(item, ctx)
|
||||||
|
Snacks.profiler.ui.highlight(ctx.buf, { file = item.item.loc.file })
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.get(cb, ctx)
|
||||||
|
---@type snacks.profiler.Find
|
||||||
|
local opts = vim.tbl_deep_extend(
|
||||||
|
"force",
|
||||||
|
{ group = "name", structure = true },
|
||||||
|
type(ctx.opts.params) == "table" and ctx.opts.params or {}
|
||||||
|
)
|
||||||
|
local _, node = Snacks.profiler.find(opts)
|
||||||
|
local items = {} ---@type trouble.Item[]
|
||||||
|
local id = 0
|
||||||
|
|
||||||
|
---@param n snacks.profiler.Node
|
||||||
|
local function add(n)
|
||||||
|
if n.trace.def then
|
||||||
|
id = id + 1
|
||||||
|
local loc = n.trace.def
|
||||||
|
local item = Item.new({
|
||||||
|
id = id,
|
||||||
|
pos = { n.trace.def.line, 0 },
|
||||||
|
text = n.trace.name,
|
||||||
|
filename = loc and loc.file,
|
||||||
|
item = n.trace,
|
||||||
|
source = "profiler",
|
||||||
|
})
|
||||||
|
items[#items + 1] = item
|
||||||
|
for _, child in pairs(n.children) do
|
||||||
|
item:add_child(add(child))
|
||||||
|
end
|
||||||
|
return item
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, child in pairs(node.children or {}) do
|
||||||
|
add(child)
|
||||||
|
end
|
||||||
|
cb(items)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
Loading…
Add table
Add a link
Reference in a new issue