rust-glancer:一个专注于低内存使用的 Rust 替代 LSP

Lobsters Hottest 工具

摘要

介绍 rust-glancer,一个专注于低内存使用的 Rust 替代语言服务器协议实现,特色是重启后立即索引以及在老硬件上更好的性能。

<p><a href="https://lobste.rs/s/am0xtj/rust_glancer_alternative_lsp_for_rust">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/21 14:56

# 你好,世界! · Rust Glancer 来源:https://rust-glancer.github.io/blog/hello-world/ 我想介绍一个过去四个月一直在开发的项目:一个替代的 Rust LSP 实现,专注于低内存使用。它有两个主要特点: - 它可以使用非常少的内存(目标是对于合理规模的项目 <100mb)。有一些注意事项,将在下面描述。 - 它允许重启后立即索引:如果你的项目已被索引,重启编辑器将无需重新索引。你的浏览器不支持嵌入视频。你可以下载录屏文件(https://rust-glancer.github.io/assets/2026-08-19-hello-world/recording.mp4)*注意:在整个视频中,使用的 RAM 保持在 100mb 以下* Rust Glancer 内存使用量保持在 100 MB 以下 这些特点使得 Rust Glancer 适合老旧计算机:我在我的旧 MacBook Pro M1 2020(8GB RAM)上测试过,表现相当不错。 机器 LSP 基础索引(引擎可用)完整索引 MacBook Pro M4 Max, 36GB (2025) Rust Glancer 5秒 8秒 MacBook Pro M4 Max, 36GB (2025) rust-analyzer 6秒 13秒 MacBook Pro M1, 8GB (2020) Rust Glancer 6秒 9秒 MacBook Pro M1, 8GB (2020) rust-analyzer 7秒 14秒 可以想象,对于像 Rust LSP 这样庞大的项目来说,四个月的时间并不充裕。Rust Glancer 还不是一个完整的 LSP,它缺少很多功能,存在一些已知错误,还有许多我想要改进的地方。但同时,它已经相当能干:它拥有完整的索引管线,支持类型推导和 trait 求解器(chalk),支持大部分“常规”Rust 语法,大部分“常规”LSP 操作也能工作:跳转到定义、悬停提示、内联提示、补全等等,不一而足。如果你感兴趣,现在就可以试用:只需在此处安装 VS Code 扩展(https://marketplace.visualstudio.com/items?itemName=rust-glancer.rust-glancer),或者,如果你愿意,从仓库(https://github.com/rust-glancer/rust-glancer/)构建并安装 vsix。本文其余部分包含项目的历史:动机、LLM 使用、计划和路线图。如果你不感兴趣,可能更想查看项目文档(https://rust-glancer.github.io/docs)。 ## 与 rust-analyzer 的区别 rust-analyzer 消耗大量内存有几个原因: 1. Rust 工作区确实包含大量必须索引的信息:成千上万的函数、结构体、trait、这些之间的关系、函数体和其中的语句等。每一个都需要被分析和记忆,如果你想实现“查找所有对此结构的引用”之类的功能,你无法真正取巧。 2. rust-analyzer 使用 salsa(https://salsa-rs.github.io/salsa/)作为其数据库。它是一个基于增量查询的数据库,可以惰性计算你所需的所有数据,而无需显式“记录”一切。这是一个非常酷的方法,但它本质上与内存绑定,使得将部分数据从内存移到其他地方变得困难。 3. rust-analyzer 使用 rowan(https://github.com/rust-analyzer/rowan)进行语法树表示。这里有一个很酷的特性是它允许部分失效:如果文件只有一部分发生更改,则只需重新解析相关部分,这使得它比每次按键都重新解析整个文件更快。然而,其内部的树状表示可能导致严重的内存碎片(意味着从操作系统获取的 RAM 量高于“实际使用”的 RAM 量)。 (1) 是我们必须接受的(尽管 Rust Glancer 做了一些优化),但 (2) 和 (3) 是 rust-analyzer 架构带来的后果。rust-analyzer 选择它们是为了让 LSP 更快,而这确实达到了目的。我开始这个项目时的想法是:如果我们*不*尝试构建增量 LSP 会怎样?如果我们得到的只是一个冻结的分析结果,只在保存时使其失效呢?显然它不会像 rust-analyzer 那样快,但它会给我们寻求的特性: 1. 分析结果可以卸载到文件系统,只在实际需要时加载到内存中。 2. 保存的分析是可重用的,由于它已经被卸载到文件系统,因此可以在编辑器重启后重用。 这就是 Rust Glancer 的核心思想。它为工作区建立一次索引,并将结果保存在文件系统中,然后当查询需要某些信息时,可以在查询期间加载所需的信息。不过这并非没有代价:冻结的工作区分析天生就比惰性增量分析慢,因为从文件系统加载和反序列化数据比从内存加载慢。为了缓解这个问题,Rust Glancer 必须使用一些技巧:例如,当你输入时,它不会在每次按键时执行完整的分析,而是尝试对当前函数体进行浅层分析,并重用*之前*的完整索引。这使得补全操作相当快,但也意味着新项(导入、结构体、trait)在你保存文档之前不会被“索引”。这应该不是什么问题:你很快就能习惯,至少在我的案例中,过一会儿也不会觉得太不妥。如果这听起来很吓人,我建议你直接试试看,其实没那么可怕。对于依赖代理工作流的人员,Rust Glancer 也针对大量的编辑器外更改进行了优化。我不确定原因,但在 rust-analyzer 中我观察到,当代理编辑代码时,内联提示可能会错位,我在 Rust Glancer 中最初也遇到了同样的问题,但通过实现一个自定义文件监视器并稍作调整得以解决。服务器对编辑器外的更改也设置了较低的优先级,因此代理更改不会导致快速重新索引。不过,重要的是要理解 Rust Glancer 相较于 rust-analyzer 有一些优势,但也有一些缺点(显然除了不完整之外)。也许我最终能解决其中一些问题,但 Rust Glancer 几乎不可能变得“就像 rust-analyzer,但更好”。我想 rust-analyzer 仍将是那些关心完整性和按键准确性的项目的默认选择,而 Rust Glancer 则适用于那些机器配置较低*或者*愿意为减少内存使用而做出一些妥协的人。 ## 如何以及为何发生 我专业编写 Rust 已有约 7 年时间,从很早开始,我就观察编译器及其工具链的开发过程。我为 rustc、clippy 和 rust-analyzer 做过一些贡献,也花了几十个小时阅读其源码进行自学。所以我相当清楚 Rust LSP 项目是*多么庞大*。同时,我对 rust-analyzer 的感情爱恨交织。它除了两点之外绝对堪称完美:内存使用和初始索引(特别是在启用构建脚本/过程宏时)。这些问题似乎经常被提起,但对我来说更为严重:我有一个相当愚蠢的工作流,我在两个显示器上打开两个相同的 IDE,里面包含一堆工作区项目。所以内存消耗大约是 2N,而在我上一组需要处理的项目中,rust-analyzer 消耗了 16GB 内存,我,呃,更愿意将这些内存用于其他用途;更不用说每次我打开 VS Code 时,由于大量并行的索引任务,我的电脑风扇就会呼呼作响。某个时候,我想到我对 Rust 的知识已经足够自信,可能不需要一个功能完整的 LSP,可以使用一些更简单、更节省内存的东西。我决定尝试为 Rust 构建一个“智能 ctags”。我非常明确地不想构建替代的 LSP,因为那任务太疯狂了。我最初并不知道…… 初始进展相当顺利:我利用了 rust-analyzer 的语法库,将项降级到内部表示,然后构建定义映射和模块结构,完成了所有声明的索引。这过程出奇地直接,于是我决定做一些原始的函数体降级。然后我决定添加非常非常简单的类型传播。接着我发现简单的类型传播没给我带来多少好处——但我已经有了这些漂亮的内联提示,所以我想更多。总的来说,我不关心复杂情况和 nightly 特性,对吧?(*对吧?……*)于是接下来是通过 impl 头部匹配进行简单的 trait 解析。你懂的,这相当令人上瘾。然而,当我决定期望以下代码也能被支持时,这种错觉破灭了: `` fn mul_by_two(vals: &[u8]) -> Vec { vals.iter().copied().map(|v| v * 2).collect() } `` 这段*代码*很简单,但要支持它我们需要: - 切片类型支持 - 闭包 / Fn traits - trait 求解 - 关联类型投影 - *一堆 nightly 的东西* 最后一项很有趣:我想避免使用 nightly,但我没想到 `std`(或一般来说 sysroot)*本身就是 nightly*。唉。所以总的来说,一个特性接一个特性,我慢慢地从“智能 ctags”迈向了“真正的 LSP”。可能,三个最大的里程碑是: 1. 声明式宏展开(我现在讨厌声明式宏了)。幸运的是,我能够重用 rust-analyzer 的大部分基础设施来实现它。 2. 合适的类型推导引擎。当我真正理解类型推导如何工作时(简而言之:我们在一个大的推导表中“链接”所有相关的类型绑定,然后尝试从所有可能的地方获取证据,提供证据的地方可以同时解决多个位置的类型),那是一个巨大的“哇哦”时刻。这可能是目前为止在这个项目中给我带来最大快乐的时刻。 3. 合适的 trait 求解引擎。我最初写道“这个项目不太可能有一个 trait 求解器”,但我*真的*想让上面提到的迭代器示例正常工作。我抵制集成 trait 求解器了一段时间,试图使用简单的技巧,比如简单的 trait 实现匹配 + 针对 `std` traits 的专门处理,但它变得越来越复杂,而且效果很差。然后我放弃了,集成了 Chalk,结果证明它比我构建的整个层次结构简单得多。不过,让 Chalk 快速运行是另一个挑战。 大概在1.5个月前,我开始将 Rust Glancer 作为日常主力工具而不是 rust-analyzer 来使用。现在,我对它的状态感到满意,可以向更广泛的受众展示了。 ## LLM 使用 这个项目是在大量使用 LLM 的情况下构建的。但它不是 vibe coded。我验证每一个拉取请求,以确保我对代码库的状态感到满意。如果你需要证明,可以检查 git 历史:它有包含 10k+ 行差异的 PR,尽管我从项目开始以来几乎每天都工作,但这些 PR 之间相隔数天。我在意代码,说实话,如果我不经常阅读代码,花 4 个月创建一个*Rust LSP*对我来说会很奇怪。我不会假装我是一个经验丰富的 LSP 开发者,代码是完美的。它处于一个我能处理的状态,但我理解在编译器工具设计方面,有些部分可能不符合惯例。代码有很多注释,我努力确保这些注释不是草率的而是有帮助的,因为*我*必须经常阅读它们;到目前为止,质量显然不如专业编写的人类文档,但在我看来,它相当有帮助且不令人讨厌。旅程的很大一部分是学习。LLM 可以是很好的领域专家,LLM 对 LSP 设计的了解比我多得多。同时,LLM 不擅长构建大型项目。因此,在开发过程中发生了多次以下循环: 1. 我构建了一些新东西。 2. LLM 的建议看起来合理,所以我采纳了。 3. 它能工作,但有些地方让我困扰。 4. 我思考设计了一段时间,发现了一个大缺陷。 5. 我与 LLM 一起修复它(有时需要一周,如果搞砸的程度特别严重的话——但搞砸得越厉害,我学到的就越多)。所以一方面,如果我要将代码所有权归咎于 LLM,我可能会抱怨:“LLM 试图让项目脱轨那么多次!11”。但由于这是*我的*代码,我认为代码可能在某些时候变糟,但随着我的学习,我会改进它。这是非常正常的软件开发流程,只是加速了。 总之,LLM 只是一个工具,是选择负责任地使用它,还是将思考外包给它,取决于个人。鉴于如今盛行的猎巫行动,我只有一个请求:不要把我贬低为一个“clanker”。这是我的代码,所以如果你认为它是糟粕,请称之为*我的*糟粕,而不是 AI 的。我接受批评,并会乐于听取反馈:我学得越多,就越能改进代码库。在我看来,我是否使用 LLM 来做到这一点并不那么重要。 ## 接下来是什么 项目已经处于一些用户可以作为日常主力工具的状态,但我对它有相当大的规划。所以在接下来的版本中,你可以期待: - 进一步的性能优化 - 更多的内存优化(主要在索引期间,加上完整索引运行后发生的一些碎片化问题,我想修复它们) - 改进的类型推导/语法支持。 - 代码操作(实现缺失的 trait 字段、自动导入等)。 - 可能的过程宏支持(我有个奇怪的想法,不需要实际代码执行,但需要一些时间来准备)。 有些功能不太可能被支持,比如通过过程宏调用支持构建脚本/过程宏(例如任何需要执行不受信任代码的功能)。我也不打算在项目当前阶段处理不必要的事情,比如迁移到新的 trait 求解器。特定的 nightly 特性等小众功能可能会推迟到项目使用稳定 Rust 达到一定程度的成熟度。此外,我在 Rust Glancer 中做了一些相当酷的小技巧,我对此有点自豪(对齐分配生命周期以减少内存碎片、引擎作为子进程模型以帮助处理内存碎片和多工作区项目、分片缓存等),所以如果人们感兴趣,我很乐意写一些博客,讲述 Rust Glancer 底层是如何工作的。文档中已经部分涵盖了这些(1 (https://rust-glancer.github.io/docs/architecture/ARCHITECTURE.html),2 (https://rust-glancer.github.io/docs/development/MEMORY.html)),如果你想立即了解一些信息的话。但无论如何,我希望这个项目现在就能对一些人有所帮助,未来能帮助更多人。

相似文章

构建一个媲美 Llama.cpp 的 Rust 推理引擎

Hacker News Top

Ferrox 是一个纯 Rust 推理引擎,可加载 GGUF 模型,并在 CPU、Metal 或 CUDA 上运行本地 LLM,提供 CLI 和兼容 OpenAI 的服务器。它的目标是在不使用任何绑定、从零编写的情况下,达到与 llama.cpp 相当的性能。