prabirshrestha / vim-lsp

async language server protocol plugin for vim and neovim
MIT License
3.13k stars 305 forks source link

Improve performance placing signs and virtual text #1455

Closed gbarta closed 1 year ago

gbarta commented 1 year ago

I had some stuttering in files with a lot of errors (the errors are due to misconfiguration).

Profiling showed that calls to getbufinfo(...).linecount were taking up a lot of the time in calls setting signs and virtual text:

FUNCTIONS SORTED ON SELF TIME
count  total (s)   self (s)  function
    3   3.115957   3.108029  <SNR>178_place_virtual_text()
    5   2.661250   2.650157  <SNR>177_place_signs()
 1817   3.641346   0.935556  <SNR>158_makeSubjectFactory()
 1552              0.616058  lsp#log()
  439   4.250543   0.428110  <SNR>186_on_stdout()
14178   2.325410   0.401909  <SNR>158_filterSourceCallback()
    5   0.804383   0.400785  <SNR>165_place_highlights()
11931   0.565887   0.377128  <SNR>158_shareTalkbackCallback()
12719   2.441088   0.298790  <SNR>158_shareSourceCallback()
11931              0.188758  <SNR>158_makeSubjectSinkCallback()
 7493              0.177195  <SNR>238_to_col()
    5   0.139655   0.139467  <SNR>236_set_textprops()
 7492   0.384446   0.115897  lsp#utils#position#lsp_to_vim()
 1410   3.730536   0.101006  <SNR>157_on_notification()
    1   0.130257   0.096629  <SNR>83_execute_term()
 1804   0.229137   0.082540  <SNR>158_mergeSourceCallback()
  104   0.080725   0.078467  <SNR>123_Highlight_Matching_Pair()
 7493   0.252220   0.075025  lsp#utils#position#lsp_character_to_vim()
 1410              0.050678  <SNR>186_get_content_length()
   80   0.040895   0.039967  <SNR>204_parse_screen()

FUNCTION  <SNR>178_place_virtual_text()
Called 3 times
Total time:   3.115957
 Self time:   3.108029

count  total (s)   self (s)
 2251   0.003563   0.003533     for l:item in lsp#utils#iteratable(a:diagnostics_response['params']['diagnostics'])
 2248   0.044210   0.036312         let l:line = lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['range']['start'])
 2248              0.013252         let l:name = get(s:severity_sign_names_mapping, get(l:item, 'severity', 3), 'LspError')
 2248              0.007830         let l:text = g:lsp_diagnostics_virtual_text_prefix . l:item['message']

                                    " Some language servers report an unexpected EOF one line past the end
 2248              1.465635         if l:line == getbufinfo(a:bufnr)[0].linecount + 1
                                        let l:line = l:line - 1
 2248              0.002182         endif

 2248              0.018263         if has('nvim')
                                        let l:hl_name = l:name . 'VirtualText'
                                        " need to do -1 for virtual text
                                        call nvim_buf_set_virtual_text(a:bufnr, s:namespace_id, l:line - 1, [[l:text, l:hl_name]], {})
 2248              0.001801         else
                                        " it's an error to add virtual text on lines that don't exist
                                        " anymore due to async processing, just skip such diagnostics
 2248              1.466185             if l:line <= getbufinfo(a:bufnr)[0].linecount
 2248              0.011529                 let l:type = 'vim_lsp_' . l:name . '_virtual_text'
 2248              0.020543                 call prop_remove({'all': v:true, 'type': l:type, 'bufnr': a:bufnr}, l:line)
 2248              0.027890                 call prop_add( l:line, 0, {   'type': l:type, 'text': l:text, 'bufnr': a:bufnr,   'text_align': g:lsp_diagnostics_virtual_text_align,   'text_padding_left': g:lsp_diagnostics_virtual_text_padding_left,   'text_wrap': g:lsp_diagnostics_virtual_text_wrap, })
 2248              0.001721             endif
 2248              0.001280         endif
 2251              0.001828     endfor

FUNCTION  <SNR>177_place_signs()
Called 5 times
Total time:   2.661250
 Self time:   2.650157

count  total (s)   self (s)
 3751   0.004842   0.004818     for l:item in lsp#utils#iteratable(a:diagnostics_response['params']['diagnostics'])
 3746   0.056488   0.045419         let l:line = lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['range']['start'])

                                    " Some language servers report an unexpected EOF one line past the end
                                    " key 'linecount' may be missing.
 3746              1.231126         if has_key(getbufinfo(a:bufnr)[0], 'linecount')
 3746              1.228966             if l:line == getbufinfo(a:bufnr)[0].linecount + 1
                                            let l:line = l:line - 1
 3746              0.003231             endif
 3746              0.002211         endif

 3746              0.017582         if has_key(l:item, 'severity') && !empty(l:item['severity'])
 3746              0.018588             let l:sign_name = get(s:severity_sign_names_mapping, l:item['severity'], 'LspError')
 3746              0.015624             let l:sign_priority = get(g:lsp_diagnostics_signs_priority_map, l:sign_name, g:lsp_diagnostics_signs_priority)
 3746              0.017495             let l:sign_priority = get(g:lsp_diagnostics_signs_priority_map, a:server . '_' . l:sign_name, l:sign_priority)
                                        " pass 0 and let vim generate sign id
 3746              0.040901             let l:sign_id = sign_place(0, s:sign_group, l:sign_name, a:bufnr,{ 'lnum': l:line, 'priority': l:sign_priority })
 3746              0.002699         endif
 3751              0.002769     endfor

Since the two slowest functions don't change the linecount, I changed them to call getbufinfo(...).linecount once rather than many times in a loop, which improved the performance making a noticable reduction in stutter:

FUNCTIONS SORTED ON SELF TIME
count  total (s)   self (s)  function
 1776   3.481431   0.881940  <SNR>158_makeSubjectFactory()
 1433              0.538464  lsp#log()
  354   3.986947   0.411101  <SNR>186_on_stdout()
13680   2.110882   0.366118  <SNR>158_filterSourceCallback()
11646   0.533962   0.356100  <SNR>158_shareTalkbackCallback()
    4   0.605748   0.300701  <SNR>165_place_highlights()
12432   2.338666   0.289876  <SNR>158_shareSourceCallback()
11646              0.177862  <SNR>158_makeSubjectSinkCallback()
    4   0.181355   0.174329  <SNR>178_place_virtual_text()
 5992              0.133296  <SNR>237_to_col()
    4   0.106953   0.101109  <SNR>177_place_signs()
    1   0.136466   0.098458  <SNR>83_execute_term()
 1372   3.525319   0.093574  <SNR>157_on_notification()
 5992   0.290182   0.086865  lsp#utils#position#lsp_to_vim()
 1634   0.205237   0.071384  <SNR>158_mergeSourceCallback()
 5992   0.190807   0.057511  lsp#utils#position#lsp_character_to_vim()
 1372              0.050213  <SNR>186_get_content_length()
    5   0.038590   0.037347  sy#repo#get_diff()
 1232              0.028958  <SNR>158_debounceTimeSourceCallback()
    2   0.026388   0.026109  <SNR>11_LoadFTPlugin()

FUNCTION  <SNR>177_place_signs()
Called 4 times
Total time:   0.106953
 Self time:   0.101109

count  total (s)   self (s)
    4              0.000037     let l:linecount = get(getbufinfo(a:bufnr)[0],'linecount',0) " linecount available from vim 8.2.0019
 3000   0.002635   0.002618     for l:item in lsp#utils#iteratable(a:diagnostics_response['params']['diagnostics'])
 2996   0.025553   0.019725         let l:line = lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['range']['start'])

                                    " Some language servers report an unexpected EOF one line past the end
 2996              0.004971         if  l:line == l:linecount + 1 && l:linecount > 0
                                        let l:line = l:line - 1
 2996              0.001555         endif

 2996              0.009090         if has_key(l:item, 'severity') && !empty(l:item['severity'])
 2996              0.010329             let l:sign_name = get(s:severity_sign_names_mapping, l:item['severity'], 'LspError')
 2996              0.009624             let l:sign_priority = get(g:lsp_diagnostics_signs_priority_map, l:sign_name, g:lsp_diagnostics_signs_priority)
 2996              0.011003             let l:sign_priority = get(g:lsp_diagnostics_signs_priority_map, a:server . '_' . l:sign_name, l:sign_priority)
                                        " pass 0 and let vim generate sign id
 2996              0.021257             let l:sign_id = sign_place(0, s:sign_group, l:sign_name, a:bufnr,{ 'lnum': l:line, 'priority': l:sign_priority })
 2996              0.001778         endif
 3000              0.001585     endfor

FUNCTION  <SNR>178_place_virtual_text()
Called 4 times
Total time:   0.181355
 Self time:   0.174329

count  total (s)   self (s)
    4              0.002049     let l:linecount = get(getbufinfo(a:bufnr)[0],'linecount',0) " linecount available from vim 8.2.0019
 3000   0.003194   0.003173     for l:item in lsp#utils#iteratable(a:diagnostics_response['params']['diagnostics'])
 2996   0.035493   0.028488         let l:line = lsp#utils#position#lsp_line_to_vim(a:bufnr, l:item['range']['start'])
 2996              0.014183         let l:name = get(s:severity_sign_names_mapping, get(l:item, 'severity', 3), 'LspError')
 2996              0.008768         let l:text = g:lsp_diagnostics_virtual_text_prefix . l:item['message']

                                    " Some language servers report an unexpected EOF one line past the end
 2996              0.006014         if l:line == l:linecount + 1 && l:linecount > 0
                                        let l:line = l:line - 1
 2996              0.001814         endif

 2996              0.018583         if has('nvim')
                                        let l:hl_name = l:name . 'VirtualText'
                                        " need to do -1 for virtual text
                                        call nvim_buf_set_virtual_text(a:bufnr, s:namespace_id, l:line - 1, [[l:text, l:hl_name]], {})
 2996              0.001986         else
                                        " it's an error to add virtual text on lines that don't exist
                                        " anymore due to async processing, just skip such diagnostics
 2996              0.005336             if l:line <= l:linecount || l:linecount == 0
 2996              0.006690                 let l:type = 'vim_lsp_' . l:name . '_virtual_text'
 2996              0.017179                 call prop_remove({'all': v:true, 'type': l:type, 'bufnr': a:bufnr}, l:line)
 2996              0.029355                 call prop_add( l:line, 0, {   'type': l:type, 'text': l:text, 'bufnr': a:bufnr,   'text_align': g:lsp_diagnostics_virtual_text_align,   'text_padding_left': g:lsp_diagnostics_virtual_text_padding_left,   'text_wrap': g:lsp_diagnostics_virtual_text_wrap, })
 2996              0.001820             endif
 2996              0.001556         endif
 3000              0.001778     endfor
gbarta commented 1 year ago

Using: VIM - Vi IMproved 9.0 (2022 Jun 28, compiled Mar 23 2023 23:47:02) MS-Windows 64-bit GUI version with OLE support Included patches: 1-1425

gbarta commented 1 year ago

I've noticed the 'vital' polyfill get_line_count at https://github.com/prabirshrestha/vim-lsp/blob/master/autoload/vital/_lsp/VS/Vim/Buffer.vim#L15

How about I use that instead so we don't need to restrict the version? Vim 8.1. is still in some supported LTS linux distros, e.g. Ubuntu focal.

prabirshrestha commented 1 year ago

That sounds good.

prabirshrestha commented 1 year ago

Merged. Thanks!