perf: split caching in state, cache and module

This commit is contained in:
Folke Lemaitre 2022-11-22 21:12:33 +01:00
parent a543134b8c
commit 54d5ff18f5
No known key found for this signature in database
GPG key ID: 41F8B1FBACAE2040
8 changed files with 455 additions and 395 deletions

109
lua/lazy/core/cache.lua Normal file
View file

@ -0,0 +1,109 @@
-- Simple string cache with fast saving and loading from file
local M = {}
local cache_path = vim.fn.stdpath("state") .. "/lazy/plugins.state"
---@type string
local cache_hash = nil
local dirty = false
---@type table<string,boolean>
local used = {}
---@type table<string,string>
local cache = {}
function M.get(key)
if cache[key] then
used[key] = true
return cache[key]
end
end
function M.set(key, value)
cache[key] = value
used[key] = true
dirty = true
end
function M.del(key)
cache[key] = nil
dirty = true
end
function M.dirty()
dirty = true
end
function M.use(pattern)
for key, _ in pairs(cache) do
if key:find(pattern) then
used[key] = true
end
end
end
function M.hash(file)
local stat = vim.loop.fs_stat(file)
return stat and (stat.mtime.sec .. stat.mtime.nsec .. stat.size)
end
function M.setup()
M.load()
vim.api.nvim_create_autocmd("User", {
pattern = "LazyDone",
once = true,
callback = function()
vim.api.nvim_create_autocmd("VimLeavePre", {
callback = function()
if dirty then
local hash = M.hash(cache_path)
-- abort when the file was changed in the meantime
if hash == nil or cache_hash == hash then
M.save()
end
end
end,
})
end,
})
end
function M.save()
require("lazy.core.state").save()
require("lazy.core.module").save()
vim.fn.mkdir(vim.fn.fnamemodify(cache_path, ":p:h"), "p")
local f = assert(io.open(cache_path, "wb"))
for key, value in pairs(cache) do
if used[key] then
f:write(key, "\0", tostring(#value), "\0", value)
end
end
f:close()
end
function M.load()
cache = {}
local f = io.open(cache_path, "rb")
if f then
cache_hash = M.hash(cache_path)
---@type string
local data = f:read("*a")
f:close()
local from = 1
local to = data:find("\0", from, true)
while to do
local key = data:sub(from, to - 1)
from = to + 1
to = data:find("\0", from, true)
local len = tonumber(data:sub(from, to - 1))
from = to + 1
cache[key] = data:sub(from, from + len - 1)
from = from + len
to = data:find("\0", from, true)
end
end
end
return M

130
lua/lazy/core/module.lua Normal file
View file

@ -0,0 +1,130 @@
local Cache = require("lazy.core.cache")
local M = {}
---@type table<string, {file: string, hash?:string}>
M.modules = {}
function M.add(modname, file)
if not M.modules[modname] then
M.modules[modname] = { file = file }
end
end
---@param modname string
function M.load(modname)
if type(package.loaded[modname]) == "table" then
return package.loaded[modname]
end
local info = M.modules[modname]
if info then
local err
---@type string|fun()|nil
local chunk = Cache.get(modname)
if not chunk then
vim.schedule(function()
vim.notify("loading " .. modname)
end)
chunk, err = loadfile(info.file)
if chunk then
Cache.set(modname, string.dump(chunk))
info.hash = info.hash or Cache.hash(info.file)
end
end
if type(chunk) == "string" then
chunk, err = loadstring(chunk --[[@as string]], "@" .. info.file)
end
if not chunk then
error(err)
end
---@type table
local mod = chunk()
package.loaded[modname] = mod
return mod
end
end
local function _add_module(dir, modname)
local d = vim.loop.fs_opendir(dir, nil, 100)
if d then
---@type {name: string, type: "file"|"directory"|"link"}[]
local entries = vim.loop.fs_readdir(d)
while entries do
for _, entry in ipairs(entries) do
local path = dir .. "/" .. entry.name
if entry.type == "directory" then
_add_module(path, modname .. "." .. entry.name)
else
local childname = entry.name:match("^(.*)%.lua$")
if childname then
local child = entry.name == "init.lua" and modname or (modname .. "." .. childname)
if child then
M.add(child, path)
end
end
end
end
entries = vim.loop.fs_readdir(d)
end
vim.loop.fs_closedir(d)
end
end
function M.add_module(path)
---@type string
local modname = path:match("/lua/(.*)/?")
assert(modname)
modname = modname:gsub("/", ".")
if vim.loop.fs_stat(path .. ".lua") then
M.add(modname, path .. ".lua")
end
_add_module(path, modname)
end
function M.setup()
-- load cache
local value = Cache.get("cache.modules")
if value then
M.modules = vim.json.decode(value)
for k, v in pairs(M.modules) do
if Cache.hash(v.file) ~= v.hash then
Cache.del(k)
M.changed = true
M.modules[k] = nil
end
end
end
-- preload core modules
local root = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":p:h:h")
for _, name in ipairs({ "util", "config", "plugin", "loader", "core.state" }) do
local modname = "lazy." .. name
M.add(modname, root .. "/" .. name:gsub("%.", "/") .. ".lua")
end
table.insert(package.loaders, 2, function(modname)
if M.modules[modname] then
return function()
return M.load(modname)
end
end
end)
return M
end
function M.save()
local value = {}
for k, v in pairs(M.modules) do
if v.hash then
value[k] = v
end
end
Cache.set("cache.modules", vim.json.encode(value))
end
return M

127
lua/lazy/core/state.lua Normal file
View file

@ -0,0 +1,127 @@
local Cache = require("lazy.core.cache")
local Module = require("lazy.core.module")
local M = {}
M.functions = { "init", "config", "run" }
M.changed = true
function M.save()
local Config = require("lazy.config")
---@class LazyState
local state = {
---@type LazyPlugin[]
plugins = {},
loaders = require("lazy.loader").loaders,
config = Config.options,
}
local skip = { installed = true, loaded = true, tasks = true, dirty = true, [1] = true, dir = true }
local funcount = 0
for _, plugin in pairs(Config.plugins) do
---@type LazyPlugin | {_chunks: string[] | table<string, number>}
local save = {}
table.insert(state.plugins, save)
for k, v in pairs(plugin) do
if type(v) == "function" then
if vim.tbl_contains(M.functions, k) then
if plugin.modname then
save[k] = true
else
funcount = funcount + 1
Cache.set("cache.state.fun." .. funcount, string.dump(v))
save[k] = funcount
end
end
elseif not skip[k] then
save[k] = v
end
end
end
Cache.set("cache.state", vim.json.encode(state))
end
local function load_plugin(plugin, fun, ...)
local mod = Module.load(plugin.modname)
for k, v in pairs(mod) do
if type(v) == "function" then
plugin[k] = v
end
end
return mod[fun](...)
end
function M.load()
---@type boolean, LazyState
local ok, state = pcall(vim.json.decode, Cache.get("cache.state"))
if not ok then
Cache.dirty()
return false
end
local Util = require("lazy.util")
local Config = require("lazy.config")
if not vim.deep_equal(Config.options, state.config) then
Cache.dirty()
return false
end
-- Check for installed plugins
---@type table<"opt"|"start", table<string,boolean>>
local installed = { opt = {}, start = {} }
for _, opt in ipairs({ "opt", "start" }) do
for _, entry in ipairs(Util.scandir(Config.options.package_path .. "/" .. opt)) do
if entry.type == "directory" or entry.type == "link" then
installed[opt][entry.name] = true
end
end
end
-- plugins
for _, plugin in ipairs(state.plugins) do
---@cast plugin LazyPlugin|{_chunks:table}
Config.plugins[plugin.name] = plugin
plugin.loaded = false
plugin.dir = Config.options.package_path .. "/" .. (plugin.opt and "opt" or "start") .. "/" .. plugin.pack
plugin.installed = installed[plugin.opt and "opt" or "start"][plugin.pack]
if plugin.modname then
-- mark module as used
if not Cache.get(plugin.modname) then
Util.error("Module missing for " .. plugin.name)
end
for _, fun in ipairs(M.functions) do
if plugin[fun] == true then
plugin[fun] = function(...)
return load_plugin(plugin, fun, ...)
end
end
end
else
for _, fun in ipairs(M.functions) do
if type(plugin[fun]) == "number" then
local chunk = Cache.get("cache.state.fun." .. plugin[fun])
if not chunk then
Util.error("Chunk missing for " .. plugin.name)
end
plugin[fun] = function(...)
plugin[fun] = loadstring(chunk)
return plugin[fun](...)
end
end
end
end
end
-- loaders
local Loader = require("lazy.loader")
Loader.loaders = state.loaders
M.changed = false
return true
end
return M