emacs-lsp / lsp-mode

Emacs client/library for the Language Server Protocol
https://emacs-lsp.github.io/lsp-mode
GNU General Public License v3.0
4.8k stars 893 forks source link

Emacs hangs temporarily while trying to get completion while rust-analyzer is initializing. #2612

Closed RomeuG closed 3 years ago

RomeuG commented 3 years ago

Describe the bug Emacs hangs temporarily while trying to get completion while rust-analyzer is initializing. rust-analyzer takes some seconds to initialize, going from a small amount of RAM to 900MB RAM (this is normal, just giving away some details).

To Reproduce Steps to reproduce the behavior(sample project + file which can be used to reproduce the issue with.)

Expected behavior Emacs should not hang temporarily.

Which Language Server did you use I used lsp-rust with rust-analyzer.

OS GNU/Linux

Error callstack Information from *lsp-log*:

Command "rls" is present on the path.
Command "/bin/rust-analyzer" is present on the path.
Command "rls" is present on the path.
Command "/bin/rust-analyzer" is present on the path.
Found the following clients for /home/romeu/Documents/Projects/rustproject/src/main.rs: (server-id rls, priority -1), (server-id rust-analyzer, priority 1)
The following clients were selected based on priority: (server-id rust-analyzer, priority 1)
RomeuG commented 3 years ago

Sorry, the title is kind of wrong. It should be: Emacs hangs temporarily while trying to get completion while rust-analyzer is initializing.

yyoncho commented 3 years ago

Hanging might be caused by establishing the watches which happens during startup and causes hanging. Can you provide perf report?

RomeuG commented 3 years ago

This is the perf report:

         168  66% - ...
         167  66%  - company--perform
         167  66%   - company--begin-new
         167  66%    - company-calculate-candidates
         167  66%     - company--fetch-candidates
         167  66%      - company-call-backend-raw
         167  66%       - apply
         167  66%        - company-capf
         167  66%         - company-capf--candidates
         167  66%          - completion-all-completions
         167  66%           - completion--nth-completion
         167  66%            - completion--some
         167  66%             - #<compiled 0x11b93509fa5b617b>
         165  65%              - completion-basic-all-completions
         165  65%               - completion-pcm--all-completions
         165  65%                - #<compiled -0x1c081f0461c2fb95>
         165  65%                 - #<compiled -0x1a98156bc45cc026>
          13   5%                  - lsp-request-while-no-input
          12   4%                   + #<compiled -0x10d7bc3be3860626>
           1   0%                  - lsp-completion--filter-candidates
           1   0%                   + -keep
           1   0%              + completion-pcm-all-completions
           1   0%              + completion-emacs22-all-completions
           1   0%  + ivy-thing-at-point
           0   0%    Automatic GC
          55  21% + command-execute
          13   5% + company-post-command
          11   4% + timer-event-handler
           3   1% + redisplay_internal (C function)
           1   0%   #<compiled -0x10d7bc3be3860626>

Doesn't seem to be the watches stuff. Even if it was, shouldn't it be async, though?

RomeuG commented 3 years ago

The completion itself is slow while typing too. Although thats because of the rust-analyzer server. But VSCode handles it asynchronously (doesn't hang while typing).

kiennq commented 3 years ago

The lsp-request-while-no-input only takes 5%, so it's not likely cause the hang. What's your Emacs version? Can you try to eval-defun for lsp-completion-at-point and try to post the perf log again?

RomeuG commented 3 years ago

@kiennq

I am using GNU Emacs 28.0.50. I am also using the native-comp branch. This also happens on Emacs version 27.1. But it seems the perf log has different results (sorry for the format):

- ...                                                             234  80%
 - company--begin-new                                             234  80%
  - company-calculate-candidates                                  234  80%
   - company--fetch-candidates                                    234  80%
    - company-call-backend-raw                                    234  80%
     - apply                                                      234  80%
      - company-capf                                              234  80%
       - company-capf--candidates                                 234  80%
        - completion-all-completions                              234  80%
         - completion--nth-completion                             234  80%
          - completion--some                                      234  80%
           - #<compiled 0x15819630a719>                           234  80%
            - completion-basic-all-completions                    234  80%
             - completion-pcm--all-completions                    234  80%
              - all-completions                                   234  80%
               - #<compiled 0x15819630a69d>                       234  80%
                - #<compiled 0x15819630a681>                      234  80%
                 - lsp-request-while-no-input                     226  77%
                  - accept-process-output                           9   3%
                   - #<compiled 0x1581958c2799>                     9   3%
                    - lsp--parser-on-message                        3   1%
                     - lsp--on-notification                         2   0%
                      + #<compiled 0x15819579bd59>                  1   0%
                     + lsp--get-message-type                        1   0%
                    - mapcar                                        1   0%
                     + lsp--parse-header                            1   0%
   Automatic GC                                                     0   0%
+ command-execute                                                  41  14%
+ company-post-command                                              5   1%
+ redisplay_internal (C function)                                   4   1%
+ #<compiled 0x1581958c2799>                                        4   1%
+ timer-event-handler                                               4   1%

It seems now lsp-request-while-no-input takes most of the time. Emacs hanged for 5 seconds, more or less.

If I run eval and use (lsp-completion-at-point), it is basically instant. (I hope this is what you asked)

kiennq commented 3 years ago

If I run eval and use (lsp-completion-at-point), it is basically instant. (I hope this is what you asked)

Well, what's I'm hoping is more information than just #<compiled ...> showing up. To do that we will need to reevaluate the functions again without let it byte-compiling, then do the repro steps again.

Can you try to go to lsp-mode.el and lsp-completion.el, run M-x eval-buffer in each file, try to repro the hang again and post the perf here?

RomeuG commented 3 years ago

@kiennq

Hey, thank you for helping me help you debug this problem. Here are the results after doing what you said:

         153  74% - ...
         153  74%  - completion-basic-all-completions
         153  74%   - completion-pcm--all-completions
         153  74%    - #<lambda 0x15fc25c9f652ea8f>
         153  74%     - cond
         153  74%      - funcall
         153  74%       - #<lambda 0x1313c1e802e4f86e>
         153  74%        - let
         153  74%         - catch
         153  74%          - let
         153  74%           - cond
         153  74%            - let*
         150  72%             - lsp-request-while-no-input
         150  72%              - let*
         150  72%               - unwind-protect
         150  72%                - progn
         148  71%                 - while
          12   5%                  - accept-process-output
          12   5%                   + #<lambda -0x15bcf0911b730f27>
           5   2%                  - not
           3   1%                     or
           2   0%                 + let*
           3   1%             + progn
           0   0%    Automatic GC
          36  17% + command-execute
          11   5% + company-post-command
           3   1% + timer-event-handler
           2   0% + redisplay_internal (C function)
           1   0% + #<lambda -0x15bcf0911b730f27>

It seems it wastes most of the time in a while loop:

(cl-defun lsp-request-while-no-input (method params)
  "Send request METHOD with PARAMS and waits until there is no input.
Return same value as `lsp--while-no-input' and respecting `non-essential'."
  (let* (resp-result resp-error done?)
    (unwind-protect
        (progn
          (lsp-request-async method
                             params
                             (lambda (res) (setf resp-result (or res :finished)))
                             :error-handler (lambda (err) (setf resp-error err))
                             :mode 'detached
                             :cancel-token :sync-request)
          (while (not (or resp-error resp-result
                          (and non-essential (input-pending-p))))
            (accept-process-output nil 0.001))
          (setq done? t)
          (cond
           ((eq resp-result :finished) nil)
           (resp-result resp-result)
           ((lsp-json-error? resp-error) (error (lsp:json-error-message resp-error)))
           ((input-pending-p) (when lsp--throw-on-input
                                (throw 'input :interrupted)))
           (t (error (lsp:json-error-message (cl-first resp-error))))))
      (unless done?
        (lsp-cancel-request-by-token :sync-request)))))
(while (not (or resp-error resp-result
                          (and non-essential (input-pending-p))))
            (accept-process-output nil 0.001))
aaronjensen commented 3 years ago

In general, it's been my experience that since moving away from company-lsp (which was an async company backend) that company-capf is pretty much unusable with a low company-idle-delay. A return to company-lsp would be welcome, though maybe this inquiry will lead somewhere...

RomeuG commented 3 years ago

I totally agree with you. I don't remember having these kinds of issues with company-lsp.

yyoncho commented 3 years ago

The only way company-capf to block if you somehow force non-essential call, my personal opinion is that we should never block.

By default, company-capf does not block.

aaronjensen commented 3 years ago

Sorry to disagree, but in practice that's just not the case. In typescript projects typing stutters all the time with an idle delay of 0.125. When I disable company it goes away. When I replace it with company-lsp and add stubs for the now missing functions, it goes away.

yyoncho commented 3 years ago

@aaronjensen do you see it with lsp-start-plain.el? If yes, can you provide a project to reproduce the issue with? I think that at some point doom had issues related to forcing non-essential calls.

yyoncho commented 3 years ago

@aaronjensen can you test after removing the non-essential, like this:

(cl-defun lsp-request-while-no-input (method params)
  "Send request METHOD with PARAMS and waits until there is no input.
Return same value as `lsp--while-no-input' and respecting `non-essential'."
  (let* (resp-result resp-error done?)
    (unwind-protect
        (progn
          (lsp-request-async method
                             params
                             (lambda (res) (setf resp-result (or res :finished)))
                             :error-handler (lambda (err) (setf resp-error err))
                             :mode 'detached
                             :cancel-token :sync-request)
          (while (not (or resp-error resp-result
                          (and (input-pending-p))))
            (accept-process-output nil 0.001))
          (setq done? t)
          (cond
           ((eq resp-result :finished) nil)
           (resp-result resp-result)
           ((lsp-json-error? resp-error) (error (lsp:json-error-message resp-error)))
           ((input-pending-p) (when lsp--throw-on-input
                                (throw 'input :interrupted)))
           (t (error (lsp:json-error-message (cl-first resp-error))))))
      (unless done?
        (lsp-cancel-request-by-token :sync-request)))))
aaronjensen commented 3 years ago

@yyoncho my problem appears to be garbage collecting. It seems that LSP auto complete, regardless of whether or not it's company-lsp or company-capf triggers garbage collecting every other character that's typed pretty much.

So, I was mistaken when I said it worked with company-lsp, I apologize for that.

One thing of note is that I too am on native-comp, so perhaps it has something to do with that?

@RomeuG if you try setting (setq gc-cons-threshold most-positive-fixnum), does perceived performance improve?

aaronjensen commented 3 years ago

Something looks very wrong. This is a memory profile. Typing about 8 characters led to 300MB of allocation?

    305,357,575  98% - timer-event-handler
    305,357,575  98%  - apply
    305,317,487  98%   - company-idle-begin
    300,309,882  96%    - company-auto-begin
    300,309,882  96%     - company--perform
    300,309,882  96%      - company--begin-new
    300,252,930  96%       - company-calculate-candidates
    300,239,238  96%        - company--fetch-candidates
    300,239,238  96%         - company-call-backend-raw
    300,239,238  96%          - apply
    300,239,238  96%           - company-capf
    300,239,238  96%            - company-capf--candidates
    300,239,238  96%             - completion-all-completions
    300,239,238  96%              - completion--nth-completion
    300,239,238  96%               - completion--some
    300,239,238  96%                - #<compiled -0x74f3aa7d67826e2>
    300,040,086  96%                 - completion-basic-all-completions
    300,040,086  96%                  - completion-pcm--all-completions
    300,039,062  96%                   - #<lambda -0x628160d5b0eba8a>
    300,039,062  96%                    - cond
    300,039,062  96%                     - funcall
    300,039,062  96%                      - #<lambda -0xb1b3cf7f5c39fe8>
    300,039,062  96%                       - let
    300,039,062  96%                        - catch
    300,039,062  96%                         - let
    300,039,062  96%                          - cond
    300,033,154  96%                           - let*
    300,009,662  96%                            - lsp-request-while-no-input
    299,980,654  96%                             - let*
    299,980,654  96%                              - unwind-protect
    299,980,654  96%                               - progn
    299,949,194  96%                                - while
    127,830,914  41%                                 - accept-process-output
    127,830,914  41%                                  - #<lambda 0xedc1b7bb7a5c80c>
     85,408,840  27%                                   - while
     85,408,840  27%                                    - if
     85,408,840  27%                                     - let*
     85,401,672  27%                                      + if
          7,168   0%                                      + and
     42,422,074  13%                                   + setq
kiennq commented 3 years ago

@aaronjensen I don't think you should set the gc-cons-threshold to very high value. Depends on language server, the json message passing around can be huge and it can quickly reach very high memory usage. Which in turn will freeze Emacs forever for garbage collecting. I'm setting (setq gc-cons-threshold #x8000000) and pretty comfy with that.

Can you try with https://github.com/emacs-lsp/lsp-mode/pull/2176 and take the profiling again? The accept-process-output can be anything, even reacting to response from other request, not necessarily because of completion. This commit is separating that.

yyoncho commented 3 years ago

Also, can you include memory profile?

Edit: ignore, previous profile was memory profile.

aaronjensen commented 3 years ago

@kiennq I know I shouldn't, I only mentioned that as an experiment. I'm currently trying out 1GB along w/ gcmh which will GC on idle. This at least allows me to type without constant stutters.

Here's the memory profile with that patch:

28 native-comp ``` 317,603,912 78% - company-post-command 261,526,331 64% - company--perform 261,473,987 64% - company--continue 261,357,275 64% - company-calculate-candidates 258,686,627 63% - company--fetch-candidates 258,684,515 63% - company-call-backend-raw 258,684,515 63% - apply 258,684,515 63% - company-capf 258,676,275 63% - company-capf--candidates 258,674,163 63% - completion-all-completions 258,673,107 63% - completion--nth-completion 258,668,963 63% - completion--some 258,668,963 63% - # 258,665,835 63% - completion-basic-all-completions 255,493,787 63% - completion-pcm--all-completions 255,493,787 63% - all-completions 255,489,563 63% - # 255,489,563 63% - cond 255,489,563 63% - funcall 255,489,563 63% - # 255,488,507 63% - let 255,487,451 63% - catch 255,487,451 63% - let 255,486,403 63% - cond 243,768,203 60% - let* 237,650,499 58% - lsp-request-while-no-input 237,563,299 58% - let* 237,563,299 58% - unwind-protect 237,563,299 58% - progn 237,383,791 58% - while 94,557,683 23% - accept-process-output 51,385,772 12% - # 35,546,260 8% - setq 28,312,740 6% concat 15,839,512 3% - while 15,839,512 3% - if 15,833,176 3% - let* 15,819,864 3% + if 13,312 0% + and 43,169,799 10% - timer-event-handler 43,169,799 10% - apply 43,169,799 10% - # 43,168,743 10% - lsp--parser-on-message 43,168,743 10% - condition-case 39,639 0% + let 178,452 0% + lsp-request-async 85,088 0% + plist-put 5,468,264 1% + if 649,440 0% + progn 11,718,200 2% + setq 1,048 0% + and 1,056 0% if 3,166,776 0% + completion-hilit-commonality 3,168 0% completion-boundaries 2,080 0% + completion-pcm-all-completions 2,112 0% completion-metadata 2,112 0% + company-require-match-p 1,757,452 0% + company--preprocess-candidates 913,196 0% + company--postprocess-candidates 46,224 0% + company-call-backend 39,773 0% + company--continue-failed 19,227 0% + company-cancel 8,320 0% company-update-candidates 51,288 0% + company-call-frontends 56,064,893 13% + company-call-frontends 10,576 0% + run-with-timer 55,425,763 13% - timer-event-handler 55,422,987 13% - apply 31,313,920 7% - # 31,310,752 7% - lsp--parser-on-message 31,310,752 7% - condition-case 896,448 0% - let 891,168 0% - let* 889,056 0% + let* 1,056 0% let 24,064,236 5% - company-idle-begin 19,482,436 4% - company-post-command 19,482,436 4% - company-call-frontends 19,482,220 4% - company-pseudo-tooltip-frontend 19,482,220 4% - apply 19,481,164 4% - # 19,481,164 4% - apply 19,481,164 4% - company-posframe-frontend 19,409,340 4% - company-posframe-show 18,514,320 4% - apply 18,513,264 4% - posframe-show 18,513,264 4% - apply 18,506,928 4% - # 17,712,448 4% - posframe--set-frame-size 17,712,448 4% - posframe--fit-frame-to-buffer 17,710,336 4% fit-frame-to-buffer-1 360,880 0% - posframe-run-poshandler 360,880 0% company-posframe-show-at-prefix 7,392 0% default-font-width 691,788 0% + company--create-lines 216 0% + company-echo-metadata-frontend 4,545,808 1% + company-auto-begin 35,992 0% + version< 40,251 0% + flycheck--handle-idle-trigger 2,896 0% + company-echo-show 1,604 0% + lsp--on-idle 80 0% + # 2,520 0% + timer-activate 184 0% + timer-inc-time ```

And here's a profile on Emacs 28 master w/o native-comp (also with that patch). I typed more on the above profile, which is probably why the memory usage is higher.

28 w/o native-comp ``` 151,066,851 74% - timer-event-handler 151,063,371 74% - apply 113,143,136 55% - company-idle-begin 101,654,504 50% - company-auto-begin 101,654,504 50% - company--perform 101,653,448 50% - company--begin-new 101,628,344 50% - company-calculate-candidates 101,572,536 50% - company--fetch-candidates 101,572,536 50% - company-call-backend-raw 101,572,536 50% - apply 101,572,536 50% - company-capf 101,572,536 50% - company-capf--candidates 101,572,536 50% - completion-all-completions 101,572,536 50% - completion--nth-completion 101,571,480 50% - completion--some 101,571,480 50% - # 100,747,976 49% - completion-pcm-all-completions 100,746,920 49% - completion-pcm--find-all-completions 100,745,896 49% - completion-pcm--all-completions 100,745,896 49% - all-completions 100,745,896 49% - # 100,745,896 49% - cond 100,745,896 49% - funcall 100,745,896 49% - # 100,745,896 49% - let 100,745,896 49% - catch 100,745,896 49% - let 100,745,896 49% - cond 100,745,896 49% - let* 100,745,896 49% - lsp-request-while-no-input 100,745,896 49% - let* 100,745,896 49% - unwind-protect 100,745,896 49% - progn 100,641,848 49% - while 49,128,392 24% - accept-process-output 39,860,584 19% - # 26,722,068 13% - while 26,722,068 13% - if 26,716,788 13% - let* 26,705,436 13% - if 26,705,436 13% - progn 26,345,468 12% - let 1,878,800 0% - decode-coding-string 1,878,800 0% apply 1,176 0% + run-at-time 359,968 0% + setq 9,240 0% + and 2,112 0% setq 13,138,516 6% + setq 9,265,696 4% + timer-event-handler 100,880 0% + lsp-request-async 2,112 0% + cond 1,024 0% completion-pcm--string->pattern 821,408 0% + completion-basic-all-completions 1,056 0% completion--styles 54,752 0% + company--postprocess-candidates 1,056 0% company--preprocess-candidates 11,376 0% + company-call-frontends 9,504 0% + company-call-backend 1,056 0% company-update-candidates 11,471,176 5% + company-post-command 17,456 0% + version< 33,856,108 16% - # 33,852,940 16% - lsp--parser-on-message 33,852,940 16% - condition-case 950,220 0% - let 948,108 0% - let* 935,436 0% - let* 930,156 0% - cond 930,156 0% - let 923,820 0% - let 913,260 0% - lsp--on-notification 911,148 0% - let* 910,092 0% - let 910,092 0% - let* 909,036 0% - let* 909,036 0% - if 909,036 0% - funcall 906,924 0% - lsp--on-diagnostics 694,406 0% - lsp--on-diagnostics-update-stats 690,182 0% - let* 689,126 0% - let 284,782 0% - lsp--fix-path-casing 284,782 0% - lsp--uri-to-path 282,670 0% - let* 280,558 0% - if 280,558 0% - lsp--uri-to-path-1 277,390 0% - let* 213,061 0% - url-generic-parse-url 26,445 0% generate-new-buffer 19,456 0% url-unhex-string 4,224 0% # 5,264 0% + lsp-remap-path-if-needed 2,112 0% + and 262,016 0% + while 131,768 0% + lsp-diagnostics--update-path 4,224 0% + let* 3,168 0% + mapc 206,182 0% + let* 1,056 0% lsp--log-notification-performance 3,168 0% + let* 3,168 0% + lsp--get-message-type 3,539,606 1% + flycheck--handle-idle-trigger 321,262 0% + company-echo-show 171,431 0% + # 18,749 0% + # 7,591 0% + # 2,080 0% + +popup--kill-buffer 1,296 0% + flycheck-display-error-at-point 1,056 0% + auto-revert-buffers 2,112 0% + timer-activate-when-idle 1,272 0% + timer-activate 72 0% + timer-inc-time ```
kiennq commented 3 years ago

Can you also post cpu profile too?

yyoncho commented 3 years ago

@aaronjensen is this a public project?

yyoncho commented 3 years ago

@aaronjensen is this a public project?

I was able to reproduce the issue - I will investigate where the issue comes from.

yyoncho commented 3 years ago

I am receiving 1500 $progress notifications when performing 1 completion...

RomeuG commented 3 years ago

@aaronjensen

It definitely improves. I still did notice some hiccups here and there, but not as bad like before.

yyoncho commented 3 years ago

can you test - https://github.com/yyoncho/lsp-mode/commit/24bc84554d4a3509d0c79804f234872af868de91 ?

aaronjensen commented 3 years ago

That did not appear to change anything for me.

memory profile ``` 472,111,521 73% - company-post-command 400,659,175 62% - company--perform 400,609,927 62% - company--continue 400,318,424 62% - company-calculate-candidates 397,768,752 62% - company--fetch-candidates 397,767,696 62% - company-call-backend-raw 397,767,696 62% - apply 397,767,696 62% - company-capf 397,751,192 62% - company-capf--candidates 397,746,968 62% - completion-all-completions 397,739,656 62% - completion--nth-completion 397,737,544 62% - completion--some 397,737,544 62% - # 209,741,728 32% - completion-basic-all-completions 200,264,904 31% - completion-pcm--all-completions 200,256,456 31% - # 200,256,456 31% - cond 200,256,456 31% - funcall 200,256,456 31% - # 200,255,400 31% - let 200,252,232 31% - catch 200,248,008 31% - let 200,244,864 31% - cond 171,596,352 26% - let* 166,604,456 25% - lsp-request-while-no-input 166,481,568 25% - let* 166,481,568 25% - unwind-protect 166,481,568 25% - progn 166,469,952 25% while 8,448 0% lsp-request-async 121,832 0% + plist-put 4,987,672 0% + progn 1,056 0% lsp-completion--clear-cache 1,056 0% + or 1,056 0% if 28,647,456 4% + setq 3,144 0% + and 3,168 0% if 9,293,744 1% + completion-hilit-commonality 1,056 0% completion-boundaries 187,987,400 29% - completion-pcm-all-completions 187,987,400 29% - completion-pcm--find-all-completions 187,986,376 29% - completion-pcm--all-completions 187,986,376 29% - # 187,986,376 29% - cond 187,986,376 29% - funcall 187,986,376 29% - # 187,986,376 29% - let 187,986,376 29% - catch 187,986,376 29% - let 187,986,376 29% - cond 187,985,320 29% - let* 163,624,392 25% - lsp-request-while-no-input 163,624,392 25% - let* 163,624,392 25% - unwind-protect 163,624,392 25% - progn 163,420,128 25% - while 163,420,128 25% - accept-process-output 100,232,232 15% - # 57,347,168 8% - while 57,347,168 8% - if 57,326,048 8% - let* 57,313,664 8% + if 9,216 0% - and 9,216 0% string-match-p 3,168 0% setq 42,885,064 6% + setq 63,176,280 9% + timer-event-handler 202,152 0% + lsp-request-async 1,056 0% + cond 24,359,872 3% + if 1,056 0% + setq 4,224 0% + completion-emacs22-all-completions 4,224 0% completion-metadata 2,284,832 0% + company--preprocess-candidates 181,416 0% + company--postprocess-candidates 160,472 0% + company-call-backend 64,356 0% + company--continue-failed 48,787 0% + company-cancel 14,768 0% + company-update-candidates 48,192 0% + company-call-frontends 71,418,674 11% - company-call-frontends 71,401,818 11% - company-pseudo-tooltip-frontend 71,401,818 11% - apply 71,398,650 11% - # 71,398,650 11% - apply 71,397,594 11% - company-posframe-frontend 69,081,490 10% - company-posframe-show 63,414,276 9% - posframe-show 63,408,996 9% - apply 60,898,324 9% - # 55,171,080 8% - posframe--set-frame-size 55,171,080 8% - posframe--fit-frame-to-buffer 4,675,308 0% - fit-frame-to-buffer-1 1,090,240 0% - redisplay_internal (C function) 596,544 0% + eval 149,944 0% + nano-modeline-update-windows 147,816 0% + which-key--hide-popup-on-frame-size-change 96,912 0% + window--adjust-process-windows 6,336 0% + mode-line-default-help-echo 5,280 0% + window--sanitize-window-sizes 2,112 0% + # 1,056 0% evil-refresh-cursor 624,808 0% + window-default-line-height 2,112 0% + window--sanitize-window-sizes 3,637,026 0% + posframe-run-poshandler 1,416,410 0% + eval 3,168 0% + aj--fix-posframe-fringe 4,844,686 0% + company--create-lines 16,856 0% + company-echo-metadata-frontend 28,392 0% + run-with-timer 1,056 0% + company--should-begin 122,897,534 19% + timer-event-handler 28,721,992 4% - # 19,992,952 3% - setq 16,059,488 2% concat 8,729,040 1% + while 9,329,282 1% + command-execute 3,254,060 0% + ... 2,187,058 0% + redisplay_internal (C function) 1,619,728 0% posframe-run-hidehandler 432,000 0% + company-pre-command 332,704 0% + web-mode-on-post-command 168,176 0% + winner-save-old-configurations 113,888 0% + lsp-ui-sideline 103,392 0% + gcmh-register-idle-gc 57,504 0% + lsp--post-command 33,208 0% + flycheck-perform-deferred-syntax-check 14,616 0% + evil-repeat-post-hook 11,080 0% + flycheck-maybe-display-error-at-point-soon 8,448 0% + which-key--hide-popup 7,800 0% + # 5,200 0% + yas--post-command-handler 4,656 0% + # 3,144 0% evil-repeat-pre-hook 1,056 0% + eldoc-schedule-timer ```
aaronjensen commented 3 years ago

One oddity... Looking at the lsp-io logs, I see that when the completion info comes back for me typing "Re" it includes essentially the entire dictionary of top level symbols. Furthermore, because I have date-fns as a dependency and the way that TypeScript appears to resolve autocompletes, it includes each function multiple times.

Furthermore, it appears to send responses even for canceled requests, though I don't know if lsp is parsing them.

Essentially, a tremendous amount of traffic is being generated. I imagine all of that is turned into Lisp strings and parsed, which creates a ton of objects to GC.

yyoncho commented 3 years ago

That did not appear to change anything for me.

the comment was for rust-analyzer issues. Is your project public? We need to measure the size of the ts-ls response. if it is too much, the only option seems to be to ask the server devs to start returning partial results. As a side note, I believe that if you compile lsp-mode with lsp-use-plists = t, the gc pressure will be decreased.

aaronjensen commented 3 years ago

No, the project is not public, but this minimal react app reproduces it: https://github.com/aaronjensen/lsp-completion-repro

Run npm install or yarn install to install packages Set company-idle-delay to 0 You can also set company-minimum-prefix-length to 1 Open App.tsx

Above the App declaration, type Rea

It doesn't grow as quickly as our app does because our app has more dependencies and more top level declarations, but this gives you an idea.

Should lsp-mode be caching these results at all? It seems like the completion results are identical regardless of what I type.

Either that, or can ts-ls be convinced to filter the results based on the prefix?

yyoncho commented 3 years ago

Should lsp-mode be caching these results at all?

Results are context-dependent

Either that, or can ts-ls be convinced to filter the results based on the prefix?

Other servers tend to filter top-level results and return partial results which involve server-side filtering.

aaronjensen commented 3 years ago

I did some testing in VS Code and it seems like they get the same results (that is, every single possible completion assuming no prefix). They do not, however, re-request completion when the prefix changes. If you go somewhere else in the file and start typing there, it re-requests.

Results are context-dependent

Understood, though for top level variables, that context with ts-ls is the global context and that's the same regardless of what you type. They do not filter, so the filtering must be done on the client side and lsp-mode should not re-request completions for every character typed. This works as expected if you wait long enough for the results to come back and be parsed. If, however, typing interrupts lsp's capf, it cancels the request and starts again. Because the response still comes back, you get flooded with request.

Ideally, lsp would not cancel the request when typing interrupts (just cancel waiting for it) and still allow caching its result. I'm not sure exactly how that would work, but it does appear that the problem is completions getting triggered that do not complete building up.

aaronjensen commented 3 years ago

Oh, and I tested lsp-use-plists t and it still ate memory quickly, but I can't say whether or not it slightly decreased memory usage.

yyoncho commented 3 years ago

Ideally, lsp would not cancel the request when typing interrupts (just cancel waiting for it) and still allow caching its result. I'm not sure exactly how that would work, but it does appear that the problem is completions getting triggered that do not complete building up.

We cannot do that because we don't know if the initial request was partial or not.

aaronjensen commented 3 years ago

we don't know if the initial request was partial or not

What do you mean by partial?

kiennq commented 3 years ago

Can you take an io trace when typing? lsp-mode should do the cache automatically if the server is saying it has sent all data. Also can you check the value of your lsp-completion-no-cache?

yyoncho commented 3 years ago

we don't know if the initial request was partial or not

What do you mean by partial?

The initial result might be partial and not containing all of the result items, thus we have to make another request. If we wait for the first result without making a second we will have to make another call but with a redundant delay.

I am testing with your project, and I don't see any huge delay also the responses are pretty small, ~1mb. Can you eval the following:

(defun lsp--get-body-length (headers)

  (let ((content-length (cdr (assoc "Content-Length" headers))))
    (if content-length
    (progn
      (message "%s >>> " content-length)
        (string-to-number content-length))

      ;; This usually means either the server or our parser is
      ;; screwed up with a previous Content-Length
      (error "No Content-Length header"))))

This will give us an idea of how big are the responses?

yyoncho commented 3 years ago

Can you take an io trace when typing? lsp-mode should do the cache automatically if the server is saying it has sent all data. Also can you check the value of your lsp-completion-no-cache?

@kiennq the issue is that the server was unable to respond in time, and we make a second request then we receive the first response and we have to parse it because there is no way to know that the request is canceled.

aaronjensen commented 3 years ago
lsp--get-body-length
346 >>>  [2 times]
401 >>> 
1122241 >>> 
346 >>> 
571645 >>> 
297748 >>> 
230289 >>> 
215595 >>> 
214210 >>> 
346 >>> 
508 >>> 

That's evalling, then holding R until it typed 6 times.

I have no idea why memory usage is growing so quickly compared to the payload sizes.

yyoncho commented 3 years ago

what is your gc-cons-threshold? These values are not that big.

aaronjensen commented 3 years ago

When this started it was 16MB. But as the memory profile shows, memory allocation grows to 100s of MB after typing just a few characters. Furthermore, a GC is triggered almost every other character typed. The payload sizes aren't massive, but something is causing them to allocate 100s of MB.

yyoncho commented 3 years ago

I tested with emacs 28 from yesterday with native comp and also with emacs 27.1, using the config from https://emacs-lsp.github.io/lsp-mode/tutorials/reactjs-tutorial/ and I didn't saw any gc spikes. Can you try to collect memory allocation from that scenario(holding 6 times R) so I can try to compare it with the one on my side.

aaronjensen commented 3 years ago

This is with my config.

profile ``` 73,558,902 81% - company-post-command 72,398,801 79% - company--perform 72,394,641 79% - company--continue 72,327,879 79% - company-calculate-candidates 71,770,062 79% - company--fetch-candidates 71,769,006 79% - company-call-backend-raw 71,769,006 79% - apply 71,769,006 79% - company-capf 71,769,006 79% - company-capf--candidates 71,769,006 79% - completion-all-completions 71,769,006 79% - completion--nth-completion 71,769,006 79% - completion--some 71,769,006 79% - # 71,764,846 79% - completion-basic-all-completions 68,887,414 76% - completion-pcm--all-completions 68,887,414 76% - # 68,887,414 76% - cond 68,887,414 76% - funcall 68,887,414 76% - # 68,886,358 76% - let 68,886,358 76% - catch 68,886,358 76% - let 68,886,358 76% - cond 66,798,578 73% - let* 63,794,082 70% - lsp-request-while-no-input 63,788,802 70% - let* 63,788,802 70% - unwind-protect 63,788,802 70% - progn 63,763,682 70% - while 21,431,166 23% - accept-process-output 16,345,944 18% - # 11,221,408 12% - setq 8,996,672 9% concat 5,124,536 5% + while 5,083,110 5% + timer-event-handler 20,896 0% + lsp-request-async 2,112 0% + if 1,056 0% + cond 5,280 0% + plist-put 2,623,280 2% + if 380,160 0% + progn 2,087,780 2% + setq 2,830,968 3% + completion-hilit-commonality 2,112 0% + completion-emacs22-all-completions 2,048 0% + completion-pcm-all-completions 477,649 0% + company--preprocess-candidates 46,376 0% + company--postprocess-candidates 35,634 0% + company--continue-failed 31,128 0% + company-call-backend 3,104 0% + company-call-frontends 1,156,645 1% - company-call-frontends 1,156,597 1% + company-pseudo-tooltip-frontend 48 0% + company-echo-metadata-frontend 2,400 0% + run-with-timer 14,436,138 15% - timer-event-handler 14,433,874 15% - apply 9,205,649 10% - company-idle-begin 7,569,689 8% - company-post-command 7,569,689 8% - company-call-frontends 7,569,641 8% - company-pseudo-tooltip-frontend 7,569,641 8% - apply 7,569,641 8% - # 7,569,641 8% - apply 7,569,641 8% - company-posframe-frontend 7,359,961 8% - company-posframe-show 6,949,634 7% - posframe-show 6,949,634 7% - apply 6,839,570 7% - # 6,660,114 7% - posframe--set-frame-size 6,660,114 7% - posframe--fit-frame-to-buffer 178,668 0% - fit-frame-to-buffer-1 8,448 0% window-default-line-height 4,224 0% redisplay_internal (C function) 114,400 0% + posframe-run-poshandler 9,608 0% + eval 332,855 0% + company--create-lines 48 0% + company-echo-metadata-frontend 1,627,720 1% + company-auto-begin 8,240 0% version< 5,103,122 5% + # 62,181 0% + # 30,842 0% + flycheck--handle-idle-trigger 29,804 0% + lsp--on-idle 1,056 0% + auto-revert-buffers 1,024 0% + # 196 0% + company-echo-show 2,112 0% + timer-activate-when-idle 72 0% + timer-activate 56 0% timer-inc-time 2,093,855 2% + command-execute 251,248 0% + redisplay_internal (C function) 143,592 0% posframe-run-hidehandler 39,910 0% + # 30,592 0% + company-pre-command 24,960 0% + web-mode-on-post-command 8,184 0% evil-repeat-pre-hook 7,520 0% + gcmh-register-idle-gc 7,392 0% + lsp-ui-sideline 7,312 0% + winner-save-old-configurations 3,264 0% + lsp--post-command 1,056 0% + eldoc-schedule-timer 1,048 0% evil-repeat-post-hook 480 0% flycheck-maybe-display-error-at-point-soon 0 0% ... ```

Unfortunately I'm not able to get the tutorial configuration working right away due to path/node installation issues.

For your test, please make sure you have company-mode enabled and:

(setq company-minimum-prefix-length 1
      company-idle-delay 0)
aaronjensen commented 3 years ago

Also, that config defaults to gc cons threshold of 100MB, which may be enough to prevent stutters when typing normally in this scenario.

Here's the profile from the config you linked to:

profile ``` timer-event-handler 38,058,784 54% - apply 38,006,126 54% - company-idle-begin 37,734,026 54% - company-auto-begin 37,734,026 54% - company--perform 37,734,026 54% - company--begin-new 37,717,842 54% - company-calculate-candidates 37,300,844 53% - company--fetch-candidates 37,300,844 53% - company-call-backend-raw 37,300,844 53% - apply 37,300,844 53% - company-capf 37,300,844 53% - company-capf--candidates 37,300,844 53% - completion-all-completions 37,300,844 53% - completion--nth-completion 37,300,844 53% - completion--some 37,300,844 53% - # 37,279,016 53% - completion-basic-all-completions 36,371,112 52% - completion-pcm--all-completions 36,371,112 52% - # 36,371,112 52% - # 32,158,788 46% - lsp-request-while-no-input 32,158,788 46% - let* 32,158,788 46% - unwind-protect 32,158,788 46% - progn 32,157,788 46% - while 11,010,804 15% - accept-process-output 11,010,804 15% - # 5,637,196 8% - setq 4,514,356 6% concat 5,373,608 7% - while 5,373,608 7% - if 5,373,608 7% - let* 5,373,608 7% - if 5,373,608 7% - progn 5,307,592 7% - let 2,997,512 4% - lsp--parser-on-message 2,997,512 4% condition-case 1,122,272 1% - decode-coding-string 1,122,272 1% apply 66,016 0% + setq 1,000 0% + lsp-request-async 1,882,576 2% + -map 1,341,724 1% + lsp-completion--filter-candidates 920,472 1% + apply 33,776 0% lsp-completion--to-internal 877,408 1% + completion-hilit-commonality 13,856 0% + completion-pcm-all-completions 6,924 0% + completion-emacs22-all-completions 416,998 0% + company--preprocess-candidates 15,136 0% + company-call-backend 269,028 0% + company-post-command 3,072 0% version< 48,064 0% + lsp--on-idle 4,304 0% + flycheck--handle-idle-trigger 192 0% + blink-cursor-start 400 0% timer-inc-time 168 0% + timer-activate 29,740,477 42% + command-execute 1,308,870 1% + company-post-command 229,000 0% + ... 82,850 0% + # 42,275 0% + redisplay_internal (C function) 2,096 0% posframe-run-hidehandler 720 0% + flycheck-maybe-display-error-at-point-soon 144 0% + lsp--post-command ```
yyoncho commented 3 years ago

@aaronjensen I can confirm that memory generation is similar but with 100mb GC cons threshold I don't see big lags. I will do some investigation to see if we can reduce the memory footprint. We soon switch to plists as well.

yyoncho commented 3 years ago

@aaronjensen which is your emacs 28 commit? It might be GC perf regression.

aaronjensen commented 3 years ago

Is there a known regression? I don’t have the commit handy but it’s within the last month.

GC only takes about 250ms but that’s super noticeable when typing and combined with the 60ms parse time(s).

I’m using 1gb gc cons threshold for now and I’ll see how that goes, but yeah improvements to memory consumption would be great.

yyoncho commented 3 years ago

@aaronjensen I am working on this, there is POC at https://github.com/yyoncho/lsp-mode/tree/perf

aaronjensen commented 3 years ago

That's great, thank you. Let me know when you have something you'd like me to test and I'd be happy to.

yyoncho commented 3 years ago

@aaronjensen you may try that perf branch with lsp-use-plists = t.(ignore the memory reports because although they report more memory usage it does not go to GC).

yyoncho commented 3 years ago

@aaronjensen as a side note - this might be related to the perceived lag - https://github.com/emacs-lsp/lsp-mode/issues/2758#issuecomment-817258467

The issue is that typing looks laggy but in fact, it is not actual lag but missing redisplay call and the ui does not represent the actual state.