PerfAnno: 在 NeoVim 中进行性能分析标注与调用图探索!
PerfAnno 是一个用于 NeoVim 的简单 lua 插件,允许你使用 perf 或其他能够生成 flamegraph 格式栈跟踪的调用图分析器(call graph profilers)的输出,对你的代码进行标注。 该插件本身与语言无关,并已在 C、C++、Lua 和 Python 上进行了测试。PerfAno 可用于轻松对 neovim 本身进行性能分析。
每一行都会标注出在该行发生的样本数,包括嵌套函数调用。这要求 perf.data 文件在记录时包含了调用图信息。
如果分析器提供了多个事件,例如 cpu cycles、branch mispredictions 和 cache misses,则你可以在这些事件之间切换。
此外,PerfAnno 提供了 Telescope、fzf-lua 或 vim.ui.select 查找器,允许你立即跳转到代码库中最热的行/函数,或特定代码区域(通常是一个函数)的最热调用者。
安装
此插件需要 NeoVim 0.10,最近一次测试是在 NeoVim 0.11 和 perf 5.16 上进行的。 调用图模式可能需要一个相对较新的、支持折叠输出的 perf 版本,但手动为旧版本添加支持应该很容易。
你应该能够以安装其他 NeoVim lua 插件的方式安装此插件,例如通过 packer 中的 use "t-troebst/perfanno.nvim"。
安装后,你需要通过调用以下命令来初始化插件:
require("perfanno").setup()
这将为你提供默认设置,如下文所示。
但是,你可能希望将 line_highlights 和 vt_highlight 设置为不与你的配色方案冲突的适当高亮,并设置一些键绑定以利用此插件。
请参阅提供的示例配置。
依赖项:
如果你想使用跳转到代码最热行的命令,你可能需要安装 telescope.nvim 或 fzf-lua。
否则(或者如果你在设置时明确禁用了两者),插件将回退到使用 vim.ui.select。
对于 :PerfAnnotateFunction 和 :PerfHottestCallersFunction,你需要 nvim-treesitter。
示例配置
以下配置将高亮设置为背景色与橙红色之间漂亮的 RGB 颜色渐变。 它还为主要标准命令设置了便捷的键绑定。
local perfanno = require("perfanno")
local util = require("perfanno.util")
perfanno.setup {
-- Creates a 10-step RGB color gradient beween background color and "#CC3300"
line_highlights = util.make_bg_highlights(nil, "#CC3300", 10),
vt_highlight = util.make_fg_highlight("#CC3300"),
}
local keymap = vim.api.nvim_set_keymap
local opts = {noremap = true, silent = true}
keymap("n", "<LEADER>plf", ":PerfLoadFlat<CR>", opts)
keymap("n", "<LEADER>plg", ":PerfLoadCallGraph<CR>", opts)
keymap("n", "<LEADER>plo", ":PerfLoadFlameGraph<CR>", opts)
keymap("n", "<LEADER>pe", ":PerfPickEvent<CR>", opts)
keymap("n", "<LEADER>pa", ":PerfAnnotate<CR>", opts)
keymap("n", "<LEADER>pf", ":PerfAnnotateFunction<CR>", opts)
keymap("v", "<LEADER>pa", ":PerfAnnotateSelection<CR>", opts)
keymap("n", "<LEADER>pt", ":PerfToggleAnnotations<CR>", opts)
keymap("n", "<LEADER>ph", ":PerfHottestLines<CR>", opts)
keymap("n", "<LEADER>ps", ":PerfHottestSymbols<CR>", opts)
keymap("n", "<LEADER>pc", ":PerfHottestCallersFunction<CR>", opts)
keymap("v", "<LEADER>pc", ":PerfHottestCallersSelection<CR>", opts)
配置
有关所有潜在配置选项的完整列表,请参阅以下设置调用。
require("perfanno").setup {
-- List of highlights that will be used to highlight hot lines (or nil to disable).
line_highlights = require("perfanno.util").make_bg_highlights(nil, "#FF0000", 10),
-- Highlight used for virtual text annotations (or nil to disable virtual text).
vt_highlight = require("perfanno.util").make_fg_highlight("#FF0000"),
-- Annotation formats that can be cycled between via :PerfCycleFormat
-- "percent" controls whether percentages or absolute counts should be displayed
-- "format" is the format string that will be used to display counts / percentages
-- "minimum" is the minimum value below which lines will not be annotated
-- Note: this also controls what shows up in the telescope finders
formats = {
{percent = true, format = "%.2f%%", minimum = 0.5},
{percent = false, format = "%d", minimum = 1}
},
-- Automatically annotate files after :PerfLoadFlat and :PerfLoadCallGraph
annotate_after_load = true,
-- Automatically annotate newly opened buffers if information is available
annotate_on_open = true,
-- Options for telescope-based hottest line finders
telescope = {
-- Enable if possible, otherwise the plugin will fall back to fzf-lua or vim.ui.select
enabled = pcall(require, "telescope"),
-- Annotate inside of the preview window
annotate = true,
},
-- Options for fzf-lua hottest line finders
fzf_lua = {
-- Enable if possible, otherwise the plugin will fall back to vim.ui.select
enabled = pcall(require, "fzf-lua"),
-- Annotate inside of the preview window
annotate = true,
},
-- Node type patterns used to find the function that surrounds the cursor
ts_function_patterns = {
-- These should work for most languages (at least those used with perf)
default = {
"function",
"method",
},
-- Otherwise you can add patterns for specific languages like:
-- weirdlang = {
-- "weirdfunc",
-- }
},
-- Overwrite the default behaviour of prompting for the path to the perf.data with a custom function that returns
-- the path to a perf file as string.
get_path_callback = nil,
-- Enable per-thread profiling support for multi-threaded applications
-- When enabled, you can select which thread(s) to profile when loading perf.data
thread_support = false,
}
local telescope = require("telescope")
local actions = telescope.extensions.perfanno.actions
telescope.setup {
extensions = {
perfanno = {
-- Special mappings in the telescope finders
mappings = {
["i"] = {
-- Find hottest callers of selected entry
["<C-h>"] = actions.hottest_callers,
-- Find hottest callees of selected entry
["<C-l>"] = actions.hottest_callees,
},
["n"] = {
["gu"] = actions.hottest_callers,
["gd"] = actions.hottest_callees,
}
}
}
}
}
这些是默认设置,因此这与 require("perfanno").setup() 等效。
工作流程
为了使用此插件,你需要使用 perf 生成准确的性能分析信息,理想情况下应包含调用图信息。 你需要使用调试信息编译你的程序,然后运行:
perf record --call-graph dwarf {program}
然后,这将生成一个 perf.data 文件,该文件可被此插件使用。
从那里,您可以使用 下方 所示的命令。
如果 dwarf 选项创建的文件过大或处理时间过长,您可能还想尝试:
perf record --call-graph fp {program}
然而,这要求你的程序和所有库都已使用 -fno-omit-frame-pointer 编译,并且你可能会发现行号略有偏差。
有关更多信息,请参阅 perf 的文档。
多线程应用程序
如果你正在分析多线程应用程序并希望分析每个线程的性能,请在你的配置中启用线程支持:
require("perfanno").setup({
thread_support = true,
})
当从多线程程序加载 perf 数据时,系统会提示您选择要分析哪个(些)线程:
- 所有线程(聚合) - 默认行为,合并来自所有线程的样本
- 单个线程 - 分析特定线程,例如 "TID 12345 (worker-thread)"
这对于识别哪些线程消耗了最多的资源或分析线程特定的性能特征非常有用。
使用其他分析器
如果您使用的是其他分析器,您需要生成一个 perf.log 文件,该文件以火焰图格式存储数据,即作为一系列以 ; 分隔的堆栈跟踪,每行末尾带有一个计数。
例如:
/path/to/src_1.cpp:30;/path/to/src_2.cpp:27;/path/to/src_1.cpp:27 47
/path/to/src_1.cpp:30;/path/to/src_2.cpp:50 20
/path/to/src_1.cpp:10;/path/to/src_3.cpp:20;/path/to/src_2.cpp:15 7
/path/to/src_1.cpp:10;/path/to/src_3.cpp:20;/path/to/src_2.cpp:50 92
命令
加载性能分析数据
:PerfLoadFlat加载扁平化的 perf 数据。显然,在此模式下您将无法找到函数的调用者。:PerfLoadCallGraph加载完整的调用图 perf 数据。这可能需要一些时间。:PerfLoadFlameGraph从perf.log文件中加载火焰图格式的数据。
如果您的工作目录中分别没有 perf.data 或 perf.log 文件,系统会提示您定位其中一个。如果设置了 annotate_after_load,这将立即注释所有缓冲区。
Lua 性能分析
PerfAnno 可用于通过原生 LuaJIT 分析器轻松对 NeoVim 进行性能分析。 只需按顺序使用以下命令:
:PerfLuaProfileStart开始性能分析。:PerfLuaProfileStop停止当前的性能分析运行,并将堆栈跟踪加载到调用 图中。如果设置了annotate_after_load,则自动注释所有缓冲区。
控制注释的显示方式
:PerfPickEvent选择 perf 数据中的不同事件进行显示。例如,您可以使用此命令在 CPU 周期、分支预测失败和缓存未命中之间切换。:PerfCycleFormat允许您在存储的格式之间切换,默认情况下在百分比和绝对计数之间切换。
注释
:PerfAnnotate注释所有当前打开的缓冲区。:PerfToggleAnnotations切换所有缓冲区中的注释。:PerfAnnotateSelection仅注释给定选择范围内的代码。行高亮显示相对于该选择范围内的总计数,如果当前格式为百分比,则显示的百分比也是相对的。:PerfAnnotateFunction执行与:PerfAnnotateSelection相同的操作,但通过 treesitter 选择包含光标的函数。
如果加载了多个事件,则需要在显示注释之前选择一个。
查找热点行
:PerfHottestLines打开一个查找器(telescope、fzf-lua 或 vim.ui.select),显示根据当前注释确定的最热行。:PerfHottestSymbols打开一个查找器,显示根据当前注释确定的最热符号(通常是函数)。:PerfHottestCallersSelection打开一个查找器,显示直接导致当前选中行的最热行。:PerfHottestCallersFunction的工作方式与:PerfHottestCallersSelection相同,但通过 treesitter 选择包含光标的函数。
在使用 telescope 或 fzf-lua 时,您可以在选择器内使用 <C-h>(调用者)和 <C-l>(被调用者)交互式地导航调用图。
缓存(实验性)
根据调用图的加载方式,生成它可能需要大量时间 (例如,在长时间运行中使用 perf),或者可能根本无法再次生成它(例如,使用 Lua 分析器)。出于这些原因,PerfAnno 支持通过以下命令将调用图保存/恢复到 缓存中:
:PerfCacheSave <name>将当前加载的调用图以给定名称保存在缓存中。:PerfCacheLoad <name>加载缓存中给定名称的调用图。如果设置了annotate_after_load,则自动 注释所有缓冲区。如果提供空名称,则加载最近缓存的调用图。:PerfCacheDelete <name>删除缓存中给定名称的调用图。
扩展
如果您希望使用此插件配合非 perf 的性能分析器,您可以直接调用 require("perfanno").load_traces 来使用每个可能事件的堆栈跟踪列表设置调用图信息。
确切格式请参见下方的示例。
local traces = {
"event 1" = {
{
count = 42,
frames = {
"symbol1 /home/user/Project/src_1.cpp:57",
"symbol2 /home/user/Project/src_2.cpp:32",
"symbol1 /home/user/Project/src_1.cpp:42"
}
},
{
count = 99,
frames = {
"symbol3 /home/user/Project/src_1.cpp:20",
"0x1231232",
"__foo_bar",
"symbol4 /home/user/Project/src_3.cpp:50"
}
},
-- more traces...
},
"event 2" = {
-- ...
},
-- more events...
}
require("perfanno").load_traces(traces)
堆栈跟踪由一个 count 表示,它告诉我们该确切跟踪出现的频率,以及一个 frames 列表。
每个堆栈帧应以一个 symbol 开头,后跟 fullpath:linenum。
如果它不符合此格式,它将被简单地解释为一个任意符号。
您也可以直接在以下格式中指定一个帧:
{symbol = "symbol1", file = "/home/user/Project/src_1.cpp", linenr = 42}
注意:跟踪中的文件路径应为规范格式下的完整、未转义路径,即使用 /full/file path/to/source.cpp:35 而非 /full/file\ path//to/../to/source.cpp:35。
我们尝试获取路径的规范表示,但通常这是最可靠的方式。
未来目标
- 添加查找器(telescope/fzf-lua)以从缓存中加载/删除调用图。
- 提高
:PerfCycleFormat的健壮性(它目前会重置相对注释,且在活动查找器内无法正常工作)。 - 添加对
:FindHottestCallers的支持,并增加深度。 - 添加
:FindHottestCallees,这本质上是:FindHottestLines,但相对于经过特定选择的堆栈跟踪。