mirror of
				https://github.com/folke/lazy.nvim.git
				synced 2025-11-04 00:11:06 +00:00 
			
		
		
		
	perf: split caching in state, cache and module
This commit is contained in:
		
					parent
					
						
							
								a543134b8c
							
						
					
				
			
			
				commit
				
					
						54d5ff18f5
					
				
			
		
					 8 changed files with 455 additions and 395 deletions
				
			
		
							
								
								
									
										109
									
								
								lua/lazy/core/cache.lua
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								lua/lazy/core/cache.lua
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										130
									
								
								lua/lazy/core/module.lua
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										127
									
								
								lua/lazy/core/state.lua
									
										
									
									
									
										Normal 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
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue