perf(picker): gc optims

This commit is contained in:
Folke Lemaitre 2025-01-21 16:36:11 +01:00
parent c1e2c619b2
commit 3fa2ea3115
No known key found for this signature in database
GPG key ID: 41F8B1FBACAE2040
8 changed files with 76 additions and 29 deletions

View file

@ -87,7 +87,7 @@ local M = {}
---@field confirm? snacks.picker.Action.spec shortcut for confirm action ---@field confirm? snacks.picker.Action.spec shortcut for confirm action
---@field auto_confirm? boolean automatically confirm if there is only one item ---@field auto_confirm? boolean automatically confirm if there is only one item
---@field main? snacks.picker.main.Config main editor window config ---@field main? snacks.picker.main.Config main editor window config
---@field on_change? fun(picker:snacks.Picker, item:snacks.picker.Item) called when the cursor changes ---@field on_change? fun(picker:snacks.Picker, item?:snacks.picker.Item) called when the cursor changes
---@field on_show? fun(picker:snacks.Picker) called when the picker is shown ---@field on_show? fun(picker:snacks.Picker) called when the picker is shown
---@field jump? snacks.picker.jump.Config|{} ---@field jump? snacks.picker.jump.Config|{}
--- Other --- Other

View file

@ -34,6 +34,14 @@ function M:count()
return #self.items return #self.items
end end
function M:close()
self.task:abort()
self.task = Async.nop()
self._find = function()
return {}
end
end
---@param search string ---@param search string
function M:changed(search) function M:changed(search)
search = vim.trim(search) search = vim.trim(search)

View file

@ -23,11 +23,11 @@ function M.new(picker)
height = 1, height = 1,
text = picker.opts.live and self.filter.search or self.filter.pattern, text = picker.opts.live and self.filter.search or self.filter.pattern,
ft = "regex", ft = "regex",
on_win = function()
vim.fn.prompt_setprompt(self.win.buf, "")
self.win:focus()
vim.cmd.startinsert() vim.cmd.startinsert()
vim.api.nvim_win_set_cursor(self.win.win, { 1, #self:get() + 1 }) vim.api.nvim_win_set_cursor(self.win.win, { 1, #self:get() + 1 })
on_win = function(win)
vim.fn.prompt_setprompt(win.buf, "")
win:focus()
end, end,
bo = { bo = {
filetype = "snacks_picker_input", filetype = "snacks_picker_input",
@ -40,29 +40,29 @@ function M.new(picker)
}, },
})) }))
local ref = picker:ref() local ref = Snacks.util.ref(self)
self.win:on( self.win:on(
{ "TextChangedI", "TextChanged" }, { "TextChangedI", "TextChanged" },
Snacks.util.throttle(function() Snacks.util.throttle(function()
local p = ref() local input = ref()
if not p or not self.win:valid() then if not input or not input.win:valid() then
return return
end end
-- only one line -- only one line
-- Can happen when someone pastes a multiline string -- Can happen when someone pastes a multiline string
if vim.api.nvim_buf_line_count(self.win.buf) > 1 then if vim.api.nvim_buf_line_count(input.win.buf) > 1 then
local line = vim.trim(self.win:text():gsub("\n", " ")) local line = vim.trim(input.win:text():gsub("\n", " "))
vim.api.nvim_buf_set_lines(self.win.buf, 0, -1, false, { line }) vim.api.nvim_buf_set_lines(input.win.buf, 0, -1, false, { line })
vim.api.nvim_win_set_cursor(self.win.win, { 1, #line + 1 }) vim.api.nvim_win_set_cursor(input.win.win, { 1, #line + 1 })
end end
vim.bo[self.win.buf].modified = false vim.bo[input.win.buf].modified = false
local pattern = self:get() local pattern = input:get()
if p.opts.live then if input.picker.opts.live then
self.filter.search = pattern input.filter.search = pattern
else else
self.filter.pattern = pattern input.filter.pattern = pattern
end end
p:match() input.picker:match()
end, { ms = picker.opts.live and 100 or 30 }), end, { ms = picker.opts.live and 100 or 30 }),
{ buf = true } { buf = true }
) )

View file

@ -132,7 +132,9 @@ function M:count()
end end
function M:close() function M:close()
-- nothing todo. Keep all items so actions can be performed on them, self.win:destroy()
self.picker = nil
-- Keep all items so actions can be performed on them,
-- even when the picker closed -- even when the picker closed
end end

View file

@ -59,6 +59,11 @@ function M:abort()
self.task:abort() self.task:abort()
end end
function M:close()
self:abort()
self.task = Async.nop()
end
---@param picker snacks.Picker ---@param picker snacks.Picker
---@param opts? {prios?: snacks.picker.Item[]} ---@param opts? {prios?: snacks.picker.Item[]}
function M:run(picker, opts) function M:run(picker, opts)

View file

@ -3,10 +3,12 @@ local Finder = require("snacks.picker.core.finder")
local uv = vim.uv or vim.loop local uv = vim.uv or vim.loop
Async.BUDGET = 10 Async.BUDGET = 10
local _id = 0
---@alias snacks.Picker.ref (fun():snacks.Picker?)|{value?: snacks.Picker} ---@alias snacks.Picker.ref (fun():snacks.Picker?)|{value?: snacks.Picker}
---@class snacks.Picker ---@class snacks.Picker
---@field id number
---@field opts snacks.picker.Config ---@field opts snacks.picker.Config
---@field finder snacks.picker.Finder ---@field finder snacks.picker.Finder
---@field format snacks.picker.format ---@field format snacks.picker.format
@ -38,7 +40,7 @@ M._active = {}
---@class snacks.picker.Last ---@class snacks.picker.Last
---@field cursor number ---@field cursor number
---@field topline number ---@field topline number
---@field opts snacks.picker.Config ---@field opts? snacks.picker.Config
---@field selected snacks.picker.Item[] ---@field selected snacks.picker.Item[]
---@field filter snacks.picker.Filter ---@field filter snacks.picker.Filter
@ -51,6 +53,8 @@ M.last = nil
---@param opts? snacks.picker.Config ---@param opts? snacks.picker.Config
function M.new(opts) function M.new(opts)
local self = setmetatable({}, M) local self = setmetatable({}, M)
_id = _id + 1
self.id = _id
self.opts = Snacks.picker.config.get(opts) self.opts = Snacks.picker.config.get(opts)
if self.opts.source == "resume" then if self.opts.source == "resume" then
return M.resume() return M.resume()
@ -70,19 +74,23 @@ function M.new(opts)
if picker_count > 0 then if picker_count > 0 then
-- clear items from previous pickers for garbage collection -- clear items from previous pickers for garbage collection
for picker, _ in pairs(M._pickers) do for picker, _ in pairs(M._pickers) do
picker.list.items = {}
picker.list.topk:clear()
picker.finder.items = {} picker.finder.items = {}
picker.list.items = {}
picker.list:clear()
picker.list.picker = nil picker.list.picker = nil
end end
end end
if self.opts.debug.leaks then if self.opts.debug.leaks and picker_count > 0 then
collectgarbage("collect") collectgarbage("collect")
picker_count = vim.tbl_count(M._pickers) picker_count = vim.tbl_count(M._pickers)
if picker_count > 0 then if picker_count > 0 then
local pickers = vim.tbl_keys(M._pickers) ---@type snacks.Picker[]
table.sort(pickers, function(a, b)
return a.id < b.id
end)
local lines = { ("# ` %d ` active pickers:"):format(picker_count) } local lines = { ("# ` %d ` active pickers:"):format(picker_count) }
for picker, _ in pairs(M._pickers) do for _, picker in ipairs(pickers) do
lines[#lines + 1] = ("- [%s]: **pattern**=%q, **search**=%q"):format( lines[#lines + 1] = ("- [%s]: **pattern**=%q, **search**=%q"):format(
picker.opts.source or "custom", picker.opts.source or "custom",
picker.input.filter.pattern, picker.input.filter.pattern,
@ -92,7 +100,10 @@ function M.new(opts)
Snacks.notify.error(lines, { title = "Snacks Picker", id = "snacks_picker_leaks" }) Snacks.notify.error(lines, { title = "Snacks Picker", id = "snacks_picker_leaks" })
Snacks.debug.metrics() Snacks.debug.metrics()
else else
Snacks.notifier.hide("snacks_picker_leaks") Snacks.notify(
"Picker leaks cleared after `collectgarbage`",
{ title = "Snacks Picker", id = "snacks_picker_leaks" }
)
end end
end end
@ -133,7 +144,7 @@ function M.new(opts)
self.preview = require("snacks.picker.core.preview").new(self.opts, layout.preview == "main" and self.main or nil) self.preview = require("snacks.picker.core.preview").new(self.opts, layout.preview == "main" and self.main or nil)
M.last = { M.last = {
opts = self.opts, opts = opts,
selected = {}, selected = {},
cursor = self.list.cursor, cursor = self.list.cursor,
filter = self.input.filter, filter = self.input.filter,
@ -457,6 +468,15 @@ function M:close()
if self.closed then if self.closed then
return return
end end
-- FIXME: lsp definitions picker can't be gc-ed without the below,
-- which makes no sense. Need to further investigate.
-- if not self.shown then
-- self.input.win.opts.relative = "editor"
-- self.input.win:show()
-- end
self:hist_record(true) self:hist_record(true)
self.closed = true self.closed = true
M.last.selected = self:selected({ fallback = false }) M.last.selected = self:selected({ fallback = false })
@ -468,15 +488,22 @@ function M:close()
if is_picker_win and vim.api.nvim_win_is_valid(self.main) then if is_picker_win and vim.api.nvim_win_is_valid(self.main) then
vim.api.nvim_set_current_win(self.main) vim.api.nvim_set_current_win(self.main)
end end
self.layout:close()
self.updater:stop() self.updater:stop()
self.finder:abort() self.finder:abort()
self.matcher:abort() self.matcher:abort()
M._active[self] = nil M._active[self] = nil
vim.schedule(function() vim.schedule(function()
-- order matters! self.finder:close()
self.matcher:close()
self.layout:close()
self.list:close()
self.input:close() self.input:close()
self.preview:close() self.preview:close()
self.resolved_layout = nil
self.preview = nil
self.matcher = nil
self.updater = nil
self.history = nil
end) end)
end end
@ -487,12 +514,12 @@ end
---@private ---@private
function M:progress(ms) function M:progress(ms)
if self.updater:is_active() then if self.updater:is_active() or self.closed then
return return
end end
self.updater = vim.defer_fn(function() self.updater = vim.defer_fn(function()
self:update() self:update()
if self:is_active() then if not self.closed and self:is_active() then
-- slower progress when we filled topk -- slower progress when we filled topk
local topk, height = self.list.topk:count(), self.list.state.height or 50 local topk, height = self.list.topk:count(), self.list.state.height or 50
self:progress(topk > height and 30 or 10) self:progress(topk > height and 30 or 10)

View file

@ -140,6 +140,8 @@ function M.request(buf, method, params, cb)
end) end)
async:suspend() async:suspend()
cancel = {}
async = Async.nop()
end end
-- Support for older versions of neovim -- Support for older versions of neovim

View file

@ -489,6 +489,9 @@ end
function M:destroy() function M:destroy()
self:close() self:close()
self.events = {} self.events = {}
self.keys = {}
self.meta = {}
self.opts = {}
end end
---@param opts? { buf: boolean } ---@param opts? { buf: boolean }