Emacs LSP 性能加速器
使用包装器可执行文件提高 lsp-mode 或 eglot 的性能。
背景和前期工作
(非常感谢 @yyoncho 维护 lsp-mode 并给予我这个项目的灵感)
根据 yyoncho 的说法,lsp-mode(eglot 也基本相同)存在几个性能问题:
- Emacs 中的 Json 解析速度慢
- 当缓冲区已满时,服务器可能会在向 Emacs 发送数据时阻塞,因为 Emacs 消耗数据的速度太慢
- 同样,Emacs 在尝试向服务器发送数据时可能会阻塞(从而阻塞 Emacs 用户界面),因为服务器可能正忙
@yyoncho 尝试通过实现 Emacs 的 原生异步非阻塞 jsonrpc 分支来解决这些问题。性能方面的结果非常好。然而,这需要修改 Emacs 源代码,而且这些更改似乎不太可能被合并到上游。此外,这可能难以维护,因为它还需要 lsp-mode 中的单独代码路径(我自己遇到了一些 问题)。
本项目的工作原理
本项目提供了一个 LSP 服务器程序的包装器可执行文件,以解决上述问题:
- 它以高速将服务器的 json 消息直接转换为 elisp 字节码(以文本表示形式),供 Emacs 读取。
- 例如,
{"objs":[{"a":1},{"a":2}]}
会被转换为#[0 "\301\302\300\303D\300\304D\"D\207" [:a :objs vector 1 2] 13]
- 这将大型 json 对象的消息解析性能提高了约 4 倍;请参阅 此处 的基准测试结果
- 尽管 Emacs 仍需要解析文本表示并将其解释为 elisp 对象,性能提升主要来自以下几点:
- 在 Emacs 中解析(
read
)elisp 对象显然经过了更好的优化,且更简单 - 通过使用字节码构造对象,我们可以消除重复的对象(例如上例中的 "a" json 键)
- 在 Emacs 中解析(
- 例如,
- 它将读写分离到不同的线程中,并在每个线程的内部缓冲区中保留待处理的消息,以避免 IO 阻塞。这解决了上述第 (2) 和第 (3) 个问题。
总的来说,这种 LSP 服务器包装器 策略实现了与原生异步非阻塞 jsonrpc 方法类似的结果,而无需修改 Emacs 源代码。
[!重要]
目前只能包装通过标准输入/输出通信的本地 LSP 服务器程序,不支持通过网络端口(本地或远程)通信的服务器。
如何使用
通常,您需要做的是:
- 用
emacs-lsp-booster
可执行文件包装您的 LSP 服务器命令。 例如,如果原始 LSP 服务器命令是pyright-langserver --stdio
,则将 lsp-mode 或 eglot 配置为运行emacs-lsp-booster [flags --] pyright-langserver --stdio
。 - 修改或更新
lsp-mode
或eglot
中的 json 解析函数,以在解析为 json 之前解析任何看到的字节码输入。
请参阅下面更详细的配置步骤。
获取或构建 emacs-lsp-booster
Linux 或 Windows 用户可以从 release 页面下载预构建的二进制文件。 (release 页面中的 macOS 二进制文件目前缺乏适当的代码签名。) nix 用户可以使用 这里 提供的 flake。
或者,您也可以在本地构建目标:
- 设置 Rust 工具链
- 运行
cargo build --release
- 在
target/release/emacs-lsp-booster
找到构建好的二进制文件
然后,将 emacs-lsp-booster
二进制文件放入您的 $PATH 中(例如 ~/.local/bin
)。
配置 lsp-mode
[!注意]
确保不要使用 Emacs 的 native-jsonrpc 自定义版本
- 对 lsp-mode 使用 plist 进行反序列化
- 将以下代码添加到您的
init.el
中:
(defun lsp-booster--advice-json-parse (old-fn &rest args)
"尝试解析字节码而不是 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?)
"在 lsp CMD 前添加 emacs-lsp-booster 命令。"
(let ((orig-result (funcall old-fn cmd test?)))
(if (and (not test?) ;; 用于检查 lsp-server-present?
(not (file-remote-p default-directory)) ;; 参见 lsp-resolve-final-command,它会添加额外的 shell 包装器
lsp-use-plists
(not (functionp 'json-rpc-connection)) ;; 原生 json-rpc
(executable-find "emacs-lsp-booster"))
(progn
(when-let ((command-from-exec-path (executable-find (car orig-result)))) ;; 从 exec-path 解析命令(以防在 $PATH 中未找到)
(setcar orig-result command-from-exec-path))
(message "为 %s 使用 emacs-lsp-booster!" orig-result)
(cons "emacs-lsp-booster" orig-result))
orig-result)))
(advice-add 'lsp-resolve-final-command :around #'lsp-booster--advice-final-command)
完成!现在像往常一样使用 lsp-mode。
配置 eglot
有关配置 eglot
的信息,请参阅 https://github.com/jdtsmith/eglot-booster。
非常感谢 @jdtsmith
如何验证它正在工作
- 检查
emacs-lsp-booster
进程是否正在运行 - 检查 stderr 缓冲区(例如,对于 lsp-mode,是
*pyright::stderr*
缓冲区;对于 eglot,是EGLOT (...) stderr*
缓冲区,注意前导空格);它应该包含emacs_lsp_booster
相关的日志。
高级用法
运行 emacs-lsp-booster --help
以获取更多选项。