Platform logo

JetBrains Platform

Plugin and extension development for JetBrains products.

IntelliJ Platform Plugins

提高基于 IntelliJ 的 IDE 响应性能之路

Read this post in other languages:

摘要:这是一篇技术博文,介绍了我们为提升基于 IntelliJ 的 IDE 中的 UI 响应所做的工作。这是一项历时多年的工作,旨在解决多个架构层面的约束。项目仍在进行中,截至目前,我们已经构建新的工具和 API,用于将性能敏感型工作从 UI 线程中分离出来。这一改变意味着 UI 线程现在持有写入锁的时长大幅缩短,约为之前的三分之一。如果您对技术细节不感兴趣,可以直接跳到文末查看图表。

漫画展示读操作和写操作不能同时进行。

对于基于 IntelliJ 的 IDE,大家抱怨最多的一点就是性能。我们了解这一情况, 也一直在努力提升 IDE 的响应性。但这并非易事:IntelliJ Platform 已有 25 年历史,其中一些架构决策已成定局。这些决策也给某些优化带来了困难。

致命困局

当 UI 线程不得不处理业务逻辑时,性能会受到影响。

IntelliJ Platform 是围绕单一读写 (RW) 锁构建的多线程框架。IDE 依赖多个核心数据结构运行:语法树 (PSI)、文件文本视图(文档子系统)以及 OS 文件系统视图(虚拟文件系统,也称为 VFS)。对这些结构的访问均受 RW 锁的保护。操作分为读操作和写操作。同一时刻,仅允许存在一个写操作;多个读操作可以并行执行,但读操作和写操作无法同时执行。

我们的 IDE 也是 UI 应用程序,这意味着它们需要使用 UI 框架。在 IntelliJ Platform 中,该框架为 Java AWT,它有一个 UI 线程 – 事件分发线程 (EDT)。此线程负责处理用户输入和绘制 UI。Java 还允许业务逻辑在此线程中运行。EDT 的性能直接影响应用程序的响应速度:如果它能快速处理绘制事件和用户输入,IDE 就会感觉流畅。

卡顿问题正源于此。写操作本身就可能造成卡顿。部分写操作(如重新解析语法树或更新文件系统视图)本身的开销就比较高。另一个比较不明显的卡顿原因是等待获取写入锁。由于读写操作无法同时执行,启动写操作意味着等待所有有效的读操作执行完毕。我们已进行大量工作,将读操作改为可取消形式,但该问题并未彻底解决:只要存在一个不配合的读操作,整个 IDE 就都会受到影响。

这自然而然地引导我们确定了核心目标:将写操作从 UI 线程中分离出来。

具有良好的意图

IntelliJ Platform 团队必须避免旧编辑器代码的问题

支持后台写操作的工作于 2019 年由 Valentin FondaratovAndrew KozlovPeter Gromov 开展。

长期以来,在 EDT 上运行的代码可以便捷地访问 IntelliJ Platform 模型。但引入后台写操作后,这种便利反而带来了问题:UI 代码不能再假定无需进行显式协调即可安全实现模型访问, 为保持兼容性,我们必须在明确这类假设条件的同时,维持大量现有 UI 代码的正常运行。

此外还存在另一个复杂难题。EDT 上的代码还可以立即启动写操作, 而普通的显式读操作无法做到这一点,因为写操作无法在读操作执行过程中随意启动。

为此我们引入了“写意图”这一概念。写意图是一种锁状态,该状态下仍允许并行读操作,但同一时间仅能由一个线程持有,并且可以原子性地升级为完整写操作。这非常适合可能需要转换为写操作的 EDT 代码。在平台中采用这种方式是实现保留现有行为的同时支持后台写入的关键一步。

该项目于 2020 年暂停,原因是所需更改量过大。很多 UI 组件(尤其是编辑器)高度依赖长期以来形成的假设,即默认可以通过 EDT 实现模型访问

大规模重构

狮子不纠缠于旧代码

项目虽已暂停,却未被放弃。2022 年,Lev SerebryakovDaniil Ovchinnikov 重启了这项工作。

在此阶段,我们重构了 IntelliJ Platform,明确了平台隐式依赖的大量假设。这项工作减少了平台在 UI 驱动代码路径中对隐式锁的一些依赖。

本阶段另一项重要工作是与 JetBrains 研究团队展开合作。之前的锁实现假设写操作仅在 EDT 上执行, 而迁移至后台则需要另一套锁机制,原有的普通 ReentrantReadWriteLock 已无法我们的满足需求。最终我们研发出一种新型可取消锁,现已成为平台的核心组成部分(参阅本研究论文)。

此阶段的工作持续至 2024 年末。

当一种锁无法满足需求时

读写锁过多会导致混淆

2025 年初,Konstantin Nisht 接手了项目的这部分工作。那时,我们已基本准备好运行首个后台写操作, 但仍遗留了一个主要难题:模态性。

IDE 中的某些 UI 元素需要阻止用户与自身以外的任何内容进行交互, 这类元素即为模态对话框,例如 Settings(设置)对话框。在 IntelliJ Platform 中,模态性还会影响模型:当模态窗口可见时,不相关的写操作应无法启动。之前,EDT 调度器处理了大部分这类工作,方法是确保在模态对话框激活期间,在非模态上下文下发起的 UI 任务不会运行。

但后台写操作无法自动适配该模型。

如果 EDT 持有写意图时显示模态对话框,此时尝试运行后台写操作会引发死锁。与此同时,我们仍要保证对话框内部的计算任务正常进行,不受外部无关任务的干扰。

为解决这一问题,我们引入了模态感知锁定策略,将模态对话框内部与外部的操作分离开来。该策略既保留了模态对话框依赖的保障机制,又支持后台写操作正常运行。

此策略同样适用于嵌套模态计算,这一点至关重要,因为实际的模态工作流并非总是简单的线性结构。完成这项适配后,我们终于能够在后台运行首批写操作。

在不破坏插件的前提下迁移任务

尽量不进行大幅改动,以免破坏现有代码

初始的写操作相对来说比较容易迁移到后台。这些操作位于工作空间模型中,主要用于使部分缓存失效。迁移完成后,就该尝试一些更具挑战性的任务:VFS 刷新。

VFS 刷新是指将文件修改事件从操作系统同步至 IDE 内部数据结构的过程。除了应用这些事件外,刷新还会调用侦听器,即响应文件系统更改的插件代码。之前的做法是,VFS 刷新在写操作中运行,相关侦听器也在这里被调用。

该做法会引发兼容性问题。多年来,大量侦听器代码已假定其会在 EDT 上运行。部分侦听器会直接访问 UI, 其中很多侦听器存在于我们无法控制的插件中,这意味着我们无法直接更改执行模型,并期望所有功能继续正常运行。

因此,我们面临的挑战不仅在于迁移写操作本身,还要在不破坏海量现有插件代码的前提下完成迁移。

基本思路很简单:保持写操作在后台运行,但在存在兼容性需求时将特定侦听器任务交还 EDT 处理。Swing 通过 invokeAndWait(...) 提供了同步任务交接机制。

遗憾的是,这种看似简单的方法隐藏着死锁风险。如果后台写操作尝试通过同步方式向 EDT 交接任务,而此时 EDT 正因等待锁而受阻,IDE 可能会冻结。

为避免出现这种情况,我们引入了内部兼容机制,允许特定 UI 事件在这些等待期间继续执行。这样,我们便实现了渐进式迁移:可以将高开销写任务移出 EDT,同时为仍依赖该 EDT 的侦听器保留兼容性。

这最终成为本项目最为关键的部分之一。该机制支持渐进式迁移侦听器、保持外部插件的兼容性,通过优先迁移耗时最久的代码最大限度提升性能。

完成 VFS 刷新后,我们又迁移了文档提交进程。该进程负责通过文档重建 PSI,在核心写操作机制就绪后,这项迁移工作变得简单很多。

我们稍后再处理

漫画讲的是某些任务需要稍后再完成,无论怎样选择,都存在弊端

后台写操作并不是万能解决方案, 它们虽然能减少 EDT 执行写操作所花的时间,却无法自动消除 EDT 等待锁所用的时间。

即使写操作在后台运行,可能仍需要通过 EDT 获取读或写意图权限。当写操作正在运行,或写操作等待获取写锁时,这些请求仍会导致 UI 冻结。由此引出了此项目的第二部分工作:尽可能减少 EDT 中的锁获取操作。

这一问题尤为突出的领域是编辑器。编辑器负责根据模型在屏幕上绘制内容,例如插入符号、折叠和文档文本。但文档修改受 RW 锁保护,而编辑器仍需在 EDT 中访问这些数据。很长时间以来,这意味着编辑器中随处可见读操作,绘制路径中也不例外。这属于重大问题,因为绘制可随时进行,这意味着编辑器可能在最需要 UI 线程保持空闲的时刻请求读访问。

针对该场景,我们采取了务实的折中方案。我们放宽了编辑器相关 EDT 路径的部分锁要求,同时将部分文档写操作保留在 EDT 上执行,以保持一致性。这样一来,编辑器绘制对锁的依赖有所降低,尽管目前尚未实现将所有文档修改迁移至后台。该部分工作仍有待完成。

EDT 的另一个锁压力源是用于异步计算的 API。为保持兼容性,许多此类计算仍与写意图锁获取绑定,这意味着这些计算可能导致 EDT 在不可预测的时刻冻结。

对此,关键发现很简单:如果有人将任务调度到 UI 线程上异步执行,该人通常不会在意任务的精确启动时间。这意味着我们无需总是阻止 EDT 等待写意图锁。多数情况下,只需延迟计算至访问可用即可。在对平台的 UI 调度进行一些更改后,该问题已大幅缓解。

成果、后续工作和致谢

后台写操作涉及 IntelliJ Platform 中的底层约定,因此比较复杂。我们仍在构建 API 和工具,以帮助插件将其逻辑从 EDT 中分离出来。这项工作尚未完成,但当前进展如下:

作为衡量指标,我们跟踪 EDT 执行写操作所花费的时间。下图基于各版本发布后一周收集的数据绘制:

2025.2 版本与 2025.3 版本之间的 UI 写操作性能对比

在版本 2025.2 中,1% 的用户将其 5% 的 UI 时间用在了写操作上; 而在 2025.3 版本中,相同占比的用户仅将 3% 的 UI 时间花在写操作上。整体来看,EDT 花在写锁上的 UI 时间预期占比从 2025.2 版本的 1.8276% 下降至 2025.3 版本的 0.5298%。

后续工作将侧重于进一步减少 EDT 中的写意图使用量。我们的目标是在输入等常规交互操作中彻底消除锁定。这一目标较难实现,因为需要重新设计操作、PSI 和文档等基础结构。过程虽艰难,但我们认为切实可行。

最后,我们要感谢所有前文未曾提及、直接或间接参与本项目的人员:Anna Saklakova, Dmitrii BatkovichVladimir KrivosheevMoncef SlimaniLev SerebryakovNikita Koval 等人。本文最初是由 Konstantin Nisht 撰写的内部文章,后由 Patrick Scheibe 改编为公开博客文章 — 与以往相比,所做的破坏性改动较少。

本博文英文原作者:

Patrick Scheibe

Patrick Scheibe

Konstantin Nisht

Konstantin Nisht

Discover more