-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(lsp): request only changed portions of the buffer in changetracker #16277
Conversation
We can elevate this into utils |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM pending our discussion on discussion on matrix/the couple comments I left.
- include the benchmark for pre/post 'nil-ing' the prev_lines table
- include the benchmark for incrementally update curr_lines
- check to what extent this mitigates lsp: synchronization bottleneck due to buf_get_lines (caused by line-by-line changes) #11867 and Saving large files is extremely slow when using nvim-lsp and rustfmt #14205. In my short tests it doesn't (which is ok/not a blocker, just want to make sure we are documenting things correctly)
Thanks for the PR!
12b8f1f
to
eee1825
Compare
Done |
Is a bit colloquial (first person) and long, can you make it a bit more dry/short with bullet points? Anyways otherwise LGTM, thanks for the changes. Tagging mathias for review as a formality |
Also you didn't end up including
AFAIK |
Anyway, I've decided that it fits here better |
eee1825
to
5fe2de0
Compare
No, I meant that your commit message is not accurate :) |
Crap. Well, I have already moved the fix back. Do you mind it? |
Can you just unmove it back? It doesn't make sense to group the commits this way |
5fe2de0
to
7fbba2e
Compare
I didn't notice a difference with my repro |
|
Can confirm that this fixes #11867 (~26s -> ~0.4s). call setline(1, getline(1, '$')) Judging by the nature of that issue (an on_lines being fired for every line in the buffer), I believe that in the end my tests are realistic. |
Cool, when you update the commit message and mathias rubber stamps (or requests changes) we will merge. |
Please wait, checking the other issue. |
Fixes #14205 too due to rust.vim's usage of |
7fbba2e
to
50b9e81
Compare
Edited the commit message |
I'm going to give the fixed incremental sync implementation a few days ~= week for people to report bugs @mfussenegger Otherwise this PR is failing lint. |
The failure is in unrelated C code, does this happen to other PRs too? |
Mathias will merge this PR when he has reviewed. It will likely be in 0.6 |
Nice. Thank you. |
50b9e81
to
aa407da
Compare
I should also note that I have found that this PR may break on_reload logic. I'll investigate this. |
False alarm, this was a bug in a specific Language Server, I've fixed it: LuaLS/lua-language-server#821 |
I have discovered a different bug related to on_reload. In short, the on_reload handler must |
aa407da
to
a070aad
Compare
The following code measures the time required to perform a buffer edit to that operates individually on each line, common to plugins such as vim-commentary. set rtp+=~/.config/nvim/plugged/nvim-lspconfig set rtp+=~/.config/nvim/plugged/vim-commentary lua require('lspconfig')['ccls'].setup({}) function! Benchmark(tries) abort let results_comment = [] let results_undo = [] for i in range(a:tries) echo printf('run %d', i+1) let begin = reltime() normal gggcG call add(results_comment, reltimefloat(reltime(begin))) let begin = reltime() silent! undo call add(results_undo, reltimefloat(reltime(begin))) redraw endfor let avg_comment = 0.0 let avg_undo = 0.0 for i in range(a:tries) echomsg printf('run %3d: comment=%fs undo=%fs', i+1, results_comment[i], results_undo[i]) let avg_comment += results_comment[i] let avg_undo += results_undo[i] endfor echomsg printf('average: comment=%fs undo=%fs', avg_comment / a:tries, avg_undo / a:tries) endfunction command! -bar Benchmark call Benchmark(10) All text changes will be recorded within a single undo operation. Both the comment operation itself and the undo operation will generate an on_lines event for each changed line. Formatter plugins using setline() have also been found to exhibit the same problem (neoformat, :RustFmt in rust.vim), as this function too generates an on_lines event for every line it changes. Using the neovim codebase as an example (commit 2ecf0a4) with neovim itself built at 2ecf0a4 with CMAKE_BUILD_TYPE=Release shows the following performance improvement: src/nvim/lua/executor.c, 1432 lines: baseline, no optimizations: comment=0.540587s undo=0.440249s without double-buffering optimization: comment=0.183314s undo=0.060663s all optimizations in this commit: comment=0.174850s undo=0.052789s src/nvim/search.c, 5467 lines: baseline, no optimizations: comment=7.420446s undo=7.656624s without double-buffering optimization: comment=0.889048s undo=0.486026s all optimizations in this commit: comment=0.662899s undo=0.243628s src/nvim/eval.c, 11355 lines: baseline, no optimizations: comment=41.775695s undo=44.583374s without double-buffering optimization: comment=3.643933s undo=2.817158s all optimizations in this commit: comment=1.510886s undo=0.707928s This performance improvement is due to avoiding expensive lua string reallocations on each operation, and instead requesting only the changed chunk of the buffer, as reported by firstline and new_lastline parameters of on_lines, plus re-using already allocated tables for storing the resulting lines to reduce the load on the garbage collector.
a070aad
to
d324dec
Compare
Is there anything left to be done with regards to this PR? |
No, I passed review/merging on a section of my core responsibilities over to @mfussenegger |
@dmitmel When I tried benchmarking this I didn't get the speedup I expected, using your reproduction from #11867 (comment) I outlined the situation (which you are probably already familiar with here): #11867 (comment). I think an alternative to this PR would involve bypassing get_lines completely and caching firstline, lastline, newlastline, etc. on each on_lines call, and only calling buf_get_lines at the beginning and end of the debounce window (without any of the intermediate caching as implemented in this PR). The alternative, which I am also ok with, is to tell users to switch to plugins which apply better atomic buffer changes (which IMO is what they should be doing anyways). What do you think? |
Honestly, at that point you might as well stop using firstline/lastline and find the modified range using the procedure I have implemented in #16437. As we have previously established in the Matrix chat, comparing even tens of thousands of strings is very very cheap, and that would considerably simplify the code by bypassing the logic necessary for combining all of the intermediate firstline/lastline ranges into a single one. |
I'm pretty sure that's like 3-4 lines of code... Anyways, if you have time can you verify this doesn't actually address the performance issues or point out how I can verify that it does? I used basically config from #11867 (comment) |
No idea. For me it does work, and the improvement is significant (22s -> 0.5s). All I did was compile Nvim on this branch and run the command I specified. Can you record a screencast or something like that? |
@dmitmel if you want the actual authorship of the commit rather than coauthor you can just apply the changes from https://github.com/neovim/neovim/pull/17118/files directly to this PR which updates it to address the rebase conflicts. |
I went ahead and merged this in #17118. Thanks for your contribution and for addressing all of my questions :) |
Closes #11867
This PR is an optimization which fixes the issue I have mentioned in the #neovim-dev room. In short: some plugins, most notably vim-commentary and tcomment.vim, generate a lot of
on_lines
events when applied to a long range. In my particular case, if I comment out a range with either of the plugins I have mentioned, they both generate oneon_lines
per each commented line. The issue here is thatchangetracking.prepare
requests the entire buffer contents withnvim_buf_get_lines(bufnr, 0, -1, true)
, which, while in itself is pretty fast, adds up quickly on hundreds ofon_lines
events.I fixed the problem by only requesting the changed parts of the buffer since, obviously, we do get the change ranges. The unchanged lines are taken from the table with previous lines. A function such as
Array#splice
from JavaScript would've helped here for modifying the lines table in-place, but alas, the best we get in Lua istable.insert
/table.remove
, which can result in easily result in hidden execution time complexity (case in point: hrsh7th/cmp-buffer#18), and a proper implementation of thissplice
function can get pretty long (see the cmp-buffer PR, or an ECMAScript-compliant implementation here), so I decided against in-place modification.Not all is lost, however, as I have found that double-buffering the lines tables also helps. This improves performance by reducing the load on the garbage collector because very long tables are not allocated on every
on_lines
. The way it is done is described in a comment in the code, but basically, when theincremental_changes
function is done withcurr_lines
andprev_lines
,curr_lines
is saved as it normally would be, butprev_lines
is cleared and also saved because clearing the contents of the table doesn't actually de-allocate the internal storage of the table, the keys withnil
values are deleted only on the next garbage collection cycle. As such, a table with a fully-allocated internal storage is kept, which will be re-used on the nexton_lines
event ascurr_lines
.Does the logic in the loops which construct
curr_lines
need commenting? Perhaps this code should go into a separate function, or a method onchangetracking
?