diff options
| -rw-r--r-- | .luacheckrc | 2 | ||||
| -rw-r--r-- | lua/lvim/impatient.lua | 309 | ||||
| -rw-r--r-- | lua/lvim/impatient/profile.lua | 109 | ||||
| -rw-r--r-- | lua/lvim/utils/hooks.lua | 5 | 
4 files changed, 274 insertions, 151 deletions
| diff --git a/.luacheckrc b/.luacheckrc index 7c592693..53307087 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,3 +1,4 @@ +---@diagnostic disable  -- vim: ft=lua tw=80  stds.nvim = { @@ -30,6 +31,7 @@ stds.nvim = {  std = "lua51+nvim"  files["tests/*_spec.lua"].std = "lua51+nvim+busted" +files["lua/lvim/impatient*"].ignore = {"121"}  -- Don't report unused self arguments of methods.  self = false diff --git a/lua/lvim/impatient.lua b/lua/lvim/impatient.lua index fe26b940..beb3862a 100644 --- a/lua/lvim/impatient.lua +++ b/lua/lvim/impatient.lua @@ -1,5 +1,4 @@  -- modified version from https://github.com/lewis6991/impatient.nvim -  local vim = vim  local api = vim.api  local uv = vim.loop @@ -7,66 +6,96 @@ local _loadfile = loadfile  local get_runtime = api.nvim__get_runtime  local fs_stat = uv.fs_stat  local mpack = vim.mpack +local loadlib = package.loadlib + +local std_cache = vim.fn.stdpath "cache" + +local sep +if jit.os == "Windows" then +  sep = "\\" +else +  sep = "/" +end + +local std_dirs = { +  ["<APPDIR>"] = os.getenv "APPDIR", +  ["<VIMRUNTIME>"] = os.getenv "VIMRUNTIME", +  ["<STD_DATA>"] = vim.fn.stdpath "data", +  ["<STD_CONFIG>"] = vim.fn.stdpath "config", +  ["<LVIM_BASE>"] = get_lvim_base_dir(), +  ["<LVIM_RUNTIME>"] = get_runtime_dir(), +  ["<LVIM_CONFIG>"] = get_config_dir(), +} -local appdir = os.getenv "APPDIR" +local function modpath_mangle(modpath) +  for name, dir in pairs(std_dirs) do +    modpath = modpath:gsub(dir, name) +  end +  return modpath +end -local M = { +local function modpath_unmangle(modpath) +  for name, dir in pairs(std_dirs) do +    modpath = modpath:gsub(name, dir) +  end +  return modpath +end + +-- Overridable by user +local default_config = { +  chunks = { +    enable = true, +    path = std_cache .. sep .. "luacache_chunks", +  }, +  modpaths = { +    enable = true, +    path = std_cache .. sep .. "luacache_modpaths", +  }, +} + +-- State used internally +local default_state = {    chunks = {      cache = {},      profile = nil,      dirty = false, -    path = vim.fn.stdpath "cache" .. "/luacache_chunks", +    get = function(self, path) +      return self.cache[modpath_mangle(path)] +    end, +    set = function(self, path, chunk) +      self.cache[modpath_mangle(path)] = chunk +    end,    },    modpaths = {      cache = {},      profile = nil,      dirty = false, -    path = vim.fn.stdpath "cache" .. "/luacache_modpaths", +    get = function(self, mod) +      if self.cache[mod] then +        return modpath_unmangle(self.cache[mod]) +      end +    end, +    set = function(self, mod, path) +      self.cache[mod] = modpath_mangle(path) +    end,    },    log = {},  } +---@diagnostic disable-next-line: undefined-field +local M = vim.tbl_deep_extend("keep", _G.__luacache_config or {}, default_config, default_state)  _G.__luacache = M -if not get_runtime then -  -- nvim 0.5 compat -  get_runtime = function(paths, all, _) -    local r = {} -    for _, path in ipairs(paths) do -      local found = api.nvim_get_runtime_file(path, all) -      for i = 1, #found do -        r[#r + 1] = found[i] -      end -    end -    return r -  end -end -  local function log(...)    M.log[#M.log + 1] = table.concat({ string.format(...) }, " ")  end -function M.print_log() +local function print_log()    for _, l in ipairs(M.log) do      print(l)    end  end -function M.enable_profile() -  local P = require "lvim.impatient.profile" - -  M.chunks.profile = {} -  M.modpaths.profile = {} - -  P.setup(M.modpaths.profile) - -  M.print_profile = function() -    P.print_profile(M) -  end - -  vim.cmd [[command! LuaCacheProfile lua _G.__luacache.print_profile()]] -end -  local function hash(modpath)    local stat = fs_stat(modpath)    if stat then @@ -75,20 +104,6 @@ local function hash(modpath)    error("Could not hash " .. modpath)  end -local function modpath_mangle(modpath) -  if appdir then -    modpath = modpath:gsub(appdir, "/$APPDIR") -  end -  return modpath -end - -local function modpath_unmangle(modpath) -  if appdir then -    modpath = modpath:gsub("/$APPDIR", appdir) -  end -  return modpath -end -  local function profile(m, entry, name, loader)    if m.profile then      local mp = m.profile @@ -107,18 +122,41 @@ local function mprofile(mod, name, loader)  end  local function cprofile(path, name, loader) +  if M.chunks.profile then +    path = modpath_mangle(path) +  end    profile(M.chunks, path, name, loader)  end -local function get_runtime_file(basename, paths) +function M.enable_profile() +  local P = require "lvim.impatient.profile" + +  M.chunks.profile = {} +  M.modpaths.profile = {} + +  loadlib = function(path, fun) +    cprofile(path, "load_start") +    local f, err = package.loadlib(path, fun) +    cprofile(path, "load_end", "standard") +    return f, err +  end + +  P.setup(M.modpaths.profile) + +  api.nvim_create_user_command("LuaCacheProfile", function() +    P.print_profile(M, std_dirs) +  end, {}) +end + +local function get_runtime_file_from_parent(basename, paths)    -- Look in the cache to see if we have already loaded a parent module.    -- If we have then try looking in the parents directory first. -  local parents = vim.split(basename, "/") +  local parents = vim.split(basename, sep)    for i = #parents, 1, -1 do -    local parent = table.concat(vim.list_slice(parents, 1, i), "/") -    local ppath = M.modpaths.cache[parent] +    local parent = table.concat(vim.list_slice(parents, 1, i), sep) +    local ppath = M.modpaths:get(parent)      if ppath then -      if ppath:sub(-9) == "/init.lua" then +      if ppath:sub(-9) == (sep .. "init.lua") then          ppath = ppath:sub(1, -10) -- a/b/init.lua -> a/b        else          ppath = ppath:sub(1, -5) -- a/b.lua -> a/b @@ -126,38 +164,71 @@ local function get_runtime_file(basename, paths)        for _, path in ipairs(paths) do          -- path should be of form 'a/b/c.lua' or 'a/b/c/init.lua' -        local modpath = ppath .. "/" .. path:sub(#("lua/" .. parent) + 2) +        local modpath = ppath .. sep .. path:sub(#("lua" .. sep .. parent) + 2)          if fs_stat(modpath) then            return modpath, "cache(p)"          end        end      end    end +end + +local rtp = vim.split(vim.o.rtp, ",") -  -- What Neovim does by default; slowest -  local modpath = get_runtime(paths, false, { is_lua = true })[1] -  return modpath, "standard" +-- Make sure modpath is in rtp and that modpath is in paths. +local function validate_modpath(modpath, paths) +  local match = false +  for _, p in ipairs(paths) do +    if vim.endswith(modpath, p) then +      match = true +      break +    end +  end +  if not match then +    return false +  end +  for _, dir in ipairs(rtp) do +    if vim.startswith(modpath, dir) then +      return fs_stat(modpath) ~= nil +    end +  end +  return false  end  local function get_runtime_file_cached(basename, paths) +  local modpath, loader    local mp = M.modpaths -  if mp.cache[basename] then -    local modpath = mp.cache[basename] -    if fs_stat(modpath) then -      mprofile(basename, "resolve_end", "cache") -      return modpath +  if mp.enable then +    local modpath_cached = mp:get(basename) +    if modpath_cached then +      modpath, loader = modpath_cached, "cache" +    else +      modpath, loader = get_runtime_file_from_parent(basename, paths) +    end + +    if modpath and not validate_modpath(modpath, paths) then +      modpath = nil + +      -- Invalidate +      mp.cache[basename] = nil +      mp.dirty = true      end -    mp.cache[basename] = nil -    mp.dirty = true    end -  local modpath, loader = get_runtime_file(basename, paths) +  if not modpath then +    -- What Neovim does by default; slowest +    modpath, loader = get_runtime(paths, false, { is_lua = true })[1], "standard" +  end +    if modpath then      mprofile(basename, "resolve_end", loader) -    log("Creating cache for module %s", basename) -    mp.cache[basename] = modpath_mangle(modpath) -    mp.dirty = true +    if mp.enable and loader ~= "cache" then +      log("Creating cache for module %s", basename) +      mp:set(basename, modpath) +      mp.dirty = true +    end    end +    return modpath  end @@ -168,8 +239,12 @@ local function extract_basename(pats)    for _, pat in ipairs(pats) do      for i, npat in ipairs {        -- Ordered by most specific -      "lua/(.*)/init%.lua", -      "lua/(.*)%.lua", +      "lua" +        .. sep +        .. "(.*)" +        .. sep +        .. "init%.lua", +      "lua" .. sep .. "(.*)%.lua",      } do        local m = pat:match(npat)        if i == 2 and m and m:sub(-4) == "init" then @@ -211,8 +286,8 @@ end  -- Copied from neovim/src/nvim/lua/vim.lua with two lines changed  local function load_package(name) -  local basename = name:gsub("%.", "/") -  local paths = { "lua/" .. basename .. ".lua", "lua/" .. basename .. "/init.lua" } +  local basename = name:gsub("%.", sep) +  local paths = { "lua" .. sep .. basename .. ".lua", "lua" .. sep .. basename .. sep .. "init.lua" }    -- Original line:    -- local found = vim.api.nvim__get_runtime(paths, false, {is_lua=true}) @@ -239,7 +314,7 @@ local function load_package(name)      -- So "foo-bar.baz" should result in "luaopen_bar_baz"      local dash = name:find("-", 1, true)      local modname = dash and name:sub(dash + 1) or name -    local f, err = package.loadlib(found[1], "luaopen_" .. modname:gsub("%.", "_")) +    local f, err = loadlib(found[1], "luaopen_" .. modname:gsub("%.", "_"))      return f or error(err)    end    return nil @@ -248,14 +323,16 @@ end  local function load_from_cache(path)    local mc = M.chunks -  if not mc.cache[path] then +  local cache = mc:get(path) + +  if not cache then      return nil, string.format("No cache for path %s", path)    end -  local mhash, codes = unpack(mc.cache[path]) +  local mhash, codes = unpack(cache) -  if mhash ~= hash(modpath_unmangle(path)) then -    mc.cache[path] = nil +  if mhash ~= hash(path) then +    mc:set(path)      mc.dirty = true      return nil, string.format("Stale cache for path %s", path)    end @@ -263,7 +340,7 @@ local function load_from_cache(path)    local chunk = loadstring(codes)    if not chunk then -    mc.cache[path] = nil +    mc:set(path)      mc.dirty = true      return nil, string.format("Cache error for path %s", path)    end @@ -274,19 +351,23 @@ end  local function loadfile_cached(path)    cprofile(path, "load_start") -  local chunk, err = load_from_cache(path) -  if chunk and not err then -    log("Loaded cache for path %s", path) -    cprofile(path, "load_end", "cache") -    return chunk +  local chunk, err + +  if M.chunks.enable then +    chunk, err = load_from_cache(path) +    if chunk and not err then +      log("Loaded cache for path %s", path) +      cprofile(path, "load_end", "cache") +      return chunk +    end +    log(err)    end -  log(err)    chunk, err = _loadfile(path) -  if not err then +  if not err and M.chunks.enable then      log("Creating cache for path %s", path) -    M.chunks.cache[modpath_mangle(path)] = { hash(path), string.dump(chunk) } +    M.chunks:set(path, { hash(path), string.dump(chunk) })      M.chunks.dirty = true    end @@ -296,9 +377,12 @@ end  function M.save_cache()    local function _save_cache(t) +    if not t.enable then +      return +    end      if t.dirty then        log("Updating chunk cache file: %s", t.path) -      local f = io.open(t.path, "w+b") +      local f = assert(io.open(t.path, "w+b"))        f:write(mpack.encode(t.cache))        f:flush()        t.dirty = false @@ -308,7 +392,7 @@ function M.save_cache()    _save_cache(M.modpaths)  end -function M.clear_cache() +local function clear_cache()    local function _clear_cache(t)      t.cache = {}      os.remove(t.path) @@ -319,9 +403,12 @@ end  local function init_cache()    local function _init_cache(t) +    if not t.enable then +      return +    end      if fs_stat(t.path) then        log("Loading cache file %s", t.path) -      local f = io.open(t.path, "rb") +      local f = assert(io.open(t.path, "rb"))        local ok        ok, t.cache = pcall(function()          return mpack.decode(f:read "*a") @@ -336,6 +423,10 @@ local function init_cache()      end    end +  if not uv.fs_stat(std_cache) then +    vim.fn.mkdir(std_cache, "p") +  end +    _init_cache(M.chunks)    _init_cache(M.modpaths)  end @@ -343,20 +434,42 @@ end  local function setup()    init_cache() +  -- Usual package loaders +  -- 1. package.preload +  -- 2. vim._load_package +  -- 3. package.path +  -- 4. package.cpath +  -- 5. all-in-one +    -- Override default functions +  for i, loader in ipairs(package.loaders) do +    if loader == vim._load_package then +      package.loaders[i] = load_package +      break +    end +  end    vim._load_package = load_package +    vim.api.nvim__get_runtime = get_runtime_cached -  -- luacheck: ignore 121    loadfile = loadfile_cached -  vim.cmd [[ -    augroup impatient -      autocmd VimEnter,VimLeave * lua _G.__luacache.save_cache() -    augroup END +  local augroup = api.nvim_create_augroup("impatient", {}) + +  api.nvim_create_user_command("LuaCacheClear", clear_cache, {}) +  api.nvim_create_user_command("LuaCacheLog", print_log, {}) + +  api.nvim_create_autocmd({ "VimEnter", "VimLeave" }, { +    group = augroup, +    callback = M.save_cache, +  }) -    command! LuaCacheClear lua _G.__luacache.clear_cache() -    command! LuaCacheLog   lua _G.__luacache.print_log() -  ]] +  api.nvim_create_autocmd("OptionSet", { +    group = augroup, +    pattern = "runtimepath", +    callback = function() +      rtp = vim.split(vim.o.rtp, ",") +    end, +  })  end  setup() diff --git a/lua/lvim/impatient/profile.lua b/lua/lvim/impatient/profile.lua index 0ab04d65..2eafbbf2 100644 --- a/lua/lvim/impatient/profile.lua +++ b/lua/lvim/impatient/profile.lua @@ -1,12 +1,13 @@  local M = {} -local api, uv = vim.api, vim.loop +local sep +if jit.os == "Windows" then +  sep = "\\" +else +  sep = "/" +end -local std_data = vim.fn.stdpath "data" -local std_config = vim.fn.stdpath "config" -local vimruntime = os.getenv "VIMRUNTIME" -local lvim_runtime = get_runtime_dir() -local lvim_config = get_config_dir() +local api, uv = vim.api, vim.loop  local function load_buffer(title, lines)    local bufnr = api.nvim_create_buf(false, false) @@ -19,19 +20,6 @@ local function load_buffer(title, lines)    api.nvim_set_current_buf(bufnr)  end -local function mod_path(path) -  if not path then -    return "?" -  end -  path = path:gsub(std_data .. "/site/pack/packer/", "<PACKER>/") -  path = path:gsub(std_data .. "/", "<STD_DATA>/") -  path = path:gsub(std_config .. "/", "<STD_CONFIG>/") -  path = path:gsub(vimruntime .. "/", "<VIMRUNTIME>/") -  path = path:gsub(lvim_runtime .. "/", "<LVIM_RUNTIME>/") -  path = path:gsub(lvim_config .. "/", "<LVIM_CONFIG>/") -  return path -end -  local function time_tostr(x)    if x == 0 then      return "?" @@ -51,7 +39,7 @@ local function mem_tostr(x)    return string.format("%1.1f%s", x, unit)  end -function M.print_profile(I) +function M.print_profile(I, std_dirs)    local mod_profile = I.modpaths.profile    local chunk_profile = I.chunks.profile @@ -67,42 +55,50 @@ function M.print_profile(I)    for path, m in pairs(chunk_profile) do      m.load = m.load_end - m.load_start      m.load = m.load / 1000000 -    m.path = mod_path(path) +    m.path = path or "?"    end    local module_content_width = 0 +  local unloaded = {} +    for module, m in pairs(mod_profile) do -    m.resolve = 0 -    if m.resolve_end then -      m.resolve = m.resolve_end - m.resolve_start -      m.resolve = m.resolve / 1000000 -    end +    local module_dot = module:gsub(sep, ".") +    m.module = module_dot -    m.module = module:gsub("/", ".") -    m.loader = m.loader or m.loader_guess +    if not package.loaded[module_dot] and not package.loaded[module] then +      unloaded[#unloaded + 1] = m +    else +      m.resolve = 0 +      if m.resolve_start and m.resolve_end then +        m.resolve = m.resolve_end - m.resolve_start +        m.resolve = m.resolve / 1000000 +      end -    local path = I.modpaths.cache[module] -    local path_prof = chunk_profile[path] -    m.path = mod_path(path) +      m.loader = m.loader or m.loader_guess -    if path_prof then -      chunk_profile[path] = nil -      m.load = path_prof.load -      m.ploader = path_prof.loader -    else -      m.load = 0 -      m.ploader = "NA" -    end +      local path = I.modpaths.cache[module] +      local path_prof = chunk_profile[path] +      m.path = path or "?" -    total_resolve = total_resolve + m.resolve -    total_load = total_load + m.load +      if path_prof then +        chunk_profile[path] = nil +        m.load = path_prof.load +        m.ploader = path_prof.loader +      else +        m.load = 0 +        m.ploader = "NA" +      end -    if #module > module_content_width then -      module_content_width = #module -    end +      total_resolve = total_resolve + m.resolve +      total_load = total_load + m.load + +      if #module > module_content_width then +        module_content_width = #module +      end -    modules[#modules + 1] = m +      modules[#modules + 1] = m +    end    end    table.sort(modules, function(a, b) @@ -181,6 +177,12 @@ function M.print_profile(I)    end    add "" +  add "Standard directories:" +  for alias, path in pairs(std_dirs) do +    add("  %-12s -> %s", alias, path) +  end +  add "" +    add("%s─%s┬%s─%s┐", tcwl, lcwl, tcwl, lcwl)    add(title1_fmt, "Resolve", "Load")    add("%s┬%s┼%s┬%s┼%s┬%s", tcwl, lcwl, tcwl, lcwl, mcwl, n) @@ -207,7 +209,17 @@ function M.print_profile(I)        add(f3, p.load, p.loader, p.path)      end      add("%s┴%s┴%s", tcwl, lcwl, n) +  end + +  if #unloaded > 0 then      add "" +    add(n) +    add "Modules which were unable to loaded" +    add(n) +    for _, p in ipairs(unloaded) do +      lines[#lines + 1] = p.module +    end +    add(n)    end    load_buffer("Impatient Profile Report", lines) @@ -216,13 +228,12 @@ end  M.setup = function(profile)    local _require = require -  -- luacheck: ignore 121    require = function(mod) -    local basename = mod:gsub("%.", "/") +    local basename = mod:gsub("%.", sep)      if not profile[basename] then        profile[basename] = {}        profile[basename].resolve_start = uv.hrtime() -      profile[basename].loader_guess = "C" +      profile[basename].loader_guess = ""      end      return _require(mod)    end @@ -232,7 +243,7 @@ M.setup = function(profile)    for i = 1, #pl do      local l = pl[i]      pl[i] = function(mod) -      local basename = mod:gsub("%.", "/") +      local basename = mod:gsub("%.", sep)        profile[basename].loader_guess = i == 1 and "preloader" or "loader #" .. i        return l(mod)      end diff --git a/lua/lvim/utils/hooks.lua b/lua/lvim/utils/hooks.lua index ce4335a9..bf0dac60 100644 --- a/lua/lvim/utils/hooks.lua +++ b/lua/lvim/utils/hooks.lua @@ -34,10 +34,7 @@ end  ---It also forces regenerating any template ftplugin files  ---Tip: Useful for clearing any outdated settings  function M.reset_cache() -  local impatient = _G.__luacache -  if impatient then -    impatient.clear_cache() -  end +  vim.cmd [[LuaCacheClear]]    local lvim_modules = {}    for module, _ in pairs(package.loaded) do      if module:match "lvim.core" or module:match "lvim.lsp" then | 
