summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorYi Ming <ofseed@foxmail.com>2026-04-22 23:38:58 +0800
committerGitHub <noreply@github.com>2026-04-22 11:38:58 -0400
commit558204d87bd4ff3a7be347e4c765c4b9b8f28c52 (patch)
tree9708e14872f4f32fddf8e61c7f8c2f2615e3e073
parent61fb88992d34d16b3058dcd8b1664f575449d964 (diff)
perf(lsp): clear table by table.clear() #39222
benchmark: https://gist.github.com/ofseed/6224529d77c016c36f7ab2f977059848 local rounds = tonumber(arg[1]) or 1000 local count = tonumber(arg[2]) or 1000 -- Load the table.clear function. local clear = require("table.clear") local function fill(t, n) for i = 1, n do t[i] = i end end local function bench_reassign(n_rounds, n_items) local t = {} local start = os.clock() for _ = 1, n_rounds do t = {} collectgarbage("collect") fill(t, n_items) end return os.clock() - start end local function bench_reassign_no_gc(n_rounds, n_items) local t = {} local start = os.clock() for _ = 1, n_rounds do t = {} fill(t, n_items) end return os.clock() - start end local function bench_clear(n_rounds, n_items) local t = {} local start = os.clock() for _ = 1, n_rounds do clear(t) fill(t, n_items) end return os.clock() - start end -- Warm up LuaJIT before the real benchmark. do local t = {} for _ = 1, 2000 do clear(t) fill(t, count) end end collectgarbage("collect") local reassign_time = bench_reassign(rounds, count) collectgarbage("collect") local reassign_no_gc_time = bench_reassign_no_gc(rounds, count) collectgarbage("collect") local clear_time = bench_clear(rounds, count) print(string.format("rounds=%d count=%d", rounds, count)) print(string.format("t = {} + GC : %.6f s", reassign_time)) print(string.format("t = {} : %.6f s", reassign_no_gc_time)) print(string.format("table.clear : %.6f s", clear_time)) print(string.format("vs + GC : %.2fx", reassign_time / clear_time)) print(string.format("vs no GC : %.2fx", reassign_no_gc_time / clear_time)) benchmark result: rounds=1000 count=1000 t = {} + GC : 0.022469 s t = {} : 0.002570 s table.clear : 0.000387 s vs + GC : 58.06x vs no GC : 6.64x `count` is how many items the table has, and `round` is how many rounds we fill the table, clear, and then refill it. `table = {}` is clear the table by resigning a new empty one, because this script does not run persistently like nvim so GC is not triggered, so I added another extreme control group that manually triggers GC.
-rw-r--r--runtime/doc/news.txt2
-rw-r--r--runtime/lua/vim/_core/table.lua27
-rw-r--r--runtime/lua/vim/lsp/_folding_range.lua33
-rw-r--r--runtime/lua/vim/lsp/codelens.lua13
-rw-r--r--test/functional/core/main_spec.lua1
5 files changed, 53 insertions, 23 deletions
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
index 9f78179c61..ef6adb92d2 100644
--- a/runtime/doc/news.txt
+++ b/runtime/doc/news.txt
@@ -168,6 +168,8 @@ PERFORMANCE
functions, which skips the Vimscript <=> Lua "bridge" (no data
conversion/marshalling) entirely, if the `vim.fn` function is called from
Lua.
+• The table holding LSP data is now cleared using `table.clear`,
+ thus reducing GC and memory reallocation during each data reset.
PLUGINS
diff --git a/runtime/lua/vim/_core/table.lua b/runtime/lua/vim/_core/table.lua
new file mode 100644
index 0000000000..58833b1c8d
--- /dev/null
+++ b/runtime/lua/vim/_core/table.lua
@@ -0,0 +1,27 @@
+-- Basic shim for LuaJIT's table.new and table.clear.
+local has_new, new = pcall(require, 'table.new')
+local has_clear, clear = pcall(require, 'table.clear')
+
+local M = {}
+
+if not has_new then
+ ---@diagnostic disable-next-line: unused-local
+ new = function(narr, nrec)
+ return {}
+ end
+end
+
+if not has_clear then
+ clear = function(tab)
+ ---@diagnostic disable-next-line: no-unknown
+ for k in pairs(tab) do
+ ---@diagnostic disable-next-line: no-unknown
+ tab[k] = nil
+ end
+ end
+end
+
+M.new = new
+M.clear = clear
+
+return M
diff --git a/runtime/lua/vim/lsp/_folding_range.lua b/runtime/lua/vim/lsp/_folding_range.lua
index e7ceee95db..b35fdfb1ea 100644
--- a/runtime/lua/vim/lsp/_folding_range.lua
+++ b/runtime/lua/vim/lsp/_folding_range.lua
@@ -1,5 +1,6 @@
local util = require('vim.lsp.util')
local log = require('vim.lsp.log')
+local tableclear = require('vim._core.table').clear
local api = vim.api
---@type table<lsp.FoldingRangeKind, true>
@@ -51,12 +52,13 @@ Capability.all[State.name] = State
--- Re-evaluate the cached foldinfo in the buffer.
function State:evaluate()
- ---@type table<integer, [integer, ">" | "<"?]?>
- local row_level = {}
- ---@type table<integer, table<lsp.FoldingRangeKind, true?>?>>
- local row_kinds = {}
- ---@type table<integer, string?>
- local row_text = {}
+ local row_level, row_kinds, row_text, row_virt_text =
+ self.row_level, self.row_kinds, self.row_text, self.row_virt_text
+
+ tableclear(row_level)
+ tableclear(row_kinds)
+ tableclear(row_text)
+ tableclear(row_virt_text)
for client_id, ranges in pairs(self.client_state) do
for _, range in ipairs(ranges) do
@@ -88,11 +90,6 @@ function State:evaluate()
end
end
end
-
- self.row_level = row_level
- self.row_kinds = row_kinds
- self.row_text = row_text
- self.row_virt_text = {}
end
--- Force `foldexpr()` to be re-evaluated, without opening folds.
@@ -190,10 +187,10 @@ end
function State:reset()
self.lang = vim.treesitter.language.get_lang(vim.bo[self.bufnr].filetype)
- self.row_level = {}
- self.row_kinds = {}
- self.row_text = {}
- self.row_virt_text = {}
+ tableclear(self.row_level)
+ tableclear(self.row_kinds)
+ tableclear(self.row_text)
+ tableclear(self.row_virt_text)
end
--- Initialize `state` and event hooks, then request folding ranges.
@@ -201,7 +198,11 @@ end
---@return vim.lsp.folding_range.State
function State:new(bufnr)
self = Capability.new(self, bufnr)
- self:reset()
+ self.lang = vim.treesitter.language.get_lang(vim.bo[self.bufnr].filetype)
+ self.row_level = {}
+ self.row_kinds = {}
+ self.row_text = {}
+ self.row_virt_text = {}
api.nvim_buf_attach(bufnr, false, {
-- Reset `bufstate` and request folding ranges.
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
index 07212e6554..1846d0f752 100644
--- a/runtime/lua/vim/lsp/codelens.lua
+++ b/runtime/lua/vim/lsp/codelens.lua
@@ -1,5 +1,6 @@
local util = require('vim.lsp.util')
local log = require('vim.lsp.log')
+local tableclear = require('vim._core.table').clear
local api = vim.api
local M = {}
@@ -99,10 +100,10 @@ end
function Provider:clear()
self:reset_timer()
self.version = nil
- self.row_version = {}
+ tableclear(self.row_version)
for _, state in pairs(self.client_state) do
- state.row_lenses = {}
+ tableclear(state.row_lenses)
api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
end
@@ -130,8 +131,8 @@ function Provider:handler(err, result, ctx)
return
end
- ---@type table<integer, lsp.CodeLens[]>
- local row_lenses = {}
+ local row_lenses = state.row_lenses
+ tableclear(row_lenses)
-- Code lenses should only span a single line.
for _, lens in ipairs(result or {}) do
@@ -140,8 +141,6 @@ function Provider:handler(err, result, ctx)
table.insert(lenses, lens)
row_lenses[row] = lenses
end
-
- state.row_lenses = row_lenses
self.version = ctx.version
api.nvim__redraw({ buf = self.bufnr, valid = true, flush = false })
@@ -516,7 +515,7 @@ function M.on_refresh(err, _, ctx)
if not provider.timer then
provider:request(client_id, function()
if api.nvim_buf_is_valid(bufnr) then
- provider.row_version = {}
+ tableclear(provider.row_version)
vim.api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
end
end)
diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua
index e95fe80343..b800aa5bf8 100644
--- a/test/functional/core/main_spec.lua
+++ b/test/functional/core/main_spec.lua
@@ -233,6 +233,7 @@ describe('vim._core', function()
'vim._core.shared',
'vim._core.stringbuffer',
'vim._core.system',
+ 'vim._core.table',
'vim._core.ui2',
'vim._core.util',
'vim._core.vimfn',