Merge pull request #18 from dmitmel/optimize-hidden-nested-loops

Fix O(n^2) complexity in the watcher hidden within nested loops created by table.{insert,remove}
This commit is contained in:
hrsh7th 2021-11-08 19:55:26 +09:00 committed by GitHub
commit a46e1ce308
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -4,7 +4,8 @@
---@field public length number ---@field public length number
---@field public pattern string ---@field public pattern string
---@field public timer any|nil ---@field public timer any|nil
---@field public words table<number, string[]> ---@field public lines_count number
---@field public lines_words table<number, string[]>
---@field public processing boolean ---@field public processing boolean
local buffer = {} local buffer = {}
@ -20,7 +21,8 @@ function buffer.new(bufnr, length, pattern)
self.length = length self.length = length
self.pattern = pattern self.pattern = pattern
self.timer = nil self.timer = nil
self.words = {} self.lines_count = 0
self.lines_words = {}
self.processing = false self.processing = false
return self return self
end end
@ -32,12 +34,14 @@ function buffer.close(self)
self.timer:close() self.timer:close()
self.timer = nil self.timer = nil
end end
self.words = {} self.lines_words = {}
self.lines_count = 0
end end
---Indexing buffer ---Indexing buffer
function buffer.index(self) function buffer.index(self)
self.processing = true self.processing = true
self.lines_count = vim.api.nvim_buf_line_count(self.bufnr)
local index = 1 local index = 1
local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) local lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false)
self.timer = vim.loop.new_timer() self.timer = vim.loop.new_timer()
@ -68,32 +72,64 @@ end
--- watch --- watch
function buffer.watch(self) function buffer.watch(self)
vim.api.nvim_buf_attach(self.bufnr, false, { vim.api.nvim_buf_attach(self.bufnr, false, {
on_lines = vim.schedule_wrap(function(_, _, _, firstline, old_lastline, new_lastline, _, _, _) -- NOTE: line indexes are 0-based and the last line is not inclusive.
on_lines = function(_, _, _, first_line, old_last_line, new_last_line, _, _, _)
if not vim.api.nvim_buf_is_valid(self.bufnr) then if not vim.api.nvim_buf_is_valid(self.bufnr) then
self:close() self:close()
return true return true
end end
-- append local delta = new_last_line - old_last_line
for i = old_lastline, new_lastline - 1 do local new_lines_count = self.lines_count + delta
table.insert(self.words, i + 1, {}) if new_lines_count == 0 then -- clear
end -- This branch protects against bugs after full-file deletion. If you
-- do, for example, gdGG, the new_last_line of the event will be zero.
-- remove -- Which is not true, a buffer always contains at least one empty line,
for _ = new_lastline, old_lastline - 1 do -- only unloaded buffers contain zero lines.
table.remove(self.words, new_lastline + 1) new_lines_count = 1
for i = self.lines_count, 2, -1 do
self.lines_words[i] = nil
end
self.lines_words[1] = {}
elseif delta > 0 then -- append
-- Explicitly reserve more slots in the array part of the lines table,
-- all of them will be filled in the next loop, but in reverse order
-- (which is why I am concerned about preallocation). Why is there no
-- built-in function to do this in Lua???
for i = self.lines_count + 1, new_lines_count do
self.lines_words[i] = vim.NIL
end
-- Move forwards the unchanged elements in the tail part.
for i = self.lines_count, old_last_line + 1, -1 do
self.lines_words[i + delta] = self.lines_words[i]
end
-- Fill in new tables for the added lines.
for i = old_last_line + 1, new_last_line do
self.lines_words[i] = {}
end
elseif delta < 0 then -- remove
-- Move backwards the unchanged elements in the tail part.
for i = old_last_line + 1, self.lines_count do
self.lines_words[i + delta] = self.lines_words[i]
end
-- Remove (already copied) tables from the end, in reverse order, so
-- that we don't make holes in the lines table.
for i = self.lines_count, new_lines_count + 1, -1 do
self.lines_words[i] = nil
end
end end
self.lines_count = new_lines_count
-- replace lines -- replace lines
local lines = vim.api.nvim_buf_get_lines(self.bufnr, firstline, new_lastline, false) local lines = vim.api.nvim_buf_get_lines(self.bufnr, first_line, new_last_line, true)
vim.api.nvim_buf_call(self.bufnr, function() vim.api.nvim_buf_call(self.bufnr, function()
for i, line in ipairs(lines) do for i, line in ipairs(lines) do
if line then if line then
self:index_line(firstline + i, line or '') self:index_line(first_line + i, line)
end end
end end
end) end)
end), end,
}) })
end end
@ -120,13 +156,13 @@ function buffer.index_line(self, linenr, line)
end end
end end
self.words[linenr] = words self.lines_words[linenr] = words
end end
--- get_words --- get_words
function buffer.get_words(self) function buffer.get_words(self)
local words = {} local words = {}
for _, line in ipairs(self.words) do for _, line in ipairs(self.lines_words) do
for _, w in ipairs(line) do for _, w in ipairs(line) do
table.insert(words, w) table.insert(words, w)
end end