Skip to content
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

[HACK]: Make lsp-completion Super Fast for Large Candidate Sets! 🚀 #4737

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

eval-exec
Copy link
Contributor

@eval-exec eval-exec commented Mar 16, 2025

  • I added a new entry to CHANGELOG.md

  • I updated documentation if applicable (docs folder)

Hello,

I'm working on a Rust project and using lsp-mode's lsp-completion for code completion.

In the Rust crate nerd_font_symbols, attempting to complete nerd_font_symbols::md:: results in a very large number of candidates. I noticed that lsp-mode's lsp-completion takes 2179ms to display the code completion.

However, when I tested the same completion in Zed Editor and VS Code, they were significantly faster, taking only 0.2 seconds to show the results.

To investigate the issue, I profiled lsp-mode.el and found that the CPU profiler pointed to sit-for as a potential bottleneck.

        1402  88%  - catch
        1402  88%   - sit-for
        1402  88%    - apply
        1402  88%     - time-function-advice
        1402  88%      - let
        1402  88%       - prog1
        1402  88%        - apply
        1400  88%         - #<interpreted-function E75>
        1400  88%          - if
        1400  88%           - let*
        1400  88%            - unwind-protect
        1400  88%             - progn
        1400  88%              - while
        1400  88%               - catch
        1400  88%                - sit-for
        1400  88%                 - apply
        1400  88%                  - time-function-advice
        1400  88%                   - let
        1400  88%                    - prog1
        1400  88%                     - apply
        1400  88%                      - #<native-comp-function sit-for>
          98   6%                       - redisplay_internal (C function)
          27   1%                        - eval
          13   0%                         - header--line-format-right-align
          13   0%                          - let*
           9   0%                           - progn
           8   0%                              string-pixel-width
           1   0%                            - lsp--send-request-async
           1   0%                             - let*
           1   0%                              - and
           1   0%                               - lsp--find-workspaces-for

I don't know why, when I do the hacks on this PR, the lsp-mode.el code-completion reduce from 2179ms to 179ms ! :

         173  70% - redisplay_internal (C function)
         103  42%  - eval
          95  38%   - tab-line-format
          95  38%    - tab-line-tabs-window-buffers
          95  38%     - seq-remove
          95  38%      - seq-filter
          95  38%       - seq-map
          95  38%        - apply
          94  38%         - time-function-advice
          94  38%          - let
          94  38%           - prog1
          94  38%            - apply
          94  38%             - #<interpreted-function BC8>
          94  38%              - let*
          94  38%               - progn
          94  38%                - if
          94  38%                 - let*
          94  38%                  - unwind-protect
          94  38%                   - progn
          94  38%                    - while
          94  38%                     - if
          94  38%                      - catch
          94  38%                       - accept-process-output
          49  20%                        - #<interpreted-function DAA>
          49  20%                         - let
          45  18%                          - while
          45  18%                           - if
          45  18%                            - let*
          45  18%                             - if
          45  18%                              - progn
          45  18%                               - condition-case
          45  18%                                - let
          45  18%                                 - save-current-buffer
          45  18%                                  - unwind-protect
          45  18%                                   - progn
          19   7%                                      decode-coding-region
          16   6%                                    - setq
          16   6%                                     - cons
          16   6%                                        json-parse-buffer
          10   4%                                    - apply
          10   4%                                       insert

So I think this PR is very important, you can reproduce this by this rust code:

use nerd_font_symbols::md;
fn main(){

    // put the cursor after next line end, then trigger lsp completion to feel it.
    md::MD_
}

And I use this short function to record the lsp-completion's timecost:

(defun time-function-advice (orig-fun &rest args)
  "Advice to measure and print the execution time of a function.
ORIG-FUN is the original function.
ARGS are the arguments to pass to the original function."
  (let ((start-time (current-time))
         (func-name (format "%s" (if (symbolp orig-fun) (symbol-name orig-fun) "anonymous" ))))
    (prog1
      (apply orig-fun args)
      (let ((elapsed (float-time (time-subtract (current-time) start-time))))
        (message "Function %s took %.6f seconds" func-name elapsed)))))

(defun add-timing-to-function (func-symbol)
  "Add timing advice to the function named by FUNC-SYMBOL."
  (interactive "aFunction to time: ")
  (advice-add func-symbol :around #'time-function-advice)
  (message "Timing added to %s" func-symbol))

(defun remove-timing-from-function (func-symbol)
  "Remove timing advice from the function named by FUNC-SYMBOL."
  (interactive "aFunction to remove timing from: ")
  (advice-remove func-symbol #'time-function-advice)
  (message "Timing removed from %s" func-symbol))


(add-timing-to-function #'lsp-request)
(add-timing-to-function #'lsp-request-while-no-input)

What do you think?

@eval-exec eval-exec changed the title hack: make lsp-completion super fast [HACK]: Make lsp-completion Super Fast for Large Candidate Sets! 🚀 Mar 16, 2025
@kiennq
Copy link
Member

kiennq commented Mar 16, 2025

The reason you observed a significant amount of time spent in lsp--send-request and lsp-request-while-no-input is that they are synchronous and thus wait for a response from the LSP server. While waiting, we continue to process incoming output from the LSP server, and all of this processing contributes to the time spent in the aforementioned functions.

Changing the sit-for value does not eliminate the need to wait for the server's response. However, the silver lining is that for cases like completion, where lsp-request-while-no-input is used, the request will be canceled as soon as the user resumes typing.

The heavy processing occurs after receiving the response from the server, as we perform parsing and filtering operations in Elisp, which can be slow.

Can you try to see how long it takes for the completion-at-point to finish? Just trigger a completion and see how long it takes to finish, please also check if the shown candidates are the same. That would be more accurate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants