mirror of https://github.com/folke/lazy.nvim.git
refactor: new cache
This commit is contained in:
parent
e115f5ec17
commit
983e1c5e34
|
@ -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<string, table<string,true>>
|
||||
_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<string, table<string,true>>
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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<string, CacheHash>
|
||||
---@type table<string, vim.loop.Stat>
|
||||
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<string,true>
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue