提升IntelliJ平台IDE响应速度的技术演进之路
TL;DR: 这是一篇关于我们如何提升基于 IntelliJ 的 IDE 用户界面(UI)响应速度的技术博客。这是一项旨在修复多个架构约束的多年计划。该项目仍在进行中,到目前为止,我们已经构建了新的工具和 API,帮助将性能敏感的工作从 UI 线程中剥离出来。这一改变意味着 UI 线程持有写锁的时间大幅缩短,仅为之前的约三分之一。如果您对技术细节不感兴趣,可以直接跳到文末查看性能图表。
[LOADING...]
关于基于 IntelliJ 的 IDE,最常见的抱怨之一就是性能问题。我们深知这一点,并正在努力使 IDE 响应更灵敏。但这并非易事:IntelliJ 平台已有 25 年历史,其部分架构决策早已根深蒂固,这也使得某些优化变得十分困难。
致命的纠葛
[LOADING...]
IntelliJ 平台是一个围绕单一读写(RW)锁构建的多线程框架。IDE 在几个核心数据结构上运行:语法树 (PSI)、文件的文本视图(Document 子系统)以及操作系统文件系统的视图(虚拟文件系统 (VFS))。对这些结构的访问受到读写锁的保护。操作分为读操作和写操作:在任何时刻,只能存在一个写操作;多个读操作可以并行运行,但读操作和写操作不能同时进行。
我们的 IDE 也是 UI 应用程序,这意味着它们使用 UI 框架。在 IntelliJ 平台中,该框架是 Java AWT,它只有一个 UI 线程:事件分发线程(EDT)。该线程负责处理用户输入和绘制 UI。Java 也允许业务逻辑在此运行。EDT 的性能直接影响应用程序的响应速度:如果它能快速处理绘制事件和用户输入,IDE 就会感觉流畅。
这就是卡顿的来源。写操作本身可能会导致卡顿。某些写操作(如重新解析语法树或更新文件系统视图)本身开销就很大。另一个不那么明显的卡顿来源是等待获取写锁。由于读操作和写操作不能同时运行,启动写操作意味着必须等待所有活动的读操作完成。我们在使读操作可取消方面做了大量工作,但问题并未完全解决:只要有一个读操作“不配合”,整个 IDE 就会受到影响。
这自然促使我们确定了一个关键目标:将写操作从 UI 线程中移出。
出于好意
[LOADING...]
支持后台写操作的工作始于 2019 年,由 Valentin Fondaratov、Andrew Kozlov 和 Peter Gromov 共同发起。
长期以来,在 EDT 上运行的代码可以方便地访问 IntelliJ 平台模型。有了后台写操作,这种便利性就成了问题:UI 代码不能再假设在没有明确协调的情况下访问模型总是安全的。为了保持兼容性,我们在使这些假设显性化的同时,不得不维持大量现有 UI 代码的正常运行。
还有一个复杂之处:EDT 上的代码可以直接启动写操作。对于普通的显式读操作来说,情况并非如此,因为写操作不能简单地在读操作中间开始。
这就是“写意图”(write-intent)发挥作用的地方。写意图是一种锁状态,它仍然允许并行读操作,但在任何时候只能由一个线程持有,并且可以原子地升级为完整的写操作。这使得它非常适合那些可能需要转换为写操作的 EDT 代码。在平台中采用这种方法是支持后台写操作并保留现有行为的重要一步。
该项目在 2020 年被搁置,因为所需的改动量巨大。许多 UI 组件,尤其是编辑器,严重依赖于长期以来关于 EDT 模型访问的假设。
大重构
[LOADING...]
项目虽然暂停,但并未放弃。2022 年,Lev Serebryakov 和 Daniil Ovchinnikov 重启了这项工作。
在此阶段,我们对 IntelliJ 平台进行了重构,以显露平台之前隐含依赖的许多假设。这项工作减少了平台在 UI 驱动代码路径中对隐式锁定的依赖。
这一阶段的另一个重要部分是我们与 JetBrains 研究团队的合作。我们之前的锁实现假设写操作仅在 EDT 上运行。将其移入后台需要不同的锁,而老旧的 ReentrantReadWriteLock 并不适合我们的需求。最终产物是一个全新的可取消锁,现在驱动着整个平台(详见这篇研究论文)。
这一阶段持续到 2024 年底。
当一个锁不够用时
[LOADING...]
2025 年初,Konstantin Nisht 接手了项目的这一部分。当时,我们已经准备好运行首批后台写操作了。但还剩下一个主要问题:模态(Modality)。
IDE 中的某些 UI 元素需要阻止用户与除自身以外的任何事物进行交互。这些就是模态对话框,例如“设置”对话框。在 IntelliJ 平台中,模态也会影响模型:当模态对话框可见时,不相关的写操作不应启动。从历史上看,EDT 调度程序通过确保在非模态上下文中启动的 UI 工作在模态对话框处于活动状态时不会运行,处理了大部分此类情况。
后台写操作无法自动融入该模型。
如果模态对话框在持有写意图的同时在 EDT 上显示,那么运行后台写操作的简单尝试可能会导致死锁。同时,我们仍然希望对话框内的计算能够在不被外部不相关工作干扰的情况下取得进展。
为了解决这个问题,我们引入了一种模态感知的锁定策略,将模态对话框内发生的事情与外部发生的事情分离开来。这既保留了模态对话框所依赖的保证,又让后台写操作得以运行。
这对于嵌套模态计算也有效,这很重要,因为实际的模态工作流并不总是平铺的。有了这个机制,我们终于能够运行首批后台写操作了。
在不破坏插件的情况下迁移工作
[LOADING...]
最初的写操作相对容易移至后台。它们存在于工作区模型(Workspace Model)中,主要用于使某些缓存失效。此后,是时候尝试更重要的任务了:VFS 刷新。
VFS 刷新是将操作系统中的文件修改事件与 IDE 内部数据结构同步的过程。除了应用这些事件外,刷新还会调用监听器,这意味着插件代码会对文件系统更改做出响应。传统上,VFS 刷新在写操作中运行,这些监听器也在那里被调用。
这产生了一个兼容性问题。多年来,大量的监听器代码都假设它们会在 EDT 上运行。一些监听器会直接访问 UI。其中许多存在于我们无法控制的插件中,这意味着我们不能简单地更改执行模型并期望一切正常。
因此,挑战不仅在于移动写操作本身,还在于在不破坏大量现有插件代码的情况下实现这一目标。
基本思路很简单:将写操作保持在后台,但当兼容性需要时,将特定的监听器工作交还给 EDT。Swing 通过 invokeAndWait(...) 为我们提供了同步移交功能。
不幸的是,这种看似简单的方法背后隐藏着死锁。如果后台写操作试图在 EDT 本身被阻塞等待锁时同步地将工作交给 EDT,IDE 就会冻结。
为了避免这种情况,我们引入了一种内部兼容性机制,允许经过精心挑选的 UI 事件在这些等待期间继续取得进展。这为我们提供了一种增量迁移的方法:我们可以在保持对仍依赖它的监听器的兼容性的同时,将昂贵的写工作从 EDT 中移出,并优先移动最耗时的部分,从而获得大部分性能提升。
事实证明,这是该项目最重要的部分之一。它让我们能够增量迁移监听器,保持对外部插件的兼容性,并仍然通过优先移动最慢的部分获得了大部分性能收益。
在 VFS 刷新之后,我们也迁移了文档提交(Document Commit)过程。这是从文档重建 PSI 的过程,一旦核心写操作机制就位,迁移它就变得直接多了。
稍后再做
[LOADING...]
后台写操作并非万能药。它们有助于减少 EDT 运行写操作的时间,但不会自动消除 EDT 等待锁的时间。
即使写操作在后台运行,EDT 仍可能被要求获取读或写意图访问权限。当写操作正在运行,或者当写操作正在等待获取写锁时,这些请求仍然可能冻结 UI。这就引出了该项目的第二部分:尽可能从 EDT 中移除锁获取。
编辑器是一个特别有问题的领域。编辑器负责根据其模型(如插入符、折叠和文档文本)在屏幕上绘制内容。但文档修改受到读写锁的保护,且编辑器仍然需要在 EDT 上访问该数据。长期以来,这意味着读操作在编辑器中随处可见,包括在绘制路径中。这是一个大问题,因为绘制可能随时发生,这意味着编辑器最终可能会在我们需要 UI 线程保持空闲的时刻要求读访问权限。
在这一领域,我们做了一个务实的权衡。我们放宽了编辑器相关 EDT 路径的一些锁要求,同时将一些文档相关的写操作保留在 EDT 上以保持一致性。这使得编辑器绘制的锁压力减小,尽管这还不能让我们将所有文档修改移至后台。我们仍需解决这一部分。
EDT 上锁压力的另一个来源是我们用于异步计算的 API。为了保持兼容性,许多此类计算最终仍与写意图获取耦合,这意味着它们可能会在不可预测的时刻冻结 EDT。
这里的关键观察很简单:如果有人安排工作在 UI 线程上异步运行,该人通常并不关心它开始的确切微秒。这意味着我们并不总是需要阻塞 EDT 来等待写意图访问。在许多情况下,我们可以推迟计算,直到该访问可用。在对平台的 UI 调度进行了一些更改后,这个问题就不那么严重了。
结果、未来工作与致谢
后台写操作很复杂,因为它们触及了 IntelliJ 平台的根本契约。我们仍在构建 API 和工具,以帮助插件将其逻辑与 EDT 解耦。工作尚未完成,但这就是我们目前的进展。
作为衡量指标,我们跟踪 EDT 花费在写操作上的时间。以下是基于每个版本发布后一周收集的数据得出的图表:
[LOADING...]
例如,在 2025.2 版本中,1% 的用户将其 5% 的 UI 时间花在了写操作上。在 2025.3 版本中,相同百分比的用户仅花费了 3%。总体而言,EDT 上花费在写锁上的预期 UI 时间份额从 2025.2 的 1.8276% 下降到了 2025.3 的 0.5298%。
未来,我们的工作将集中于从 EDT 中移除更多的写意图使用。我们希望消除输入等常见交互中的锁定。这是一个困难的目标,因为它需要重新思考 Action、PSI 和 Document 等基本结构。这很难,但我们认为是可以做到的。
最后,我们要感谢所有未提及但直接或间接参与此项目的人员:Anna Saklakova、Dmitrii Batkovich、Vladimir Krivosheev、Moncef Slimani、Lev Serebryakov 和 Nikita Koval 等。这篇文章最初是由 Konstantin Nisht 撰写的内部文章,并由 Patrick Scheibe 改编为公开博客 —— 其中的破坏性变更比平时更少。