feat: separate bootstrap logic to launch faster (#250)

* chore: track setup() time

* feat: avoid setup() to be called twice

* chore: track time between Database:start()

* feat: add bootstrap option to load DB in advance

* feat: initialize DB before frecency class starts

* chore: add more logging

* feat!: load DB in Neovim starting

only if the plugin is loaded non-lazily.

* fix: simplify logic for timer

* fix: detect error and safely finish

* chore: remove unnecessary method
This commit is contained in:
JINNOUCHI Yasushi 2024-08-31 15:02:15 +09:00 committed by GitHub
parent 38f2a2207e
commit a6482c2fbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 68 additions and 33 deletions

View File

@ -4,6 +4,7 @@ local os_util = require "frecency.os_util"
---@class FrecencyOpts ---@class FrecencyOpts
---@field recency_values? { age: integer, value: integer }[] default: see lua/frecency/config.lua ---@field recency_values? { age: integer, value: integer }[] default: see lua/frecency/config.lua
---@field auto_validate? boolean default: true ---@field auto_validate? boolean default: true
---@field bootstrap? boolean default: true
---@field db_root? string default: vim.fn.stdpath "state" ---@field db_root? string default: vim.fn.stdpath "state"
---@field db_safe_mode? boolean default: true ---@field db_safe_mode? boolean default: true
---@field db_validate_threshold? integer default: 10 ---@field db_validate_threshold? integer default: 10
@ -33,6 +34,7 @@ local Config = {}
---@class FrecencyRawConfig ---@class FrecencyRawConfig
---@field recency_values { age: integer, value: integer }[] default: see lua/frecency/config.lua ---@field recency_values { age: integer, value: integer }[] default: see lua/frecency/config.lua
---@field auto_validate boolean default: true ---@field auto_validate boolean default: true
---@field bootstrap boolean default: true
---@field db_root string default: vim.fn.stdpath "state" ---@field db_root string default: vim.fn.stdpath "state"
---@field db_safe_mode boolean default: true ---@field db_safe_mode boolean default: true
---@field db_validate_threshold integer default: 10 ---@field db_validate_threshold integer default: 10
@ -59,6 +61,7 @@ Config.new = function()
local keys = { local keys = {
recency_values = true, recency_values = true,
auto_validate = true, auto_validate = true,
bootstrap = true,
db_root = true, db_root = true,
db_safe_mode = true, db_safe_mode = true,
db_validate_threshold = true, db_validate_threshold = true,
@ -98,6 +101,7 @@ end
---@type FrecencyRawConfig ---@type FrecencyRawConfig
Config.default_values = { Config.default_values = {
auto_validate = true, auto_validate = true,
bootstrap = true,
db_root = vim.fn.stdpath "state" --[[@as string]], db_root = vim.fn.stdpath "state" --[[@as string]],
db_safe_mode = true, db_safe_mode = true,
db_validate_threshold = 10, db_validate_threshold = 10,
@ -158,6 +162,7 @@ Config.setup = function(ext_config)
vim.validate { vim.validate {
recency_values = { opts.recency_values, "t" }, recency_values = { opts.recency_values, "t" },
auto_validate = { opts.auto_validate, "b" }, auto_validate = { opts.auto_validate, "b" },
bootstrap = { opts.bootstrap, "b" },
db_root = { opts.db_root, "s" }, db_root = { opts.db_root, "s" },
db_safe_mode = { opts.db_safe_mode, "b" }, db_safe_mode = { opts.db_safe_mode, "b" },
db_validate_threshold = { opts.db_validate_threshold, "n" }, db_validate_threshold = { opts.db_validate_threshold, "n" },

View File

@ -22,6 +22,7 @@ local Path = lazy_require "plenary.path" --[[@as FrecencyPlenaryPath]]
---@field private _file_lock FrecencyFileLock ---@field private _file_lock FrecencyFileLock
---@field private file_lock_rx async fun(): ... ---@field private file_lock_rx async fun(): ...
---@field private file_lock_tx fun(...): nil ---@field private file_lock_tx fun(...): nil
---@field private is_started boolean
---@field private tbl FrecencyDatabaseTable ---@field private tbl FrecencyDatabaseTable
---@field private version FrecencyDatabaseVersion ---@field private version FrecencyDatabaseVersion
---@field private watcher_rx FrecencyPlenaryAsyncControlChannelRx ---@field private watcher_rx FrecencyPlenaryAsyncControlChannelRx
@ -36,6 +37,7 @@ Database.new = function()
return setmetatable({ return setmetatable({
file_lock_rx = file_lock_rx, file_lock_rx = file_lock_rx,
file_lock_tx = file_lock_tx, file_lock_tx = file_lock_tx,
is_started = false,
tbl = Table.new(version), tbl = Table.new(version),
version = version, version = version,
watcher_rx = watcher_rx, watcher_rx = watcher_rx,
@ -74,6 +76,11 @@ end
---@async ---@async
---@return nil ---@return nil
function Database:start() function Database:start()
timer.track "Database:start() start"
if self.is_started then
return
end
self.is_started = true
local target = self:filename() local target = self:filename()
self.file_lock_tx(FileLock.new(target)) self.file_lock_tx(FileLock.new(target))
self.watcher_tx.send "load" self.watcher_tx.send "load"
@ -94,6 +101,7 @@ function Database:start()
log.debug("DB coroutine end:", mode) log.debug("DB coroutine end:", mode)
end end
end)() end)()
timer.track "Database:start() finish"
end end
---@async ---@async

View File

@ -1,7 +1,11 @@
---@type FrecencyDatabase?
local database
---This object is intended to be used as a singleton, and is lazily loaded. ---This object is intended to be used as a singleton, and is lazily loaded.
---When methods are called at the first time, it calls the constructor and ---When methods are called at the first time, it calls the constructor and
---setup() to be initialized. ---setup() to be initialized.
---@class FrecencyInstance ---@class FrecencyInstance
---@field bootstrap async fun(): nil
---@field complete fun(findstart: 1|0, base: string): integer|''|string[] ---@field complete fun(findstart: 1|0, base: string): integer|''|string[]
---@field delete async fun(path: string): nil ---@field delete async fun(path: string): nil
---@field query fun(opts?: FrecencyQueryOpts): FrecencyQueryEntry[]|string[] ---@field query fun(opts?: FrecencyQueryOpts): FrecencyQueryEntry[]|string[]
@ -20,7 +24,7 @@ local frecency = setmetatable({}, {
return function(...) return function(...)
if not instance() then if not instance() then
rawset(self, "instance", require("frecency.klass").new()) rawset(self, "instance", require("frecency.klass").new(database))
local is_async = key == "delete" or key == "validate_database" or key == "register" local is_async = key == "delete" or key == "validate_database" or key == "register"
instance():setup(is_async) instance():setup(is_async)
end end
@ -45,8 +49,9 @@ local function setup(ext_config)
end end
local config = require "frecency.config" local config = require "frecency.config"
config.setup(ext_config) config.setup(ext_config)
local timer = require "frecency.timer"
timer.track "setup() start"
vim.api.nvim_set_hl(0, "TelescopeBufferLoaded", { link = "String", default = true }) vim.api.nvim_set_hl(0, "TelescopeBufferLoaded", { link = "String", default = true })
vim.api.nvim_set_hl(0, "TelescopePathSeparator", { link = "Directory", default = true }) vim.api.nvim_set_hl(0, "TelescopePathSeparator", { link = "Directory", default = true })
@ -86,7 +91,15 @@ local function setup(ext_config)
end, end,
}) })
if config.bootstrap and vim.v.vim_did_enter == 0 then
database = require("frecency.database").new()
async_call(function()
database:start()
end)
end
setup_done = true setup_done = true
timer.track "setup() finish"
end end
return { return {

View File

@ -9,16 +9,25 @@ local wait = require "frecency.wait"
local lazy_require = require "frecency.lazy_require" local lazy_require = require "frecency.lazy_require"
local async = lazy_require "plenary.async" --[[@as FrecencyPlenaryAsync]] local async = lazy_require "plenary.async" --[[@as FrecencyPlenaryAsync]]
---@enum FrecencyStatus
local STATUS = {
NEW = 0,
SETUP_CALLED = 1,
SETUP_FINISHED = 2,
}
---@class Frecency ---@class Frecency
---@field private buf_registered table<integer, boolean> flag to indicate the buffer is registered to the database. ---@field private buf_registered table<integer, boolean> flag to indicate the buffer is registered to the database.
---@field private database FrecencyDatabase ---@field private database FrecencyDatabase
---@field private picker FrecencyPicker ---@field private picker FrecencyPicker
---@field private status FrecencyStatus
local Frecency = {} local Frecency = {}
---@param database? FrecencyDatabase
---@return Frecency ---@return Frecency
Frecency.new = function() Frecency.new = function(database)
local self = setmetatable({ buf_registered = {} }, { __index = Frecency }) --[[@as Frecency]] local self = setmetatable({ buf_registered = {}, status = STATUS.NEW }, { __index = Frecency }) --[[@as Frecency]]
self.database = Database.new() self.database = database or Database.new()
return self return self
end end
@ -26,22 +35,35 @@ end
---@param is_async boolean ---@param is_async boolean
---@return nil ---@return nil
function Frecency:setup(is_async) function Frecency:setup(is_async)
if self.status >= STATUS.SETUP_CALLED then
return
end
self.status = STATUS.SETUP_CALLED
timer.track "frecency.setup() start"
---@async ---@async
local function init() local function init()
timer.track "init() start"
self.database:start() self.database:start()
self:assert_db_entries() self:assert_db_entries()
if config.auto_validate then if config.auto_validate then
self:validate_database() self:validate_database()
end end
timer.track "init() finish" timer.track "frecency.setup() finish"
self.status = STATUS.SETUP_FINISHED
end end
if is_async then if is_async then
init() init()
else return
wait(init)
end end
local ok, status = wait(init)
if ok then
return
end
-- NOTE: This means init() has failed. Try again.
self.status = STATUS.NEW
self:error(status == -1 and "init() never returns during the time" or "init() is interrupted during the time")
end end
---This can be calledBy `require("telescope").extensions.frecency.frecency`. ---This can be calledBy `require("telescope").extensions.frecency.frecency`.
@ -79,8 +101,11 @@ end
---@param force? boolean ---@param force? boolean
---@return nil ---@return nil
function Frecency:validate_database(force) function Frecency:validate_database(force)
timer.track "validate_database() start"
local unlinked = self.database:unlinked_entries() local unlinked = self.database:unlinked_entries()
timer.track "validate_database() calculate unlinked"
if #unlinked == 0 or (not force and #unlinked < config.db_validate_threshold) then if #unlinked == 0 or (not force and #unlinked < config.db_validate_threshold) then
timer.track "validate_database() finish: no unlinked"
return return
end end
local function remove_entries() local function remove_entries()
@ -89,6 +114,7 @@ function Frecency:validate_database(force)
end end
if not config.db_safe_mode then if not config.db_safe_mode then
remove_entries() remove_entries()
timer.track "validate_database() finish: removed"
return return
end end
-- HACK: This is needed because the default implementaion of vim.ui.select() -- HACK: This is needed because the default implementaion of vim.ui.select()

View File

@ -18,21 +18,11 @@ function M.track(event)
end end
if M.has_lazy then if M.has_lazy then
local stats = require "lazy.stats" local stats = require "lazy.stats"
---@param n integer local function make_key(num)
---@return string local key = num and ("[telescope-frecency] %s: %d"):format(event, num) or "[telescope-frecency] " .. event
local function make_key(n) return stats._stats.times[key] and make_key((num or 1) + 1) or key
return ("[telescope-frecency] %s: %d"):format(event, n)
end end
local key stats.track(make_key())
local num = 0
while true do
key = make_key(num)
if not stats._stats.times[key] then
break
end
num = num + 1
end
stats.track(key)
end end
end end

View File

@ -40,15 +40,8 @@ end
---@param f FrecencyWaitCallback ---@param f FrecencyWaitCallback
---@param opts FrecencyWaitConfig? ---@param opts FrecencyWaitConfig?
---@return nil ---@return boolean ok
---@return nil|-1|-2 status
return function(f, opts) return function(f, opts)
local wait = Wait.new(f, opts) return Wait.new(f, opts):run()
local ok, status = wait:run()
if ok then
return
elseif status == -1 then
error "callback never returnes during the time"
elseif status == -2 then
error "callback is interrupted during the time"
end
end end