diff --git a/lua/lazy/core/cache.lua b/lua/lazy/core/cache.lua index ddb167a..92f8630 100644 --- a/lua/lazy/core/cache.lua +++ b/lua/lazy/core/cache.lua @@ -6,46 +6,29 @@ local M = {} ---@alias CacheHash {mtime: {sec:number, nsec:number}, size:number} ---@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 } -M.config = { - enabled = false, - path = vim.fn.stdpath("cache") .. "/lazy/luac", +---@class ModuleCache +---@field _rtp string[] +---@field _rtp_key string +local Cache = { + ---@type table> + _topmods = {}, + _loadfile = loadfile, } -M._loadfile = loadfile - -M.stats = { - find = { total = 0, time = 0, index = 0, stat = 0, not_found = 0 }, -} ----@type string -M._rtp_key = nil ----@type string[] -M._rtp = nil ----@type table> -M._topmods = {} - -function M.get_rtp() - if vim.in_fast_event() then - return M._rtp or {} - end - local key = vim.go.rtp - if vim.go.rtp ~= M._rtp_key then - M._rtp = {} - for _, path in ipairs(vim.api.nvim_get_runtime_file("", true)) do - path = M.normalize(path) - -- skip after directories - if path:sub(-6, -1) ~= "/after" then - M._rtp[#M._rtp + 1] = path - end - end - M._rtp_key = key - end - return M._rtp -end -- slightly faster/different version than vim.fs.normalize -- we also need to have it here, since the cache will load vim.fs -function M.normalize(path) +---@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 @@ -57,97 +40,36 @@ function M.normalize(path) return path:sub(-1) == "/" and path:sub(1, -2) or path end -function M.reset(path) - M._topmods[M.normalize(path)] = nil -end - --- index the top-level lua modules for this path ----@return string[] -function M.get_topmods(path) - if not M._topmods[path] then - M.stats.find.index = M.stats.find.index + 1 - M._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 - ---@cast name string - ---@cast t "file"|"directory"|"link" - -- 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) - elseif t == "link" or t == "directory" then - topname = name - end - if topname then - M._topmods[path][topname] = true +---@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 end + Cache._rtp_key = key end - return M._topmods[path] -end - ----@param modname string ----@param opts? {rtp:string[], patterns:string[]} ----@return string?, CacheHash? -function M.find(modname, opts) - opts = opts or {} - local start = uv.hrtime() - M.stats.find.total = M.stats.find.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 or M.get_rtp() - - for _, path in ipairs(rtp) do - if M.get_topmods(path)[topmod] then - for _, pattern in ipairs(patterns) do - local modpath = path .. "/lua/" .. basename .. pattern - M.stats.find.stat = M.stats.find.stat + 1 - local hash = uv.fs_stat(modpath) - if hash then - M.stats.find.time = M.stats.find.time + uv.hrtime() - start - return modpath, hash - end - end - end - end - - -- module not found - M.stats.find.not_found = M.stats.find.not_found + 1 - M.stats.find.time = M.stats.find.time + uv.hrtime() - start -end - -function M.setup() - M.config.enabled = true - vim.fn.mkdir(vim.fn.fnamemodify(M.config.path, ":p"), "p") - _G.loadfile = M.loadfile - table.insert(package.loaders, 2, M.loader) + return Cache._rtp end ---@param name string can be a module name, or a file name -function M.cache_file(name) - return M.config.path .. "/" .. name:gsub("[/\\]", "%%") .. ".luac" -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 +---@private +function Cache.cache_file(name) + return M.path .. "/" .. name:gsub("[/\\]", "%%") .. ".luac" end ---@param entry CacheEntry -function M.write(name, entry) - local cname = M.cache_file(name) +---@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, @@ -163,8 +85,9 @@ function M.write(name, entry) end ---@return CacheEntry? -function M.read(name) - local cname = M.cache_file(name) +---@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]] @@ -186,58 +109,49 @@ function M.read(name) end ---@param modname string -function M.loader(modname) +---@private +function Cache.loader(modname) modname = modname:gsub("/", ".") - - local modpath, hash = M.find(modname) - ---@type function?, string? - local chunk, err + local modpath, hash = Cache.find(modname) if modpath then - chunk, err = M._load(modname, modpath, { hash = hash }) + return Cache.load(modpath, { hash = hash }) end - return chunk or err or ("module " .. modname .. " not found") + 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 ----@return any, string? -function M.loadfile(modpath) - modpath = M.normalize(modpath) - return M._load(modpath, modpath) -end - -function M.check_loaded(modname) - ---@diagnostic disable-next-line: no-unknown - local mod = package.loaded[modname] - if type(mod) == "table" then - return function() - return mod - end - end -end - ----@param modkey string ----@param modpath string ----@param opts? {hash?: CacheHash, entry?:CacheEntry} +---@param opts? {hash?: CacheHash, mode?: "b"|"t"|"bt", env?:table} ---@return function?, string? error_message -function M._load(modkey, modpath, opts) +---@private +function Cache.load(modpath, opts) opts = opts or {} - if not M.config.enabled then - return M._loadfile(modpath) - end - ---@type function?, string? - local chunk, err - chunk = M.check_loaded(modkey) - if chunk then - return chunk - end local hash = opts.hash or uv.fs_stat(modpath) if not hash then -- trigger correct error - return M._loadfile(modpath) + return Cache._loadfile(modpath) end - local entry = opts.entry or M.read(modkey) - if entry and M.eq(entry.hash, hash) then + ---@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 @@ -246,12 +160,147 @@ function M._load(modkey, modpath, opts) end entry = { hash = hash, modpath = modpath } - chunk, err = M._loadfile(entry.modpath) + chunk, err = Cache._loadfile(entry.modpath) if chunk then entry.chunk = string.dump(chunk) - M.write(modkey, entry) + 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 + M.enabled = false + -- selene: allow(global_usage) + _G.loadfile = Cache._loadfile + ---@diagnostic disable-next-line: no-unknown + for l, loader in ipairs(package.loaders) do + if loader == Cache.loader then + table.remove(package.loaders, l) + end + end +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 + -- 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) + elseif t == "link" or t == "directory" then + topname = name + end + if topname then + Cache._topmods[path][topname] = true + end + end + end + return Cache._topmods[path] +end + +---@param modname string +---@param opts? CacheFindOpts +---@return string? modpath +function M.find(modname, opts) + local modpath = Cache.find(modname, opts) + return modpath +end + +function M.inspect() + local function ms(nsec) + return math.floor(nsec / 1e6 * 1000 + 0.5) / 1000 .. "ms" + 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 + vim.api.nvim_echo(chunks, true, {}) +end + +M._Cache = Cache + return M diff --git a/lua/lazy/core/loader.lua b/lua/lazy/core/loader.lua index 7ec16a0..0f1b977 100644 --- a/lua/lazy/core/loader.lua +++ b/lua/lazy/core/loader.lua @@ -341,7 +341,7 @@ function M.get_main(plugin) local normname = Util.normname(plugin.name) ---@type string[] local mods = {} - for modname, _ in pairs(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 @@ -452,7 +452,7 @@ end ---@param modname string function M.loader(modname) - local modpath = Cache.find(modname, { rtp = Util.get_unloaded_rtp(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 @@ -463,7 +463,13 @@ function M.loader(modname) end M.load(plugin, { require = modname }) end - return Cache._load(modname, modpath) + local mod = package.loaded[modname] + if type(mod) == "table" then + return function() + return mod + end + end + return loadfile(modpath) end end end diff --git a/lua/lazy/core/util.lua b/lua/lazy/core/util.lua index f11669f..42325dd 100644 --- a/lua/lazy/core/util.lua +++ b/lua/lazy/core/util.lua @@ -242,9 +242,11 @@ end function M.find_root(modname) local Cache = require("lazy.core.cache") - local rtp = vim.deepcopy(Cache.get_rtp()) - vim.list_extend(rtp, M.get_unloaded_rtp(modname)) - local modpath = Cache.find(modname, { rtp = rtp, patterns = { "", ".lua" } }) + 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 diff --git a/lua/lazy/init.lua b/lua/lazy/init.lua index bd68c5f..df05824 100644 --- a/lua/lazy/init.lua +++ b/lua/lazy/init.lua @@ -34,7 +34,7 @@ function M.setup(spec, opts) local start = vim.loop.hrtime() -- load module cache before anything else - require("lazy.core.cache").setup() + require("lazy.core.cache").enable() require("lazy.stats").track("LazyStart") diff --git a/lua/lazy/manage/reloader.lua b/lua/lazy/manage/reloader.lua index 9774c30..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 = {} @@ -44,7 +47,7 @@ function M.check(start) 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 ec5570e..6f6f857 100644 --- a/lua/lazy/view/render.lua +++ b/lua/lazy/view/render.lua @@ -672,12 +672,12 @@ 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() end