From 462633bae11255133f099163dda17180b3a6dc27 Mon Sep 17 00:00:00 2001 From: Folke Lemaitre Date: Mon, 13 Feb 2023 12:01:56 +0100 Subject: [PATCH] perf: new file-based cache that ensures correct rtp order (#532) * perf: new file-based cache that ensures rtp is alweays correct and will cache all files, including those after startup * refactor: new cache * test: fix tests * fix(cache): cache file names on Windows * feat(cache): allow to disable the cache * docs: updated cache settings --- README.md | 10 +- lua/lazy/core/cache.lua | 728 ++++++++++++----------------------- lua/lazy/core/config.lua | 6 +- lua/lazy/core/loader.lua | 28 +- lua/lazy/core/util.lua | 39 +- lua/lazy/docs.lua | 5 - lua/lazy/init.lua | 5 +- lua/lazy/manage/reloader.lua | 15 +- lua/lazy/view/render.lua | 31 +- tests/core/util_spec.lua | 36 +- 10 files changed, 354 insertions(+), 549 deletions(-) diff --git a/README.md b/README.md index 450d8f0..37fa84f 100644 --- a/README.md +++ b/README.md @@ -347,7 +347,7 @@ return { init = " ", import = " ", keys = " ", - lazy = "鈴 ", + lazy = "󰒲 ", loaded = "●", not_loaded = "○", plugin = " ", @@ -409,14 +409,6 @@ return { performance = { cache = { enabled = true, - path = vim.fn.stdpath("cache") .. "/lazy/cache", - -- Once one of the following events triggers, caching will be disabled. - -- To cache all modules, set this to `{}`, but that is not recommended. - -- The default is to disable on: - -- * VimEnter: not useful to cache anything else beyond startup - -- * BufReadPre: this will be triggered early when opening a file from the command line directly - disable_events = { "UIEnter", "BufReadPre" }, - ttl = 3600 * 24 * 5, -- keep unused modules for up to 5 days }, reset_packpath = true, -- reset the package path to improve startup time rtp = { diff --git a/lua/lazy/core/cache.lua b/lua/lazy/core/cache.lua index 3f30ff2..89d5a4a 100644 --- a/lua/lazy/core/cache.lua +++ b/lua/lazy/core/cache.lua @@ -1,240 +1,264 @@ local ffi = require("ffi") ----@diagnostic disable-next-line: no-unknown local uv = vim.loop local M = {} -M.dirty = false -M.VERSION = "1" .. jit.version - ----@class LazyCacheConfig -M.config = { - enabled = true, - path = vim.fn.stdpath("cache") .. "/lazy/cache", - -- Once one of the following events triggers, caching will be disabled. - -- To cache all modules, set this to `{}`, but that is not recommended. - -- The default is to disable on: - -- * VimEnter: not useful to cache anything else beyond startup - -- * BufReadPre: this will be triggered early when opening a file from the command line directly - disable_events = { "UIEnter", "BufReadPre" }, - ttl = 3600 * 24 * 5, -- keep unused modules for up to 5 days -} -M.debug = false - ----@type CacheHash -local cache_hash ---@alias CacheHash {mtime: {sec:number, nsec:number}, size:number} ----@alias CacheEntry {hash:CacheHash, modpath:string, chunk:string, used:number} ----@type table -M.cache = {} -M.enabled = true ----@type string[] -M.rtp = nil -M.rtp_total = 0 -M.stats = { - find = { total = 0, time = 0, rtp = 0, unloaded = 0, index = 0, stat = 0, not_found = 0 }, - autoload = { total = 0, time = 0 }, +---@alias CacheEntry {hash:CacheHash, modpath:string, chunk:string} + +---@class CacheFindOpts +---@field rtp? boolean Search for modname in the runtime path (defaults to `true`) +---@field patterns? string[] Paterns to use (defaults to `{"/init.lua", ".lua"}`) +---@field paths? string[] Extra paths to search for modname + +M.VERSION = 1 +M.path = vim.fn.stdpath("cache") .. "/lazy/luac" +M.enabled = false +M.stats = { total = 0, time = 0, index = 0, stat = 0, not_found = 0 } + +---@class ModuleCache +---@field _rtp string[] +---@field _rtp_key string +local Cache = { + ---@type table> + _topmods = {}, + _loadfile = loadfile, } -M.me = debug.getinfo(1, "S").source:sub(2) -M.me = vim.fn.fnamemodify(M.me, ":p:h:h:h:h"):gsub("\\", "/") ----@type table -M.topmods = { lazy = { M.me } } ----@type table -M.indexed = { [M.me] = { "lazy" } } -M.indexed_unloaded = false -M.indexed_rtp = 0 --- selene:allow(global_usage) -M._loadfile = _G.loadfile --- checks whether the cached modpath is still valid -function M.check_path(modname, modpath) - -- HACK: never return packer paths - if modpath:find("/site/pack/packer/", 1, true) then - return false - end - - -- check rtp excluding plugins. This is a very small list, so should be fast - for _, path in ipairs(M.get_rtp()) do - if modpath:find(path .. "/", 1, true) == 1 then - return true +-- slightly faster/different version than vim.fs.normalize +-- we also need to have it here, since the cache will load vim.fs +---@private +function Cache.normalize(path) + if path:sub(1, 1) == "~" then + local home = vim.loop.os_homedir() + if home:sub(-1) == "\\" or home:sub(-1) == "/" then + home = home:sub(1, -2) end + path = home .. path:sub(2) end - - -- the correct lazy path should be part of rtp. - -- so if we get here, this is folke using the local dev instance ;) - if modname and (modname == "lazy" or modname:sub(1, 5) == "lazy.") then - return false - end - - return modname and M.check_autoload(modname, modpath) + path = path:gsub("\\", "/"):gsub("/+", "/") + return path:sub(-1) == "/" and path:sub(1, -2) or path end -function M.check_autoload(modname, modpath) - local start = uv.hrtime() - M.stats.autoload.total = M.stats.autoload.total + 1 - - -- check plugins. Again fast, since we check the plugin name from the path. - -- only needed when the plugin mod has been loaded - ---@type LazyCorePlugin - local Plugin = package.loaded["lazy.core.plugin"] - if Plugin then - local plugin = Plugin.find(modpath) - if plugin and modpath:find(plugin.dir, 1, true) == 1 then - -- we're not interested in loader time, so calculate delta here - M.stats.autoload.time = M.stats.autoload.time + uv.hrtime() - start - -- don't load if we're loading specs or if the plugin is already loaded - if not (Plugin.loading or plugin._.loaded) then - if plugin.module == false then - error("Plugin " .. plugin.name .. " is not loaded and is configured with module=false") - end - require("lazy.core.loader").load(plugin, { require = modname }) +---@private +function Cache.get_rtp() + if vim.in_fast_event() then + return Cache._rtp or {} + end + local key = vim.go.rtp + if key ~= Cache._rtp_key then + Cache._rtp = {} + for _, path in ipairs(vim.api.nvim_get_runtime_file("", true)) do + path = Cache.normalize(path) + -- skip after directories + if path:sub(-6, -1) ~= "/after" then + Cache._rtp[#Cache._rtp + 1] = path end - return true + end + Cache._rtp_key = key + end + return Cache._rtp +end + +---@param name string can be a module name, or a file name +---@private +function Cache.cache_file(name) + return M.path .. "/" .. name:gsub("[/\\:]", "%%") .. "c" +end + +---@param entry CacheEntry +---@private +function Cache.write(name, entry) + local cname = Cache.cache_file(name) + local f = assert(uv.fs_open(cname, "w", 438)) + local header = { + M.VERSION, + entry.hash.size, + entry.hash.mtime.sec, + entry.hash.mtime.nsec, + #entry.modpath, + } + uv.fs_write(f, ffi.string(ffi.new("const uint32_t[5]", header), 20)) + uv.fs_write(f, entry.modpath) + uv.fs_write(f, entry.chunk) + uv.fs_close(f) +end + +---@return CacheEntry? +---@private +function Cache.read(name) + local cname = Cache.cache_file(name) + local f = uv.fs_open(cname, "r", 438) + if f then + local hash = uv.fs_fstat(f) --[[@as CacheHash]] + local data = uv.fs_read(f, hash.size, 0) --[[@as string]] + uv.fs_close(f) + + ---@type integer[]|{[0]:integer} + local header = ffi.cast("uint32_t*", ffi.new("const char[20]", data:sub(1, 20))) + if header[0] ~= M.VERSION then + return + end + local modpath = data:sub(21, 20 + header[4]) + return { + hash = { size = header[1], mtime = { sec = header[2], nsec = header[3] } }, + chunk = data:sub(20 + header[4] + 1), + modpath = modpath, + } + end +end + +---@param modname string +---@private +function Cache.loader(modname) + modname = modname:gsub("/", ".") + local modpath, hash = Cache.find(modname) + if modpath then + return Cache.load(modpath, { hash = hash }) + end + return "module " .. modname .. " not found" +end + +---@param filename? string +---@param mode? "b"|"t"|"bt" +---@param env? table +---@return function?, string? error_message +---@private +function Cache.loadfile(filename, mode, env) + filename = Cache.normalize(filename) + return Cache.load(filename, { mode = mode, env = env }) +end + +---@param h1 CacheHash +---@param h2 CacheHash +---@private +function Cache.eq(h1, h2) + return h1 and h2 and h1.size == h2.size and h1.mtime.sec == h2.mtime.sec and h1.mtime.nsec == h2.mtime.nsec +end + +---@param modpath string +---@param opts? {hash?: CacheHash, mode?: "b"|"t"|"bt", env?:table} +---@return function?, string? error_message +---@private +function Cache.load(modpath, opts) + opts = opts or {} + local hash = opts.hash or uv.fs_stat(modpath) + if not hash then + -- trigger correct error + return Cache._loadfile(modpath) + end + + ---@type function?, string? + local chunk, err + local entry = Cache.read(modpath) + if entry and Cache.eq(entry.hash, hash) then + -- found in cache and up to date + chunk, err = loadstring(entry.chunk --[[@as string]], "@" .. entry.modpath) + if not (err and err:find("cannot load incompatible bytecode", 1, true)) then + return chunk, err end end - M.stats.autoload.time = M.stats.autoload.time + uv.hrtime() - start - return false + entry = { hash = hash, modpath = modpath } + + chunk, err = Cache._loadfile(entry.modpath) + if chunk then + entry.chunk = string.dump(chunk) + Cache.write(modpath, entry) + end + return chunk, err +end + +---@param modname string +---@param opts? CacheFindOpts +---@return string? modpath, CacheHash? hash +function Cache.find(modname, opts) + opts = opts or {} + local start = uv.hrtime() + M.stats.total = M.stats.total + 1 + modname = modname:gsub("/", ".") + local basename = modname:gsub("%.", "/") + local idx = modname:find(".", 1, true) + local topmod = idx and modname:sub(1, idx - 1) or modname + + -- OPTIM: search for a directory first when topmod == modname + local patterns = opts.patterns or (topmod == modname and { "/init.lua", ".lua" } or { ".lua", "/init.lua" }) + local rtp = opts.rtp ~= false and Cache.get_rtp() or {} + if opts.paths then + rtp = vim.deepcopy(rtp) + for _, dir in ipairs(opts.paths) do + rtp[#rtp + 1] = Cache.normalize(dir) + end + end + for p, pattern in ipairs(patterns) do + patterns[p] = "/lua/" .. basename .. pattern + end + + for _, path in ipairs(rtp) do + if M.lsmod(path)[topmod] then + for _, pattern in ipairs(patterns) do + local modpath = path .. pattern + M.stats.stat = M.stats.stat + 1 + local hash = uv.fs_stat(modpath) + if hash then + M.stats.time = M.stats.time + uv.hrtime() - start + return modpath, hash + end + end + end + end + + -- module not found + M.stats.not_found = M.stats.not_found + 1 + M.stats.time = M.stats.time + uv.hrtime() - start +end + +--- Resets the topmods cache for the path +---@param path string +function M.reset(path) + Cache._topmods[Cache.normalize(path)] = nil +end + +function M.enable() + if M.enabled then + return + end + M.enabled = true + vim.fn.mkdir(vim.fn.fnamemodify(M.path, ":p"), "p") + -- selene: allow(global_usage) + _G.loadfile = Cache.loadfile + table.insert(package.loaders, 2, Cache.loader) end function M.disable() if not M.enabled then return end - -- selene:allow(global_usage) - _G.loadfile = M._loadfile M.enabled = false - if M.debug and vim.tbl_count(M.topmods) > 1 then - M.log(M.topmods, { level = vim.log.levels.WARN, title = "topmods" }) - end - if M.debug and false then - local stats = vim.deepcopy(M.stats) - stats.time = (stats.time or 0) / 1e6 - stats.find.time = (stats.find.time or 0) / 1e6 - stats.autoload.time = (stats.autoload.time or 0) / 1e6 - M.log(stats, { title = "stats" }) - end -end - ----@param msg string|table ----@param opts? LazyNotifyOpts -function M.log(msg, opts) - if M.debug then - msg = vim.deepcopy(msg) - vim.schedule(function() - require("lazy.core.util").debug(msg, opts) - end) - end -end - -function M.check_loaded(modname) + -- selene: allow(global_usage) + _G.loadfile = Cache._loadfile ---@diagnostic disable-next-line: no-unknown - local mod = package.loaded[modname] - if type(mod) == "table" then - return function() - return mod + for l, loader in ipairs(package.loaders) do + if loader == Cache.loader then + table.remove(package.loaders, l) end end end ----@param modname string ----@return fun()|string -function M.loader(modname) - modname = modname:gsub("/", ".") - local entry = M.cache[modname] - - local chunk, err - if entry then - if M.check_path(modname, entry.modpath) then - M.stats.find.total = M.stats.find.total + 1 - chunk, err = M.load(modname, entry.modpath) - else - M.cache[modname] = nil - M.dirty = true - end - end - if not chunk then - -- find the modpath and load the module - local modpath = M.find(modname) - if modpath then - M.check_autoload(modname, modpath) - if M.enabled then - chunk, err = M.load(modname, modpath) - else - chunk = M.check_loaded(modname) - if not chunk then - chunk, err = M._loadfile(modpath) - end +-- Return the top-level `/lua/*` modules for this path +---@return string[] +function M.lsmod(path) + if not Cache._topmods[path] then + M.stats.index = M.stats.index + 1 + Cache._topmods[path] = {} + local handle = vim.loop.fs_scandir(path .. "/lua") + while handle do + local name, t = vim.loop.fs_scandir_next(handle) + if not name then + break end - end - end - return chunk or err or ("module " .. modname .. " not found") -end - ----@param modpath string ----@return any, string? -function M.loadfile(modpath) - modpath = modpath:gsub("\\", "/") - return M.load(modpath, modpath) -end - ----@param modkey string ----@param modpath string ----@return function?, string? error_message -function M.load(modkey, modpath) - local chunk, err - chunk = M.check_loaded(modkey) - if chunk then - return chunk - end - modpath = modpath:gsub("\\", "/") - local hash = M.hash(modpath) - if not hash then - -- trigger correct error - return M._loadfile(modpath) - end - - local entry = M.cache[modkey] - if entry then - entry.modpath = modpath - entry.used = os.time() - if M.eq(entry.hash, hash) then - -- found in cache and up to date - chunk, err = loadstring(entry.chunk --[[@as string]], "@" .. entry.modpath) - if not (err and err:find("cannot load incompatible bytecode", 1, true)) then - return chunk, err - end - end - else - entry = { hash = hash, modpath = modpath, used = os.time() } - M.cache[modkey] = entry - end - entry.hash = hash - - if M.debug then - M.log("`" .. modpath .. "`", { level = vim.log.levels.WARN, title = "Cache.load" }) - end - - chunk, err = M._loadfile(entry.modpath) - M.dirty = true - if chunk then - entry.chunk = string.dump(chunk) - else - M.cache[modkey] = nil - end - return chunk, err -end - --- index the top-level lua modules for this path -function M._index(path) - if not M.indexed[path] and path:sub(-6, -1) ~= "/after" then - M.stats.find.index = M.stats.find.index + 1 - ---@type LazyUtilCore - local Util = package.loaded["lazy.core.util"] - if not Util then - return false - end - M.indexed[path] = {} - Util.ls(path .. "/lua", function(_, name, t) + -- HACK: type is not always returned due to a bug in luv + t = t or vim.loop.fs_stat(path .. "/" .. name).type + ---@type string local topname if name:sub(-4) == ".lua" then topname = name:sub(1, -5) @@ -242,283 +266,41 @@ function M._index(path) topname = name end if topname then - M.topmods[topname] = M.topmods[topname] or {} - if not vim.tbl_contains(M.topmods[topname], path) then - table.insert(M.topmods[topname], path) - end - if not vim.tbl_contains(M.indexed[path], topname) then - table.insert(M.indexed[path], topname) - end - end - end) - return true - end - return false -end - -function M.get_topmods(path) - M._index(path) - return M.indexed[path] or {} -end - ----@param modname string ----@return string? -function M.find_root(modname) - if M.cache[modname] then - -- check if modname is in cache - local modpath = M.cache[modname].modpath - if M.check_path(modname, modpath) and uv.fs_stat(modpath) then - local root = modpath:gsub("/init%.lua$", ""):gsub("%.lua$", "") - return root - end - else - -- in case modname is just a directory and not a real mod, - -- check for any children in the cache - for child, entry in pairs(M.cache) do - if child:find(modname, 1, true) == 1 then - if M.check_path(child, entry.modpath) and uv.fs_stat(entry.modpath) then - local basename = modname:gsub("%.", "/") - local childbase = child:gsub("%.", "/") - local ret = entry.modpath:gsub("/init%.lua$", ""):gsub("%.lua$", "") - local idx = assert(ret:find(childbase, 1, true)) - return ret:sub(1, idx - 1) .. basename - end + Cache._topmods[path][topname] = true end end end - - -- not found in cache, so find the root with the special pattern - local modpath = M.find(modname, { patterns = { "" } }) - if modpath then - local root = modpath:gsub("/init%.lua$", ""):gsub("%.lua$", "") - return root - end + return Cache._topmods[path] end ---@param modname string ----@param opts? {patterns?:string[]} ----@return string? +---@param opts? CacheFindOpts +---@return string? modpath function M.find(modname, opts) - opts = opts or {} - - M.stats.find.total = M.stats.find.total + 1 - local start = uv.hrtime() - local basename = modname:gsub("%.", "/") - local idx = modname:find(".", 1, true) - local topmod = idx and modname:sub(1, idx - 1) or modname - - -- search for a directory first when topmod == modname - local patterns = topmod == modname and { "/init.lua", ".lua" } or { ".lua", "/init.lua" } - - if opts.patterns then - vim.list_extend(patterns, opts.patterns) - end - - -- check top-level mods to find the module - local function _find() - for _, toppath in ipairs(M.topmods[topmod] or {}) do - for _, pattern in ipairs(patterns) do - local path = toppath .. "/lua/" .. basename .. pattern - M.stats.find.stat = M.stats.find.stat + 1 - if uv.fs_stat(path) then - return path - end - end - end - end - - local modpath = _find() - if not modpath then - -- update rtp - local rtp = M.list_rtp() - if #rtp ~= M.indexed_rtp then - M.indexed_rtp = #rtp - local updated = false - for _, path in ipairs(rtp) do - updated = M._index(path) or updated - end - if updated then - modpath = _find() - end - end - - -- update unloaded - if not modpath and not M.indexed_unloaded then - M.indexed_unloaded = true - local updated = false - ---@type LazyCoreConfig - local Config = package.loaded["lazy.core.config"] - if Config and Config.spec then - for _, plugin in pairs(Config.spec.plugins) do - if not (M.indexed[plugin.dir] or plugin._.loaded or plugin.module == false) then - updated = M._index(plugin.dir) or updated - end - end - end - if updated then - modpath = _find() - end - end - - -- module not found - if not modpath then - M.stats.find.not_found = M.stats.find.not_found + 1 - end - end - - M.stats.find.time = M.stats.find.time + uv.hrtime() - start + local modpath = Cache.find(modname, opts) return modpath end --- returns the cached RTP excluding plugin dirs -function M.get_rtp() - local rtp = M.list_rtp() - if not M.rtp or #rtp ~= M.rtp_total then - M.rtp_total = #rtp - M.rtp = {} - ---@type table - local skip = {} - -- only skip plugins once Config has been setup - ---@type LazyCoreConfig - local Config = package.loaded["lazy.core.config"] - if Config then - for _, plugin in pairs(Config.plugins) do - if plugin.name ~= "lazy.nvim" then - skip[plugin.dir] = true - end - end - end - for _, path in ipairs(rtp) do - ---@type string - path = path:gsub("\\", "/") - if not skip[path] and not path:find("after/?$") then - M.rtp[#M.rtp + 1] = path - end - end +function M.inspect() + local function ms(nsec) + return math.floor(nsec / 1e6 * 1000 + 0.5) / 1000 .. "ms" end - return M.rtp -end - -function M.list_rtp() - return vim.api.nvim_get_runtime_file("", true) -end - ----@param opts? LazyConfig -function M.setup(opts) - -- no fancy deep extend here. just set the options - if opts and opts.performance and opts.performance.cache then - ---@diagnostic disable-next-line: no-unknown - for k, v in pairs(opts.performance.cache) do - ---@diagnostic disable-next-line: no-unknown - M.config[k] = v - end + local props = { + { "total", M.stats.total, "Number" }, + { "time", ms(M.stats.time), "Bold" }, + { "avg time", ms(M.stats.time / M.stats.total), "Bold" }, + { "index", M.stats.index, "Number" }, + { "fs_stat", M.stats.stat, "Number" }, + { "not found", M.stats.not_found, "Number" }, + } + local chunks = {} ---@type string[][] + for _, prop in ipairs(props) do + chunks[#chunks + 1] = { "* " .. prop[1] .. ": " } + chunks[#chunks + 1] = { tostring(prop[2]) .. "\n", prop[3] } end - M.debug = opts and opts.debug - M.enabled = M.config.enabled - - if M.enabled then - table.insert(package.loaders, 2, M.loader) - M.load_cache() - -- selene:allow(global_usage) - _G.loadfile = M.loadfile - if #M.config.disable_events > 0 then - vim.api.nvim_create_autocmd(M.config.disable_events, { once = true, callback = M.disable }) - end - else - -- we need to always add the loader since this will autoload unloaded modules - table.insert(package.loaders, M.loader) - end - - return M + vim.api.nvim_echo(chunks, true, {}) end ----@return CacheHash? -function M.hash(file) - local ok, ret = pcall(uv.fs_stat, file) - return ok and ret or nil -end - ----@param h1 CacheHash ----@param h2 CacheHash -function M.eq(h1, h2) - return h1 and h2 and h1.size == h2.size and h1.mtime.sec == h2.mtime.sec and h1.mtime.nsec == h2.mtime.nsec -end - -function M.save_cache() - vim.fn.mkdir(vim.fn.fnamemodify(M.config.path, ":p:h"), "p") - local f = assert(uv.fs_open(M.config.path, "w", 438)) - uv.fs_write(f, M.VERSION) - uv.fs_write(f, "\0") - for modname, entry in pairs(M.cache) do - if entry.used > os.time() - M.config.ttl then - entry.modname = modname - local header = { - entry.hash.size, - entry.hash.mtime.sec, - entry.hash.mtime.nsec, - #modname, - #entry.chunk, - #entry.modpath, - entry.used, - } - uv.fs_write(f, ffi.string(ffi.new("const uint32_t[7]", header), 28)) - uv.fs_write(f, modname) - uv.fs_write(f, entry.chunk) - uv.fs_write(f, entry.modpath) - end - end - uv.fs_close(f) -end - -function M.load_cache() - M.cache = {} - local f = uv.fs_open(M.config.path, "r", 438) - if f then - cache_hash = uv.fs_fstat(f) --[[@as CacheHash]] - local data = uv.fs_read(f, cache_hash.size, 0) --[[@as string]] - uv.fs_close(f) - - local zero = data:find("\0", 1, true) - if not zero then - return - end - - if M.VERSION ~= data:sub(1, zero - 1) then - return - end - - local offset = zero + 1 - while offset + 1 < #data do - local header = ffi.cast("uint32_t*", ffi.new("const char[28]", data:sub(offset, offset + 27))) - offset = offset + 28 - local modname = data:sub(offset, offset + header[3] - 1) - offset = offset + header[3] - local chunk = data:sub(offset, offset + header[4] - 1) - offset = offset + header[4] - local file = data:sub(offset, offset + header[5] - 1) - offset = offset + header[5] - M.cache[modname] = { - hash = { size = header[0], mtime = { sec = header[1], nsec = header[2] } }, - chunk = chunk, - modpath = file, - used = header[6], - } - end - end -end - -function M.autosave() - vim.api.nvim_create_autocmd("VimLeavePre", { - callback = function() - if M.dirty then - local hash = M.hash(M.config.path) - -- abort when the file was changed in the meantime - if hash == nil or M.eq(cache_hash, hash) then - M.save_cache() - end - end - end, - }) -end +M._Cache = Cache return M diff --git a/lua/lazy/core/config.lua b/lua/lazy/core/config.lua index c1a236f..bc8257b 100644 --- a/lua/lazy/core/config.lua +++ b/lua/lazy/core/config.lua @@ -113,8 +113,9 @@ M.defaults = { notify = true, -- get a notification when changes are found }, performance = { - ---@type LazyCacheConfig - cache = nil, + cache = { + enabled = true, + }, reset_packpath = true, -- reset the package path to improve startup time rtp = { reset = true, -- reset the runtime path to $VIMRUNTIME and your config directory @@ -226,7 +227,6 @@ function M.setup(opts) pattern = "VeryLazy", once = true, callback = function() - require("lazy.core.cache").autosave() require("lazy.view.commands").setup() if M.options.change_detection.enabled then require("lazy.manage.reloader").enable() diff --git a/lua/lazy/core/loader.lua b/lua/lazy/core/loader.lua index e826654..0f1b977 100644 --- a/lua/lazy/core/loader.lua +++ b/lua/lazy/core/loader.lua @@ -72,7 +72,7 @@ function M.install_missing() -- remove and installed plugins from indexed, so cache will index again for _, p in pairs(Config.plugins) do if p._.installed then - Cache.indexed[p.dir] = nil + Cache.reset(p.dir) end end -- reload plugins @@ -341,7 +341,7 @@ function M.get_main(plugin) local normname = Util.normname(plugin.name) ---@type string[] local mods = {} - for _, modname in ipairs(Cache.get_topmods(plugin.dir)) do + for modname, _ in pairs(Cache.lsmod(plugin.dir)) do mods[#mods + 1] = modname local modnorm = Util.normname(modname) -- if we found an exact match, then use that @@ -450,4 +450,28 @@ function M.colorscheme(name) end end +---@param modname string +function M.loader(modname) + local modpath = Cache.find(modname, { rtp = false, paths = Util.get_unloaded_rtp(modname) }) + if modpath then + local plugin = Plugin.find(modpath) + if plugin and modpath:find(plugin.dir, 1, true) == 1 then + -- don't load if we're loading specs or if the plugin is already loaded + if not (Plugin.loading or plugin._.loaded) then + if plugin.module == false then + error("Plugin " .. plugin.name .. " is not loaded and is configured with module=false") + end + M.load(plugin, { require = modname }) + end + local mod = package.loaded[modname] + if type(mod) == "table" then + return function() + return mod + end + end + return loadfile(modpath) + end + end +end + return M diff --git a/lua/lazy/core/util.lua b/lua/lazy/core/util.lua index fd2cce4..42325dd 100644 --- a/lua/lazy/core/util.lua +++ b/lua/lazy/core/util.lua @@ -217,11 +217,46 @@ function M.walkmods(root, fn, modname) end) end +---@param modname string +function M.get_unloaded_rtp(modname) + modname = modname:gsub("/", ".") + local idx = modname:find(".", 1, true) + local topmod = idx and modname:sub(1, idx - 1) or modname + topmod = M.normname(topmod) + + local rtp = {} + local Config = require("lazy.core.config") + if Config.spec then + for _, plugin in pairs(Config.spec.plugins) do + if not (plugin._.loaded or plugin.module == false) then + if topmod == M.normname(plugin.name) then + table.insert(rtp, 1, plugin.dir) + else + table.insert(rtp, plugin.dir) + end + end + end + end + return rtp +end + +function M.find_root(modname) + local Cache = require("lazy.core.cache") + local modpath = Cache.find(modname, { + rtp = true, + paths = M.get_unloaded_rtp(modname), + patterns = { "", ".lua" }, + }) + if modpath then + local root = modpath:gsub("/init%.lua$", ""):gsub("%.lua$", "") + return root + end +end + ---@param modname string ---@param fn fun(modname:string, modpath:string) function M.lsmod(modname, fn) - local Cache = require("lazy.core.cache") - local root = Cache.find_root(modname) + local root = M.find_root(modname) if not root then return end diff --git a/lua/lazy/docs.lua b/lua/lazy/docs.lua index da3a166..bf42e98 100644 --- a/lua/lazy/docs.lua +++ b/lua/lazy/docs.lua @@ -131,12 +131,7 @@ function M.colors() end function M.update() - local cache_config = M.extract("lua/lazy/core/cache.lua", "\nM%.config = ({.-\n})") local config = M.extract("lua/lazy/core/config.lua", "\nM%.defaults = ({.-\n})") - config = config:gsub( - "\n%s*%-%-%-@type LazyCacheConfig.*cache = nil,", - "\n" .. M.indent("cache = " .. cache_config .. ",", 4) - ) config = config:gsub("%s*debug = false.\n", "\n") M.save({ bootstrap = M.extract("lua/lazy/init.lua", "function M%.bootstrap%(%)\n(.-)\nend"), diff --git a/lua/lazy/init.lua b/lua/lazy/init.lua index a0b073a..e81c4ec 100644 --- a/lua/lazy/init.lua +++ b/lua/lazy/init.lua @@ -34,13 +34,16 @@ function M.setup(spec, opts) local start = vim.loop.hrtime() -- load module cache before anything else - require("lazy.core.cache").setup(opts) + if not (opts and opts.performance and opts.performance.cache and opts.performance.cache.enabled == false) then + require("lazy.core.cache").enable() + end require("lazy.stats").track("LazyStart") local Util = require("lazy.core.util") local Config = require("lazy.core.config") local Loader = require("lazy.core.loader") + table.insert(package.loaders, 3, Loader.loader) Util.track({ plugin = "lazy.nvim" }) -- setup start Util.track("module", vim.loop.hrtime() - start) diff --git a/lua/lazy/manage/reloader.lua b/lua/lazy/manage/reloader.lua index 7d30d3d..b6e1f1e 100644 --- a/lua/lazy/manage/reloader.lua +++ b/lua/lazy/manage/reloader.lua @@ -1,4 +1,3 @@ -local Cache = require("lazy.core.cache") local Config = require("lazy.core.config") local Util = require("lazy.util") local Plugin = require("lazy.core.plugin") @@ -6,12 +5,11 @@ local Loader = require("lazy.core.loader") local M = {} ----@type table +---@type table M.files = {} ---@type vim.loop.Timer M.timer = nil -M.root = nil function M.enable() if M.timer then @@ -19,7 +17,6 @@ function M.enable() end if #Config.spec.modules > 0 then M.timer = vim.loop.new_timer() - M.root = vim.fn.stdpath("config") .. "/lua" M.check(true) M.timer:start(2000, 2000, M.check) end @@ -32,6 +29,12 @@ function M.disable() end end +---@param h1 vim.loop.Stat +---@param h2 vim.loop.Stat +function M.eq(h1, h2) + return h1 and h2 and h1.size == h2.size and h1.mtime.sec == h2.mtime.sec and h1.mtime.nsec == h2.mtime.nsec +end + function M.check(start) ---@type table local checked = {} @@ -41,10 +44,10 @@ function M.check(start) -- spec is a module local function check(_, modpath) checked[modpath] = true - local hash = Cache.hash(modpath) + local hash = vim.loop.fs_stat(modpath) if hash then if M.files[modpath] then - if not Cache.eq(M.files[modpath], hash) then + if not M.eq(M.files[modpath], hash) then M.files[modpath] = hash table.insert(changes, { file = modpath, what = "changed" }) end diff --git a/lua/lazy/view/render.lua b/lua/lazy/view/render.lua index e4f4529..6f6f857 100644 --- a/lua/lazy/view/render.lua +++ b/lua/lazy/view/render.lua @@ -672,33 +672,14 @@ function M:debug() self:append("Cache.find()", "LazyH2"):nl() self:props({ - { "total", Cache.stats.find.total, "Number" }, - { "time", self:ms(Cache.stats.find.time, 3), "Bold" }, - { "avg time", self:ms(Cache.stats.find.time / Cache.stats.find.total, 3), "Bold" }, - { "index", Cache.stats.find.index, "Number" }, - { "fs_stat", Cache.stats.find.stat, "Number" }, - { "not found", Cache.stats.find.not_found, "Number" }, + { "total", Cache.stats.total, "Number" }, + { "time", self:ms(Cache.stats.time, 3), "Bold" }, + { "avg time", self:ms(Cache.stats.time / Cache.stats.total, 3), "Bold" }, + { "index", Cache.stats.index, "Number" }, + { "fs_stat", Cache.stats.stat, "Number" }, + { "not found", Cache.stats.not_found, "Number" }, }, { indent = 2 }) self:nl() - - self:append("Cache.autoload()", "LazyH2"):nl() - self:props({ - { "total", Cache.stats.autoload.total, "Number" }, - { "time", self:ms(Cache.stats.autoload.time, 3), "Bold" }, - { "avg time", self:ms(Cache.stats.autoload.time / Cache.stats.autoload.total, 3), "Bold" }, - }, { indent = 2 }) - self:nl() - - self:append("Cache", "LazyH2"):nl() - local Cache = require("lazy.core.cache") - Util.foreach(Cache.cache, function(modname, entry) - local kb = math.floor(#entry.chunk / 10.24) / 100 - self:append("● ", "LazySpecial", { indent = 2 }):append(modname):append(" " .. kb .. "Kb", "Bold") - if entry.modpath ~= modname then - self:append(" " .. vim.fn.fnamemodify(entry.modpath, ":p:~:."), "LazyComment") - end - self:nl() - end) end return M diff --git a/tests/core/util_spec.lua b/tests/core/util_spec.lua index 87102d3..86b9f06 100644 --- a/tests/core/util_spec.lua +++ b/tests/core/util_spec.lua @@ -51,10 +51,8 @@ describe("util", function() local files = Helpers.fs_create(test.files) -- test with empty cache - Cache.cache = {} - Cache.indexed = {} - Cache.indexed_rtp = false - local root = Cache.find_root(test.mod) + package.loaded["lazy.core.cache"] = nil + local root = Util.find_root(test.mod) assert(root, "no root found for " .. test.mod .. " (test " .. t .. ")") assert.same(Helpers.path(test.root), root) local mods = {} @@ -65,13 +63,8 @@ describe("util", function() assert.same(expected, mods) -- fill the cache - Cache.cache = {} - for i, file in ipairs(files) do - Cache.cache[test.mods[i]] = { modpath = file } - end - Cache.indexed = {} - Cache.indexed_rtp = false - root = Cache.find_root(test.mod) + package.loaded["lazy.core.cache"] = nil + root = Util.find_root(test.mod) assert(root, "no root found for " .. test.mod .. " (test " .. t .. ")") assert.same(Helpers.path(test.root), root) mods = {} @@ -85,12 +78,12 @@ describe("util", function() it("find the correct root with dels", function() Cache.cache = {} - Cache.indexed = {} - Cache.indexed_rtp = false + Cache._topmods = {} + Cache.topmods_rtp = false vim.opt.rtp:append(Helpers.path("old")) Helpers.fs_create({ "old/lua/foobar/init.lua" }) Cache.cache["foobar"] = { modpath = Helpers.path("old/lua/foobar/init.lua") } - local root = Cache.find_root("foobar") + local root = Util.find_root("foobar") assert(root, "foobar root not found") assert.same(Helpers.path("old/lua/foobar"), root) @@ -98,24 +91,22 @@ describe("util", function() assert(not vim.loop.fs_stat(Helpers.path("old/lua/foobar")), "old/lua/foobar should not exist") -- vim.opt.rtp = rtp - Cache.indexed = {} - Cache.indexed_rtp = false + Cache._topmods = {} vim.opt.rtp:append(Helpers.path("new")) Helpers.fs_create({ "new/lua/foobar/init.lua" }) - root = Cache.find_root("foobar") + root = Util.find_root("foobar") assert(root, "foobar root not found") assert.same(Helpers.path("new/lua/foobar"), root) end) it("find the correct root with mod dels", function() Cache.cache = {} - Cache.indexed = {} - Cache.indexed_rtp = false + Cache._topmods = {} Cache.enabled = true vim.opt.rtp:append(Helpers.path("old")) Helpers.fs_create({ "old/lua/foobar/test.lua" }) Cache.cache["foobar.test"] = { modpath = Helpers.path("old/lua/foobar/test.lua") } - local root = Cache.find_root("foobar") + local root = Util.find_root("foobar") assert(root, "foobar root not found") assert.same(Helpers.path("old/lua/foobar"), root) assert(not Cache.cache["foobar"], "foobar should not be in cache") @@ -124,11 +115,10 @@ describe("util", function() Helpers.fs_rm("old") -- vim.opt.rtp = rtp - Cache.indexed = {} - Cache.indexed_rtp = false + Cache._topmods = {} vim.opt.rtp:append(Helpers.path("new")) Helpers.fs_create({ "new/lua/foobar/test.lua" }) - root = Cache.find_root("foobar") + root = Util.find_root("foobar") assert(root, "foobar root not found") assert.same(Helpers.path("new/lua/foobar"), root) end)