diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index ea340105d3..6eaa69cb64 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -177,6 +177,8 @@ PERFORMANCE Lua. • The table holding LSP data is now cleared using `table.clear`, thus reducing GC and memory reallocation during each data reset. +• When parsing the received Content-Length messages, + the RPC client will no longer allocate extra strings. PLUGINS diff --git a/runtime/lua/vim/_core/stringbuffer.lua b/runtime/lua/vim/_core/stringbuffer.lua index 0241784a9b..e789f7b003 100644 --- a/runtime/lua/vim/_core/stringbuffer.lua +++ b/runtime/lua/vim/_core/stringbuffer.lua @@ -22,21 +22,22 @@ if has_strbuffer then return M end +--- @class vim._core.stringbuffer.ptr +--- @field [integer] integer + --- @class vim._core.stringbuffer +--- @field private ptr vim._core.stringbuffer.ptr --- @field private buf string[] --- @field package len integer absolute length of the `buf` --- @field package skip_ptr integer local StrBuffer = {} StrBuffer.__index = StrBuffer ---- @return string -function StrBuffer:tostring() +function StrBuffer:_normalize() if #self.buf > 1 then self.buf = { table.concat(self.buf) } end - -- assert(self.len == #(self.buf[1] or ''), 'len mismatch') - if self.skip_ptr > 0 then if self.buf[1] then self.buf[1] = self.buf[1]:sub(self.skip_ptr + 1) @@ -45,18 +46,21 @@ function StrBuffer:tostring() self.skip_ptr = 0 end - -- assert(self.len == #(self.buf[1] or ''), 'len mismatch') + return self +end - return self.buf[1] or '' +--- @return string +function StrBuffer:tostring() + return self:_normalize().buf[1] or '' end StrBuffer.__tostring = StrBuffer.tostring --- @private ---- Efficiently peak at the first `n` characters of the buffer. +--- Efficiently peek at the first `n` characters of the buffer. --- @param n integer --- @return string -function StrBuffer:_peak(n) +function StrBuffer:_peek(n) local skip, buf1 = self.skip_ptr, self.buf[1] if buf1 and (n + skip) < #buf1 then return buf1:sub(skip + 1, skip + n) @@ -81,7 +85,7 @@ end --- @return string function StrBuffer:get(n) n = n or self.len - local r = self:_peak(n) + local r = self:_peek(n) self:skip(n) return r end @@ -99,8 +103,27 @@ function StrBuffer:reset() return self end +--- Efficiently read the character at the 0-based index `k` in the buffer. +--- @param k integer +--- @return integer +function StrBuffer:_index(k) + return self:_normalize().buf[1]:byte(k + 1) +end + +--- @return vim._core.stringbuffer.ptr, integer +function StrBuffer:ref() + return self.ptr, self.len - self.skip_ptr +end + function M.new() - return setmetatable({}, StrBuffer):reset() + local self = setmetatable({}, StrBuffer):reset() + ---@diagnostic disable-next-line: invisible + self.ptr = setmetatable({}, { + __index = function(_, k) + return StrBuffer._index(self, k) + end, + }) + return self end --- @param buf vim._core.stringbuffer diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 7d85be89c9..6739073cd3 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -191,22 +191,30 @@ local default_dispatchers = { } --- @async -local function request_parser_loop() - local buf = strbuffer.new() +local function parse_content_length() + local strbuf = strbuffer.new() while true do - local msg = buf:tostring() - local header_end = msg:find('\r\n\r\n', 1, true) - if header_end then - local header = buf:get(header_end + 1) - buf:skip(2) -- skip past header boundary - local content_length = get_content_length(header) - while strbuffer.len(buf) < content_length do - buf:put(coroutine.yield()) + local header_len ---@type integer? + local ptr, len = strbuf:ref() + for i = 0, len - 4 do + -- Find the header boundary "\r\n\r\n" + -- (compare bytes instead of string.find(), to avoid a string alloc). + if ptr[i] == 13 and ptr[i + 1] == 10 and ptr[i + 2] == 13 and ptr[i + 3] == 10 then + header_len = i + 2 + break end - local body = buf:get(content_length) - buf:put(coroutine.yield(body)) + end + if header_len then + local header = strbuf:get(header_len) + strbuf:skip(2) -- skip past header boundary + local content_length = get_content_length(header) + while strbuffer.len(strbuf) < content_length do + strbuf:put(coroutine.yield()) + end + local body = strbuf:get(content_length) + strbuf:put(coroutine.yield(body)) else - buf:put(coroutine.yield()) + strbuf:put(coroutine.yield()) end end end @@ -218,7 +226,7 @@ end function M.create_read_loop(handle_body, on_exit, on_error) on_exit = on_exit or function() end on_error = on_error or function() end - local co = coroutine.create(request_parser_loop) + local co = coroutine.create(parse_content_length) coroutine.resume(co) return function(err, chunk) if err then