Repository: neovim/neovim

Issue #22426: Treesitter performance (tracking issue)
Created: 2023-02-27T09:54:16Z
Problem
Our treesitter implementation could be more efficient to overcome many known issues with performance. Specifically around large files.



 Injection languages are not incremental
See: #18107
Resolved with: #22309


 Rendering long lines is inefficient
See: #14756
Resolved with: #15405


 Parsing blocks user input
This is especially bad for the initial parse since this takes the longest.
Either:
(a): We run parsing on a separate thread (difficult).
(b): Use treesitters parsing timeout feature to segment the parse over several event-loop iterations (less-difficult).
See: #20765
Resolved with: (b) #22420


 Injection queries are run too often
Currently we run the full injection query on every edit to a buffer.

 Incrementally run the query on tree changes (#23070).

 ... on tree changes that intersect the injection regions.


 Partially run the query for rendered lines.



 Injections are not highlighted incrementally
Currently every injection is parsed, even if it never gets displayed.
Resolved with: #24647



Other

 Add better benchmarking to the testsuite (#23070)

Files for benchmarking:

https://raw.githubusercontent.com/torvalds/linux/master/drivers/gpu/drm/amd/include/asic_reg/bif/bif_5_1_sh_mask.h
https://raw.githubusercontent.com/torvalds/linux/master/drivers/gpu/drm/amd/include/asic_reg/dcn/dcn_3_2_0_sh_mask.h
https://github.com/justinmk/notes/blob/master/delicious.md (slow edits)

Duplicate issue #27184: Treesitter performance in cpp: editing with treesitter highlightning is noticably slow
Created: 2024-01-24T22:49:57Z
Problem
Editing cpp files can be remarkably slower when Treesitter highlighting is enabled.
There seems to a lot of invocation to on_bytes, and a lot of calls to tree:edit because all trees have their tree:edit method called, is this necessary? For notice: I'm rather new to neovim/treesitter development.
Steps to reproduce
Have a cpp file with this content:
#define A 0
repeated for 2 lines (tmp_2.cpp) or 10 000 lines (tmp_10000.cpp).
Configuration file with only treesitter and nvim-profiler enabled:
call plug#begin(stdpath('data') . '/treesitter_plugged')
Plug 'nvim-treesitter/nvim-treesitter'
Plug 'https://github.com/wookayin/nvim-profiler'
call plug#end()

lua require('nvim-treesitter.configs').setup { highlight = {enable=true}}
Scenario 1.
Visually select all 10k lines and perform <c-a> to increment the number on each line, notice the significant delay until the command completes.
When profiled from the command line with hyperfine we can see the different in time with treesitter highlight enabled/disabled.

Treesitter enabled: 239.3 ms
Treesitter disabled: 25.8 ms

Details treesitter enabled
$ VIMRUNTIME=./runtime hyperfine --ignore-failure 'build/bin/nvim -u ~/.config/nvim/treesitter.vim ~/tmp_10000.cpp -c"normal! V10000j^A" -c"cq"'
Benchmark 1: build/bin/nvim -u ~/.config/nvim/treesitter.vim ~/tmp.cpp -c"normal! V10000j" -c"cq"
  Time (mean ± σ):     258.8 ms ±  15.1 ms    [User: 230.9 ms, System: 17.1 ms]
  Range (min … max):   239.3 ms … 274.4 ms    10 runs
Details treesitter disabled:
$ VIMRUNTIME=./runtime hyperfine --ignore-failure 'build/bin/nvim -u ~/.config/nvim/treesitter.vim ~/tmp.cpp -c"normal! V10000j^A" -c"cq"'
Benchmark 1: build/bin/nvim -u ~/.config/nvim/treesitter.vim ~/tmp.cpp -c"normal! V10000j^A" -c"cq"
  Time (mean ± σ):      34.0 ms ±  10.2 ms    [User: 25.6 ms, System: 7.7 ms]
  Range (min … max):    25.8 ms …  59.4 ms    49 runs
When profiled with :LuaProfile run 3000 the top of the log says:
  181  ./runtime/lua/vim/treesitter/languagetree.lua:908
  <-   180  ./runtime/lua/vim/treesitter/languagetree.lua:947 < ./runtime/lua/vim/treesitter/languagetree.lua:1005 < ./runtime/lua/vim/treesitter.lua:67
  <-     1  ./runtime/lua/vim/treesitter/languagetree.lua:1005 < ./runtime/lua/vim/treesitter.lua:67

Looking at the code at line 908, we see:
  print("#self._trees", #self._trees)       " < -- this print is inserted by me
  for _, tree in pairs(self._trees) do
    tree:edit(                                              " <--- this is line 908
      start_byte,
      end_byte_old,
      end_byte_new,
      start_row,
      start_col,
      end_row_old,
      end_col_old,
      end_row_new,
      end_col_new
    )
  end
which is called after invoking on_bytes due to an edit (according to runtime/doc/api.txt:2111.
Scenario 2.
Putting some prints in the runtime lua code to instrument number of calls being done and for loop iterations. Then run this

open tmp_10000.cpp
Modify window/textsize to only show 10 lines of code
Visually select the first two lines
Press  <ctrl-a> (only the first two lines are changed, no other line is actually modified or editted)

Output:
1
#self._trees 1
#self._trees 11
2
#self._trees 1
#self._trees 11
3
#self._trees 1
#self._trees 11
4
#self._trees 1
#self._trees 11

Strangeness:

Why are there 4 calls and not 2 to function LanguageTree:_on_bytes()?
For each call, all trees visible in the window(?) + 1(why?), is iterated over. Is this necessary when actually onlt the first two lines changed?

The :InspectTree output for the tmp_2.cpp file looks like this:
(preproc_def ; [1:1 - 2:0]
  name: (identifier) ; [1:9 - 9]
  value: (preproc_arg ; [1:11 - 11]
    (ERROR ; [1:11 - 11]
      (number_literal)))) ; [1:11 - 11]
(preproc_def ; [2:1 - 3:0]
  name: (identifier) ; [2:9 - 9]
  value: (preproc_arg ; [2:11 - 11]
    (ERROR ; [2:11 - 11]
      (number_literal)))) ; [2:11 - 11]

Expected behavior
Treesitter should not add much delay to simple editing commands.
Neovim version (nvim -v)
NVIM v0.10.0-dev-2186+gb280d57db
Vim (not Nvim) behaves the same?
no, related to treesitter
Operating system/version
Ubuntu 22.04
Terminal name/version
Wezterm
$TERM environment variable
xterm-256color
Installation
RelWithDebInfo build from repo

Closing comment (2024-01-24T23:06:38Z): To provide more explanation: this should be another instance of #27155 (tracked in #22426;  Injection queries are run too often -- Currently we run the full injection query on every edit to a buffer)
Note that "C preprocessors" involves injections of the C lang itself:
((preproc_arg) @injection.content
  (#set! injection.language "c"))