diff options
-rw-r--r-- | .luacheckrc | 2 | ||||
-rw-r--r-- | lua/lvim/impatient.lua | 309 | ||||
-rw-r--r-- | lua/lvim/impatient/profile.lua | 109 | ||||
-rw-r--r-- | lua/lvim/utils/hooks.lua | 5 |
4 files changed, 274 insertions, 151 deletions
diff --git a/.luacheckrc b/.luacheckrc index 7c592693..53307087 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,3 +1,4 @@ +---@diagnostic disable -- vim: ft=lua tw=80 stds.nvim = { @@ -30,6 +31,7 @@ stds.nvim = { std = "lua51+nvim" files["tests/*_spec.lua"].std = "lua51+nvim+busted" +files["lua/lvim/impatient*"].ignore = {"121"} -- Don't report unused self arguments of methods. self = false diff --git a/lua/lvim/impatient.lua b/lua/lvim/impatient.lua index fe26b940..beb3862a 100644 --- a/lua/lvim/impatient.lua +++ b/lua/lvim/impatient.lua @@ -1,5 +1,4 @@ -- modified version from https://github.com/lewis6991/impatient.nvim - local vim = vim local api = vim.api local uv = vim.loop @@ -7,66 +6,96 @@ local _loadfile = loadfile local get_runtime = api.nvim__get_runtime local fs_stat = uv.fs_stat local mpack = vim.mpack +local loadlib = package.loadlib + +local std_cache = vim.fn.stdpath "cache" + +local sep +if jit.os == "Windows" then + sep = "\\" +else + sep = "/" +end + +local std_dirs = { + ["<APPDIR>"] = os.getenv "APPDIR", + ["<VIMRUNTIME>"] = os.getenv "VIMRUNTIME", + ["<STD_DATA>"] = vim.fn.stdpath "data", + ["<STD_CONFIG>"] = vim.fn.stdpath "config", + ["<LVIM_BASE>"] = get_lvim_base_dir(), + ["<LVIM_RUNTIME>"] = get_runtime_dir(), + ["<LVIM_CONFIG>"] = get_config_dir(), +} -local appdir = os.getenv "APPDIR" +local function modpath_mangle(modpath) + for name, dir in pairs(std_dirs) do + modpath = modpath:gsub(dir, name) + end + return modpath +end -local M = { +local function modpath_unmangle(modpath) + for name, dir in pairs(std_dirs) do + modpath = modpath:gsub(name, dir) + end + return modpath +end + +-- Overridable by user +local default_config = { + chunks = { + enable = true, + path = std_cache .. sep .. "luacache_chunks", + }, + modpaths = { + enable = true, + path = std_cache .. sep .. "luacache_modpaths", + }, +} + +-- State used internally +local default_state = { chunks = { cache = {}, profile = nil, dirty = false, - path = vim.fn.stdpath "cache" .. "/luacache_chunks", + get = function(self, path) + return self.cache[modpath_mangle(path)] + end, + set = function(self, path, chunk) + self.cache[modpath_mangle(path)] = chunk + end, }, modpaths = { cache = {}, profile = nil, dirty = false, - path = vim.fn.stdpath "cache" .. "/luacache_modpaths", + get = function(self, mod) + if self.cache[mod] then + return modpath_unmangle(self.cache[mod]) + end + end, + set = function(self, mod, path) + self.cache[mod] = modpath_mangle(path) + end, }, log = {}, } +---@diagnostic disable-next-line: undefined-field +local M = vim.tbl_deep_extend("keep", _G.__luacache_config or {}, default_config, default_state) _G.__luacache = M -if not get_runtime then - -- nvim 0.5 compat - get_runtime = function(paths, all, _) - local r = {} - for _, path in ipairs(paths) do - local found = api.nvim_get_runtime_file(path, all) - for i = 1, #found do - r[#r + 1] = found[i] - end - end - return r - end -end - local function log(...) M.log[#M.log + 1] = table.concat({ string.format(...) }, " ") end -function M.print_log() +local function print_log() for _, l in ipairs(M.log) do print(l) end end -function M.enable_profile() - local P = require "lvim.impatient.profile" - - M.chunks.profile = {} - M.modpaths.profile = {} - - P.setup(M.modpaths.profile) - - M.print_profile = function() - P.print_profile(M) - end - - vim.cmd [[command! LuaCacheProfile lua _G.__luacache.print_profile()]] -end - local function hash(modpath) local stat = fs_stat(modpath) if stat then @@ -75,20 +104,6 @@ local function hash(modpath) error("Could not hash " .. modpath) end -local function modpath_mangle(modpath) - if appdir then - modpath = modpath:gsub(appdir, "/$APPDIR") - end - return modpath -end - -local function modpath_unmangle(modpath) - if appdir then - modpath = modpath:gsub("/$APPDIR", appdir) - end - return modpath -end - local function profile(m, entry, name, loader) if m.profile then local mp = m.profile @@ -107,18 +122,41 @@ local function mprofile(mod, name, loader) end local function cprofile(path, name, loader) + if M.chunks.profile then + path = modpath_mangle(path) + end profile(M.chunks, path, name, loader) end -local function get_runtime_file(basename, paths) +function M.enable_profile() + local P = require "lvim.impatient.profile" + + M.chunks.profile = {} + M.modpaths.profile = {} + + loadlib = function(path, fun) + cprofile(path, "load_start") + local f, err = package.loadlib(path, fun) + cprofile(path, "load_end", "standard") + return f, err + end + + P.setup(M.modpaths.profile) + + api.nvim_create_user_command("LuaCacheProfile", function() + P.print_profile(M, std_dirs) + end, {}) +end + +local function get_runtime_file_from_parent(basename, paths) -- Look in the cache to see if we have already loaded a parent module. -- If we have then try looking in the parents directory first. - local parents = vim.split(basename, "/") + local parents = vim.split(basename, sep) for i = #parents, 1, -1 do - local parent = table.concat(vim.list_slice(parents, 1, i), "/") - local ppath = M.modpaths.cache[parent] + local parent = table.concat(vim.list_slice(parents, 1, i), sep) + local ppath = M.modpaths:get(parent) if ppath then - if ppath:sub(-9) == "/init.lua" then + if ppath:sub(-9) == (sep .. "init.lua") then ppath = ppath:sub(1, -10) -- a/b/init.lua -> a/b else ppath = ppath:sub(1, -5) -- a/b.lua -> a/b @@ -126,38 +164,71 @@ local function get_runtime_file(basename, paths) for _, path in ipairs(paths) do -- path should be of form 'a/b/c.lua' or 'a/b/c/init.lua' - local modpath = ppath .. "/" .. path:sub(#("lua/" .. parent) + 2) + local modpath = ppath .. sep .. path:sub(#("lua" .. sep .. parent) + 2) if fs_stat(modpath) then return modpath, "cache(p)" end end end end +end + +local rtp = vim.split(vim.o.rtp, ",") - -- What Neovim does by default; slowest - local modpath = get_runtime(paths, false, { is_lua = true })[1] - return modpath, "standard" +-- Make sure modpath is in rtp and that modpath is in paths. +local function validate_modpath(modpath, paths) + local match = false + for _, p in ipairs(paths) do + if vim.endswith(modpath, p) then + match = true + break + end + end + if not match then + return false + end + for _, dir in ipairs(rtp) do + if vim.startswith(modpath, dir) then + return fs_stat(modpath) ~= nil + end + end + return false end local function get_runtime_file_cached(basename, paths) + local modpath, loader local mp = M.modpaths - if mp.cache[basename] then - local modpath = mp.cache[basename] - if fs_stat(modpath) then - mprofile(basename, "resolve_end", "cache") - return modpath + if mp.enable then + local modpath_cached = mp:get(basename) + if modpath_cached then + modpath, loader = modpath_cached, "cache" + else + modpath, loader = get_runtime_file_from_parent(basename, paths) + end + + if modpath and not validate_modpath(modpath, paths) then + modpath = nil + + -- Invalidate + mp.cache[basename] = nil + mp.dirty = true end - mp.cache[basename] = nil - mp.dirty = true end - local modpath, loader = get_runtime_file(basename, paths) + if not modpath then + -- What Neovim does by default; slowest + modpath, loader = get_runtime(paths, false, { is_lua = true })[1], "standard" + end + if modpath then mprofile(basename, "resolve_end", loader) - log("Creating cache for module %s", basename) - mp.cache[basename] = modpath_mangle(modpath) - mp.dirty = true + if mp.enable and loader ~= "cache" then + log("Creating cache for module %s", basename) + mp:set(basename, modpath) + mp.dirty = true + end end + return modpath end @@ -168,8 +239,12 @@ local function extract_basename(pats) for _, pat in ipairs(pats) do for i, npat in ipairs { -- Ordered by most specific - "lua/(.*)/init%.lua", - "lua/(.*)%.lua", + "lua" + .. sep + .. "(.*)" + .. sep + .. "init%.lua", + "lua" .. sep .. "(.*)%.lua", } do local m = pat:match(npat) if i == 2 and m and m:sub(-4) == "init" then @@ -211,8 +286,8 @@ end -- Copied from neovim/src/nvim/lua/vim.lua with two lines changed local function load_package(name) - local basename = name:gsub("%.", "/") - local paths = { "lua/" .. basename .. ".lua", "lua/" .. basename .. "/init.lua" } + local basename = name:gsub("%.", sep) + local paths = { "lua" .. sep .. basename .. ".lua", "lua" .. sep .. basename .. sep .. "init.lua" } -- Original line: -- local found = vim.api.nvim__get_runtime(paths, false, {is_lua=true}) @@ -239,7 +314,7 @@ local function load_package(name) -- So "foo-bar.baz" should result in "luaopen_bar_baz" local dash = name:find("-", 1, true) local modname = dash and name:sub(dash + 1) or name - local f, err = package.loadlib(found[1], "luaopen_" .. modname:gsub("%.", "_")) + local f, err = loadlib(found[1], "luaopen_" .. modname:gsub("%.", "_")) return f or error(err) end return nil @@ -248,14 +323,16 @@ end local function load_from_cache(path) local mc = M.chunks - if not mc.cache[path] then + local cache = mc:get(path) + + if not cache then return nil, string.format("No cache for path %s", path) end - local mhash, codes = unpack(mc.cache[path]) + local mhash, codes = unpack(cache) - if mhash ~= hash(modpath_unmangle(path)) then - mc.cache[path] = nil + if mhash ~= hash(path) then + mc:set(path) mc.dirty = true return nil, string.format("Stale cache for path %s", path) end @@ -263,7 +340,7 @@ local function load_from_cache(path) local chunk = loadstring(codes) if not chunk then - mc.cache[path] = nil + mc:set(path) mc.dirty = true return nil, string.format("Cache error for path %s", path) end @@ -274,19 +351,23 @@ end local function loadfile_cached(path) cprofile(path, "load_start") - local chunk, err = load_from_cache(path) - if chunk and not err then - log("Loaded cache for path %s", path) - cprofile(path, "load_end", "cache") - return chunk + local chunk, err + + if M.chunks.enable then + chunk, err = load_from_cache(path) + if chunk and not err then + log("Loaded cache for path %s", path) + cprofile(path, "load_end", "cache") + return chunk + end + log(err) end - log(err) chunk, err = _loadfile(path) - if not err then + if not err and M.chunks.enable then log("Creating cache for path %s", path) - M.chunks.cache[modpath_mangle(path)] = { hash(path), string.dump(chunk) } + M.chunks:set(path, { hash(path), string.dump(chunk) }) M.chunks.dirty = true end @@ -296,9 +377,12 @@ end function M.save_cache() local function _save_cache(t) + if not t.enable then + return + end if t.dirty then log("Updating chunk cache file: %s", t.path) - local f = io.open(t.path, "w+b") + local f = assert(io.open(t.path, "w+b")) f:write(mpack.encode(t.cache)) f:flush() t.dirty = false @@ -308,7 +392,7 @@ function M.save_cache() _save_cache(M.modpaths) end -function M.clear_cache() +local function clear_cache() local function _clear_cache(t) t.cache = {} os.remove(t.path) @@ -319,9 +403,12 @@ end local function init_cache() local function _init_cache(t) + if not t.enable then + return + end if fs_stat(t.path) then log("Loading cache file %s", t.path) - local f = io.open(t.path, "rb") + local f = assert(io.open(t.path, "rb")) local ok ok, t.cache = pcall(function() return mpack.decode(f:read "*a") @@ -336,6 +423,10 @@ local function init_cache() end end + if not uv.fs_stat(std_cache) then + vim.fn.mkdir(std_cache, "p") + end + _init_cache(M.chunks) _init_cache(M.modpaths) end @@ -343,20 +434,42 @@ end local function setup() init_cache() + -- Usual package loaders + -- 1. package.preload + -- 2. vim._load_package + -- 3. package.path + -- 4. package.cpath + -- 5. all-in-one + -- Override default functions + for i, loader in ipairs(package.loaders) do + if loader == vim._load_package then + package.loaders[i] = load_package + break + end + end vim._load_package = load_package + vim.api.nvim__get_runtime = get_runtime_cached - -- luacheck: ignore 121 loadfile = loadfile_cached - vim.cmd [[ - augroup impatient - autocmd VimEnter,VimLeave * lua _G.__luacache.save_cache() - augroup END + local augroup = api.nvim_create_augroup("impatient", {}) + + api.nvim_create_user_command("LuaCacheClear", clear_cache, {}) + api.nvim_create_user_command("LuaCacheLog", print_log, {}) + + api.nvim_create_autocmd({ "VimEnter", "VimLeave" }, { + group = augroup, + callback = M.save_cache, + }) - command! LuaCacheClear lua _G.__luacache.clear_cache() - command! LuaCacheLog lua _G.__luacache.print_log() - ]] + api.nvim_create_autocmd("OptionSet", { + group = augroup, + pattern = "runtimepath", + callback = function() + rtp = vim.split(vim.o.rtp, ",") + end, + }) end setup() diff --git a/lua/lvim/impatient/profile.lua b/lua/lvim/impatient/profile.lua index 0ab04d65..2eafbbf2 100644 --- a/lua/lvim/impatient/profile.lua +++ b/lua/lvim/impatient/profile.lua @@ -1,12 +1,13 @@ local M = {} -local api, uv = vim.api, vim.loop +local sep +if jit.os == "Windows" then + sep = "\\" +else + sep = "/" +end -local std_data = vim.fn.stdpath "data" -local std_config = vim.fn.stdpath "config" -local vimruntime = os.getenv "VIMRUNTIME" -local lvim_runtime = get_runtime_dir() -local lvim_config = get_config_dir() +local api, uv = vim.api, vim.loop local function load_buffer(title, lines) local bufnr = api.nvim_create_buf(false, false) @@ -19,19 +20,6 @@ local function load_buffer(title, lines) api.nvim_set_current_buf(bufnr) end -local function mod_path(path) - if not path then - return "?" - end - path = path:gsub(std_data .. "/site/pack/packer/", "<PACKER>/") - path = path:gsub(std_data .. "/", "<STD_DATA>/") - path = path:gsub(std_config .. "/", "<STD_CONFIG>/") - path = path:gsub(vimruntime .. "/", "<VIMRUNTIME>/") - path = path:gsub(lvim_runtime .. "/", "<LVIM_RUNTIME>/") - path = path:gsub(lvim_config .. "/", "<LVIM_CONFIG>/") - return path -end - local function time_tostr(x) if x == 0 then return "?" @@ -51,7 +39,7 @@ local function mem_tostr(x) return string.format("%1.1f%s", x, unit) end -function M.print_profile(I) +function M.print_profile(I, std_dirs) local mod_profile = I.modpaths.profile local chunk_profile = I.chunks.profile @@ -67,42 +55,50 @@ function M.print_profile(I) for path, m in pairs(chunk_profile) do m.load = m.load_end - m.load_start m.load = m.load / 1000000 - m.path = mod_path(path) + m.path = path or "?" end local module_content_width = 0 + local unloaded = {} + for module, m in pairs(mod_profile) do - m.resolve = 0 - if m.resolve_end then - m.resolve = m.resolve_end - m.resolve_start - m.resolve = m.resolve / 1000000 - end + local module_dot = module:gsub(sep, ".") + m.module = module_dot - m.module = module:gsub("/", ".") - m.loader = m.loader or m.loader_guess + if not package.loaded[module_dot] and not package.loaded[module] then + unloaded[#unloaded + 1] = m + else + m.resolve = 0 + if m.resolve_start and m.resolve_end then + m.resolve = m.resolve_end - m.resolve_start + m.resolve = m.resolve / 1000000 + end - local path = I.modpaths.cache[module] - local path_prof = chunk_profile[path] - m.path = mod_path(path) + m.loader = m.loader or m.loader_guess - if path_prof then - chunk_profile[path] = nil - m.load = path_prof.load - m.ploader = path_prof.loader - else - m.load = 0 - m.ploader = "NA" - end + local path = I.modpaths.cache[module] + local path_prof = chunk_profile[path] + m.path = path or "?" - total_resolve = total_resolve + m.resolve - total_load = total_load + m.load + if path_prof then + chunk_profile[path] = nil + m.load = path_prof.load + m.ploader = path_prof.loader + else + m.load = 0 + m.ploader = "NA" + end - if #module > module_content_width then - module_content_width = #module - end + total_resolve = total_resolve + m.resolve + total_load = total_load + m.load + + if #module > module_content_width then + module_content_width = #module + end - modules[#modules + 1] = m + modules[#modules + 1] = m + end end table.sort(modules, function(a, b) @@ -181,6 +177,12 @@ function M.print_profile(I) end add "" + add "Standard directories:" + for alias, path in pairs(std_dirs) do + add(" %-12s -> %s", alias, path) + end + add "" + add("%s─%s┬%s─%s┐", tcwl, lcwl, tcwl, lcwl) add(title1_fmt, "Resolve", "Load") add("%s┬%s┼%s┬%s┼%s┬%s", tcwl, lcwl, tcwl, lcwl, mcwl, n) @@ -207,7 +209,17 @@ function M.print_profile(I) add(f3, p.load, p.loader, p.path) end add("%s┴%s┴%s", tcwl, lcwl, n) + end + + if #unloaded > 0 then add "" + add(n) + add "Modules which were unable to loaded" + add(n) + for _, p in ipairs(unloaded) do + lines[#lines + 1] = p.module + end + add(n) end load_buffer("Impatient Profile Report", lines) @@ -216,13 +228,12 @@ end M.setup = function(profile) local _require = require - -- luacheck: ignore 121 require = function(mod) - local basename = mod:gsub("%.", "/") + local basename = mod:gsub("%.", sep) if not profile[basename] then profile[basename] = {} profile[basename].resolve_start = uv.hrtime() - profile[basename].loader_guess = "C" + profile[basename].loader_guess = "" end return _require(mod) end @@ -232,7 +243,7 @@ M.setup = function(profile) for i = 1, #pl do local l = pl[i] pl[i] = function(mod) - local basename = mod:gsub("%.", "/") + local basename = mod:gsub("%.", sep) profile[basename].loader_guess = i == 1 and "preloader" or "loader #" .. i return l(mod) end diff --git a/lua/lvim/utils/hooks.lua b/lua/lvim/utils/hooks.lua index ce4335a9..bf0dac60 100644 --- a/lua/lvim/utils/hooks.lua +++ b/lua/lvim/utils/hooks.lua @@ -34,10 +34,7 @@ end ---It also forces regenerating any template ftplugin files ---Tip: Useful for clearing any outdated settings function M.reset_cache() - local impatient = _G.__luacache - if impatient then - impatient.clear_cache() - end + vim.cmd [[LuaCacheClear]] local lvim_modules = {} for module, _ in pairs(package.loaded) do if module:match "lvim.core" or module:match "lvim.lsp" then |