diff --git a/.envrc b/.envrc index 32465e7..d522e34 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1,3 @@ export VIRTUAL_ENV=venv layout python +python -c 'import pyparsing' 2>/dev/null || pip install -r scripts/requirements.txt diff --git a/.gitignore b/.gitignore index c90db5d..bb036c1 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ venv/ doc/tags scripts/nvim_doc_tools scripts/nvim-typecheck-action +tests/perf/ +profile.json diff --git a/Makefile b/Makefile index 4799368..bd7bf6c 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,23 @@ fastlint: scripts/nvim_doc_tools venv luacheck lua tests --formatter plain stylua --check lua tests +## profile: use LuaJIT profiler to profile the plugin +.PHONY: profile +profile: + nvim --clean -u tests/perf_harness.lua -c 'lua jit_profile()' + +## flame_profile: create a trace in the chrome profiler format +.PHONY: flame_profile +flame_profile: + nvim --clean -u tests/perf_harness.lua -c 'lua flame_profile()' + @echo "Visit https://ui.perfetto.dev/ and load the profile.json file" + +## benchmark: benchmark performance opening directory with many files +.PHONY: benchmark +benchmark: + nvim --clean -u tests/perf_harness.lua -c 'lua benchmark(10)' + @cat tests/perf/benchmark.txt + scripts/nvim_doc_tools: git clone https://github.com/stevearc/nvim_doc_tools scripts/nvim_doc_tools @@ -44,4 +61,4 @@ scripts/nvim-typecheck-action: ## clean: reset the repository to a clean state .PHONY: clean clean: - rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv + rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv tests/perf profile.json diff --git a/tests/perf_harness.lua b/tests/perf_harness.lua new file mode 100644 index 0000000..679004d --- /dev/null +++ b/tests/perf_harness.lua @@ -0,0 +1,122 @@ +vim.fn.mkdir("tests/perf/.env", "p") +local root = vim.fn.fnamemodify("./tests/perf/.env", ":p") + +for _, name in ipairs({ "config", "data", "state", "runtime", "cache" }) do + vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. name +end + +vim.opt.runtimepath:prepend(vim.fn.fnamemodify(".", ":p")) + +---@module 'oil' +---@type oil.SetupOpts +local setup_opts = { + -- columns = { "icon", "permissions", "size", "mtime" }, +} + +local num_files = 100000 + +if not vim.uv.fs_stat(string.format("tests/perf/file %d.txt", num_files)) then + vim.notify("Creating files") + for i = 1, num_files, 1 do + local filename = ("tests/perf/file %d.txt"):format(i) + local fd = vim.uv.fs_open(filename, "a", 420) + assert(fd) + vim.uv.fs_close(fd) + end +end + +local function wait_for_done(callback) + vim.api.nvim_create_autocmd("User", { + pattern = "OilEnter", + once = true, + callback = callback, + }) +end + +function _G.jit_profile() + require("oil").setup(setup_opts) + local outfile = "tests/perf/profile.txt" + require("jit.p").start("3Fpli1s", outfile) + local start = vim.uv.hrtime() + require("oil").open("tests/perf") + + wait_for_done(function() + local delta = vim.uv.hrtime() - start + require("jit.p").stop() + print("Elapsed:", delta / 1e6, "ms") + vim.cmd.edit({ args = { outfile } }) + end) +end + +function _G.benchmark(iterations) + require("oil").setup(setup_opts) + local num_outliers = math.floor(0.1 * iterations) + local times = {} + + local run_profile + run_profile = function() + -- Clear out state + vim.cmd.enew() + for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_is_valid(bufnr) and bufnr ~= vim.api.nvim_get_current_buf() then + vim.api.nvim_buf_delete(bufnr, { force = true }) + end + end + + local start = vim.uv.hrtime() + wait_for_done(function() + local delta = vim.uv.hrtime() - start + table.insert(times, delta / 1e6) + if #times < iterations then + vim.schedule(run_profile) + else + -- Remove the outliers + table.sort(times) + for _ = 1, num_outliers do + table.remove(times, 1) + table.remove(times) + end + + local total = 0 + for _, time in ipairs(times) do + total = total + time + end + + local lines = { + table.concat( + vim.tbl_map(function(t) + return string.format("%dms", math.floor(t)) + end, times), + " " + ), + string.format("Average: %dms", math.floor(total / #times)), + } + vim.fn.writefile(lines, "tests/perf/benchmark.txt") + vim.cmd.qall() + end + end) + require("oil").open("tests/perf") + end + + run_profile() +end + +function _G.flame_profile() + if not vim.uv.fs_stat("tests/perf/profile.nvim") then + vim + .system({ "git", "clone", "https://github.com/stevearc/profile.nvim", "tests/perf/profile.nvim" }) + :wait() + end + vim.opt.runtimepath:prepend(vim.fn.fnamemodify("./tests/perf/profile.nvim", ":p")) + local profile = require("profile") + profile.instrument_autocmds() + profile.instrument("oil*") + + require("oil").setup(setup_opts) + profile.start() + require("oil").open("tests/perf") + wait_for_done(function() + profile.stop("profile.json") + vim.cmd.qall() + end) +end