perf: improve for source providing huge list of items (#1980)

* perf: avoid creating closure in cache.ensure and drop some cached getters

This mainly addresses the perf issue on large amount of calls to
`entry.new`. Previously every `cache.ensure` calls in the code path of
it creates an anonymous function, and it seems that luajit just could
not inline it. Function creation is not expensive in luajit, but that
overhead is noticeable if every `cache.ensure` call creates a function.
The first improvemnt is to solidate the cache callback and attach it to
the metatable of `entry`. This ensures that every created entry instance
share the same cache callback and no new functions will be frequently created,
reduces the ram usage and GC overhead.

To improve it further, some frequently accessed fields of entry like
`completion_item` and `offset` is refactored to use simple table access
instead of getter pattern. The current cached getter is implemented
using `cache.ensure`, which introduces two more levels of function calls
on each access: `cache.key` and `cache.get`. The overhead is okay if but
noticeable if entries amount is quite large: you need to call 4 functions on
a simple `completion_item` field access for each item.

All of the changes done in the commit is just constant time
optimization. But the different is huge if tested with LS providing
large amount of entries like tailwindcss.

* perf: delay fuzzy match on displayed vim item

`entry.get_vim_item` is a very heavy call, especially when user do
complex stuff on item formatting. Delay its call to window displaying to
let `performance.max_view_entries` applied to it.

* remove unneeded fill_defaults

* update gha

---------

Co-authored-by: hrsh7th <629908+hrsh7th@users.noreply.github.com>
This commit is contained in:
yioneko
2024-10-20 12:28:20 +08:00
committed by GitHub
parent ae644feb7b
commit 1a1d7ecb73
12 changed files with 468 additions and 408 deletions

View File

@@ -24,6 +24,9 @@ local char = require('cmp.utils.char')
---@field public completion_context lsp.CompletionContext|nil
---@field public status cmp.SourceStatus
---@field public complete_dedup function
---@field public default_replace_range lsp.Range
---@field public default_insert_range lsp.Range
---@field public position_encoding lsp.PositionEncodingKind
local source = {}
---@alias cmp.SourceStatus 1 | 2 | 3
@@ -41,6 +44,7 @@ source.new = function(name, s)
self.cache = cache.new()
self.complete_dedup = async.dedup()
self.revision = 0
self.position_encoding = self:get_position_encoding_kind()
self:reset()
return self
end
@@ -108,7 +112,7 @@ source.get_entries = function(self, ctx)
local entries = {}
local matching_config = self:get_matching_config()
for _, e in ipairs(target_entries) do
local o = e:get_offset()
local o = e.offset
if not inputs[o] then
inputs[o] = string.sub(ctx.cursor_before_line, o)
end
@@ -118,7 +122,7 @@ source.get_entries = function(self, ctx)
e.exact = false
if e.score >= 1 then
e.matches = match.matches
e.exact = e:get_filter_text() == inputs[o] or e:get_word() == inputs[o]
e.exact = e.filter_text == inputs[o] or e.word == inputs[o]
if entry_filter(e, ctx) then
entries[#entries + 1] = e
@@ -138,46 +142,46 @@ source.get_entries = function(self, ctx)
end
---Get default insert range (UTF8 byte index).
---@package
---@return lsp.Range
source.get_default_insert_range = function(self)
if not self.context then
error('context is not initialized yet.')
end
return self.cache:ensure({ 'get_default_insert_range', tostring(self.revision) }, function()
return {
start = {
line = self.context.cursor.row - 1,
character = self.offset - 1,
},
['end'] = {
line = self.context.cursor.row - 1,
character = self.context.cursor.col - 1,
},
}
end)
source._get_default_insert_range = function(self)
return {
start = {
line = self.context.cursor.row - 1,
character = self.offset - 1,
},
['end'] = {
line = self.context.cursor.row - 1,
character = self.context.cursor.col - 1,
},
}
end
---Get default replace range (UTF8 byte index).
---@package
---@return lsp.Range
source.get_default_replace_range = function(self)
if not self.context then
error('context is not initialized yet.')
end
source._get_default_replace_range = function(self)
local _, e = pattern.offset('^' .. '\\%(' .. self:get_keyword_pattern() .. '\\)', string.sub(self.context.cursor_line, self.offset))
return {
start = {
line = self.context.cursor.row - 1,
character = self.offset,
},
['end'] = {
line = self.context.cursor.row - 1,
character = (e and self.offset + e - 2 or self.context.cursor.col - 1),
},
}
end
return self.cache:ensure({ 'get_default_replace_range', tostring(self.revision) }, function()
local _, e = pattern.offset('^' .. '\\%(' .. self:get_keyword_pattern() .. '\\)', string.sub(self.context.cursor_line, self.offset))
return {
start = {
line = self.context.cursor.row - 1,
character = self.offset,
},
['end'] = {
line = self.context.cursor.row - 1,
character = (e and self.offset + e - 2 or self.context.cursor.col - 1),
},
}
end)
---@deprecated use source.default_insert_range instead
source.get_default_insert_range = function(self)
return self.default_insert_range
end
---@deprecated use source.default_replace_range instead
source.get_default_replae_range = function(self)
return self.default_replace_range
end
---Return source name.
@@ -322,6 +326,9 @@ source.complete = function(self, ctx, callback)
self.offset = offset
self.request_offset = offset
self.context = ctx
self.default_replace_range = self:_get_default_replace_range()
self.default_insert_range = self:_get_default_insert_range()
self.position_encoding = self:get_position_encoding_kind()
self.completion_context = completion_context
self.source:complete(
vim.tbl_extend('keep', misc.copy(self:get_source_config()), {
@@ -346,11 +353,11 @@ source.complete = function(self, ctx, callback)
self.status = source.SourceStatus.COMPLETED
self.entries = {}
for _, item in ipairs(response.items or response) do
if (item or {}).label then
if item.label then
local e = entry.new(ctx, self, item, response.itemDefaults)
if not e:is_invalid() then
table.insert(self.entries, e)
self.offset = math.min(self.offset, e:get_offset())
self.offset = math.min(self.offset, e.offset)
end
end
end