Improve the performance of lsp-mode or eglot using a wrapper executable.
(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):
@yyoncho tried to solve these issues by implementing a native async non-blocking jsonrpc fork of emacs. The result is very good regarding performance. However, it requires modifications in the Emacs source code and it seems unlikely that those changes could be merged upstream. Also, it could prove difficult to maintain, since it also requires a separate code path in lsp-mode (I myself encountered some issues).
This project provides a wrapper-executable around lsp server programs, to work around the above-mentioned issues:
{"objs":[{"a":1},{"a":2}]}
would be converted to #[0 "\301\302\300\303D\300\304D\"D\207" [:a :objs vector 1 2] 13]
read
ing) elisp object is apparently better optimized and simpler in EmacsOverall, this lsp server wrapper strategy achieves similar result as the native async non-blocking jsonrpc approach without requiring modifications in Emacs source code.
[!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).
Generally, what you need to do is:
emacs-lsp-booster
executable.
For example, if the original lsp server command is pyright-langserver --stdio
, configure lsp-mode or eglot to run emacs-lsp-booster [flags --] pyright-langserver --stdio
instead.lsp-mode
or eglot
to parse any bytecode input seen, prior to parsing it as json.See more detailed configuration steps below.
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 build the target locally:
cargo build --release
target/release/emacs-lsp-booster
Then, put the emacs-lsp-booster
binary in your $PATH (e.g. ~/.local/bin
).
lsp-mode
[!NOTE]
Make sure NOT to use the native-jsonrpc custom version of Emacs
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.
eglot
Please see https://github.com/jdtsmith/eglot-booster for information on configuring eglot
.
Huge thanks to @jdtsmith
emacs-lsp-booster
process is running*pyright::stderr*
buffer; for eglot, the EGLOT (...) stderr*
buffer, note the leading space); it should contain emacs_lsp_booster
related log.Run emacs-lsp-booster --help
for more options.