Emacs LSP performance booster
Emacs LSP performance booster
Improve the performance of lsp-mode or eglot using a wrapper executable.
Background & Prior work
(Huge thanks to @yyoncho for both maintaining lsp-mode and giving me the inspiration of this project).
According to yyoncho, the are several performance issues related to lsp-mode (mostly the same for eglot):
- Json parsing in Emacs is slow
- The server may block on sending data to emacs when the buffer is full, because Emacs is consuming the data too slowly
- Similarly, Emacs may block while attempting to send data to the server (hence blocking the Emacs UI), because the server may be busy
How this project works
This project provides a wrapper-executable around lsp server programs, to work around the above-mentioned issues:
- It converts json messages from the server at high speed directly into elisp bytecode (in text representation) for Emacs to read.
{"objs":[{"a":1},{"a":2}]} would be converted to #[0 "\301\302\300\303D\300\304D\"D\207" [:a :objs vector 1 2] 13]
* This improves the message parsing performance in Emacs by ~4x for large json objects; see benchmark result here
* Although Emacs still needs to parse the text representation and interpret it into elisp objects, the performance gain mainly comes from the following:
* Parsing (reading) elisp object is apparently better optimized and simpler in Emacs
* By using bytecode to construct objects, we can eliminate duplicated objects (e.g. the "a" json key in above example)
- It separates reading and writing into different threads and keeps pending messages in internal buffers within each thread, to avoid blocking on IO. This solves issues (2) and (3) mentioned above.
[!IMPORTANT]
At present only local lsp server programs which communicate by standard input/output can be wrapped, not servers communicating over network ports (local or remote).
How to use
Generally, what you need to do is:
- Wrap your lsp server command with this
emacs-lsp-boosterexecutable.
pyright-langserver --stdio, configure lsp-mode or eglot to run emacs-lsp-booster [flags --] pyright-langserver --stdio instead.
- Advise or update the json parsing function in
lsp-modeoreglotto parse any bytecode input seen, prior to parsing it as json.
Obtain or build emacs-lsp-booster
For Linux or Windows users, you may download the prebuilt binary from release. (The macOS binary in the release page lacks proper code signing for now.) A flake for nix users is available here.
Alternatively, you may install it from crates.io:
- Setup Rust toolchain
- Run
cargo install emacs-lsp-booster. Make sure to include the cargo bin folder in your $PATH.
Configure lsp-mode
[!NOTE]
Make sure NOT to use the native-jsonrpc custom version of Emacs
- Use plist for deserialization for lsp-mode
- Add the following code to your
init.el:
(defun lsp-booster--advice-json-parse (old-fn &rest args)
"Try to parse bytecode instead of json."
(or
(when (equal (following-char) ?#)
(let ((bytecode (read (current-buffer))))
(when (byte-code-function-p bytecode)
(funcall bytecode))))
(apply old-fn args)))
(advice-add (if (progn (require 'json)
(fboundp 'json-parse-buffer))
'json-parse-buffer
'json-read)
:around
#'lsp-booster--advice-json-parse)
(defun lsp-booster--advice-final-command (old-fn cmd &optional test?) "Prepend emacs-lsp-booster command to lsp CMD." (let ((orig-result (funcall old-fn cmd test?))) (if (and (not test?) ;; for check lsp-server-present? (not (file-remote-p default-directory)) ;; see lsp-resolve-final-command, it would add extra shell wrapper lsp-use-plists (not (functionp 'json-rpc-connection)) ;; native json-rpc (executable-find "emacs-lsp-booster")) (progn (when-let ((command-from-exec-path (executable-find (car orig-result)))) ;; resolve command from exec-path (in case not found in $PATH) (setcar orig-result command-from-exec-path)) (message "Using emacs-lsp-booster for %s!" orig-result) (cons "emacs-lsp-booster" orig-result)) orig-result))) (advice-add 'lsp-resolve-final-command :around #'lsp-booster--advice-final-command)
Done! Now try to use lsp-mode as usual.
Configure eglot
Please see https://github.com/jdtsmith/eglot-booster for information on configuring eglot. Huge thanks to @jdtsmith
How to verify it's working
- Check that
emacs-lsp-boosterprocess is running - Check the stderr buffer (e.g. for lsp-mode,
pyright::stderrbuffer; for eglot, theEGLOT (...) stderr*buffer, note the leading space); it should containemacslspboosterrelated log.
Advanced usage
Run emacs-lsp-booster --help for more options.