Как JetBrains избавляются от фризов в IntelliJ-IDE: перевод статьи про background write-действия

Перевод технической статьи JetBrains Platform: как RW lock и EDT порождают фризы IntelliJ-IDE, почему проект переноса write-действий в фон шёл семь лет, и что поменялось в 2025.3 — с сохранением имён участников, дат и метрик.

Обложка: Как JetBrains избавляются от фризов в IntelliJ-IDE: перевод статьи про background write-действия

Если у вас при правке большого проекта в IntelliJ IDEA, WebStorm или PyCharm IDE периодически замирает — шестерёнка приколочена к UI-потоку, и за ней стоит очередь из операций, которые нельзя распараллелить. JetBrains вычищают её уже семь лет, и только что отчитались об очередном шаге: доля времени UI-потока, занятая write-действиями, упала с 1,83% до 0,53% между версиями 2025.2 и 2025.3 — в три с половиной раза.

Это перевод оригинальной статьи от Patrick Scheibe (по мотивам внутренней статьи Konstantin Nisht) на блоге JetBrains Platform про многолетнюю работу команды по перекладыванию write-операций в фон. Для разработчиков, которые пишут плагины под IntelliJ Platform или просто борются с фризами в IDE — это редкая возможность заглянуть под капот.

Ключевые выводы
  • Корень фризов IntelliJ-IDE — single read-write lock на общие структуры данных (PSI, Document, VFS) и single UI-поток AWT (EDT)
  • С 2019 года JetBrains переносят write-действия в фон, с паузой в 2020-2022 годах
  • В 2024 году появился новый cancellable lock по результатам совместной работы с JetBrains Research
  • В 2025 году Konstantin Nisht решил проблему modality (модальные диалоги) — первые write-действия ушли в фон
  • Мигрировали: Workspace Model, VFS refresh, document commit
  • 2025.2 → 2025.3: доля времени UI на write-lock упала с 1,8276% до 0,5298%, в 1% худших случаев — с 5% до 3%
TL;DR от авторов: это технический пост про многолетнюю работу над отзывчивостью IntelliJ-based IDE. JetBrains строят инструменты и API, которые позволяют выполнять нагрузочные операции вне UI-потока. UI-поток теперь держит write-блокировку примерно в три раза меньше времени, чем раньше. Если технические детали не интересуют — листайте к графикам в конце.

Одна из самых частых жалоб на IntelliJ-based IDE — производительность. Мы знаем. И работаем над тем, чтобы сделать IDE более отзывчивой. Это не всегда просто: платформе IntelliJ 25 лет, и некоторые архитектурные решения впаяны в неё намертво. Именно они делают ряд оптимизаций сложными.

Смертоносное переплетение

IntelliJ Platform — многопоточный фреймворк, построенный вокруг единой read-write-блокировки (RW lock). IDE работает с несколькими ключевыми структурами данных: синтаксическими деревьями (PSI), текстовым представлением файлов (Document subsystem) и представлением файловой системы ОС (Virtual File System, VFS). Доступ к этим структурам защищён RW-блокировкой. Операции делятся на read actions и write actions. В каждый момент времени может существовать только одно write-действие; read-действий может быть параллельно сколько угодно, но read- и write-действие одновременно идти не могут.

Ещё наши IDE — это UI-приложения. Значит, они используют UI-фреймворк. В IntelliJ Platform это Java AWT с единственным UI-потоком: Event Dispatch Thread (EDT). Этот поток обрабатывает пользовательский ввод и отрисовывает интерфейс. Java также позволяет запускать там бизнес-логику. Производительность EDT напрямую определяет ощущение отзывчивости приложения: если поток быстро обрабатывает события перерисовки и ввод — IDE кажется шустрой.

Отсюда и берутся фризы. Само write-действие может их вызывать: некоторые write-действия, например пересбор синтаксического дерева или обновление представления файловой системы, тяжёлые сами по себе. Другой, менее очевидный источник фризов — ожидание write-блокировки. Поскольку read- и write-действия не могут идти параллельно, запуск write-действия означает ожидание завершения всех активных read-действий. Мы много работали над тем, чтобы read-действия можно было отменять, но проблема не уходит полностью: если хоть одно read-действие неотменяемо — страдает вся IDE.

Это и привело нас к ключевой цели: перенести write-действия с UI-потока в фон.

С благими намерениями

Работа над фоновыми write-действиями началась в 2019 году. Этим занялись Valentin Fondaratov, Andrew Kozlov и Peter Gromov.

Долгие годы код на EDT имел удобный прямой доступ к моделям IntelliJ Platform. С фоновыми write-действиями эта удобная особенность становится проблемой: UI-код больше не может считать, что доступ к модели всегда безопасен без явной координации. Чтобы сохранить совместимость, нужно было заставить работать большое количество старого UI-кода и при этом сделать все зависимости явными.

Было и другое осложнение. Код на EDT мог сразу запустить write-действие. Для обычных явных read-действий это не так — write-действие не может просто начаться посередине read-действия.

Здесь в игру вступает write-intent. Это состояние блокировки, которое всё ещё допускает параллельные read-действия, но может быть взято только одним потоком одновременно и атомарно апгрейдится до полноценного write-действия. Такой режим хорошо подходит EDT-коду, которому может потребоваться перейти к write-действию. Его внедрение в платформу стало важным шагом к поддержке фоновых write-операций без ломки текущего поведения.

В 2020 году проект поставили на паузу — объём требуемых изменений оказался колоссальным. Много UI-компонентов, особенно редактор, опирались на давние допущения о доступе к моделям с EDT.

Большой рефакторинг

Проект не бросили, а в 2022 году работу возобновили Lev Serebryakov и Daniil Ovchinnikov.

На этом этапе IntelliJ Platform отрефакторили так, чтобы вывести наружу множество неявных допущений, на которых она держалась. Это уменьшило зависимость части UI-кода от неявной блокировки.

Другой важной частью стала совместная работа с командой JetBrains Research. Прежняя реализация блокировки предполагала, что write-действия выполняются только на EDT. Перенос их в фон требовал другого типа блокировки, а обычный ReentrantReadWriteLock не подходил под наши нужды. Результат — новая отменяемая (cancellable) блокировка, которая теперь управляет платформой (см. статью исследования).

Этот этап длился до конца 2024 года.

Когда одной блокировки недостаточно

В начале 2025 года эту часть проекта возглавил Konstantin Nisht. К тому моменту мы почти были готовы запустить первые фоновые write-действия. Оставалась одна крупная проблема — modality (модальность).

Некоторые UI-элементы IDE должны блокировать пользователю возможность взаимодействовать с чем-то другим. Это модальные диалоги, например диалог Settings. В IntelliJ Platform модальность влияет и на модель: пока виден модальный диалог, не связанные с ним write-действия не должны запускаться. Исторически большую часть этого обеспечивал EDT-планировщик — он просто не запускал UI-работу из немодального контекста, пока модальный диалог открыт.

Фоновые write-действия в эту модель автоматически не вписывались.

Если на EDT показан модальный диалог, держащий write-intent, то наивная попытка запустить фоновое write-действие может привести к дедлоку. При этом мы хотим, чтобы вычисления внутри диалога двигались вперёд и их не тормозила работа, не связанная с диалогом.

Чтобы это решить, мы ввели стратегию блокировок с учётом модальности — она разделяет, что происходит внутри модального диалога, а что снаружи. Это сохраняет гарантии, на которые опираются модальные диалоги, и позволяет фоновым write-действиям выполняться.

Подход также работает для вложенных модальных вычислений — что важно, потому что настоящие модальные процессы не всегда плоские. С этим на месте мы наконец смогли запустить первые фоновые write-действия.

Переносим работу, не ломая плагины

Первые write-действия перенести в фон было относительно легко. Они жили в Workspace Model и в основном использовались для инвалидации кешей. После этого наступила очередь чего-то посерьёзнее: VFS refresh.

VFS refresh — это процесс синхронизации событий изменения файлов от операционной системы с внутренними структурами IDE. Помимо применения этих событий refresh вызывает листенеры — код плагинов, реагирующий на изменения файловой системы. Традиционно VFS refresh выполняется в write-действии, и эти листенеры вызываются там же.

Возникает проблема совместимости. За много лет большое количество кода листенеров стало предполагать, что выполняется на EDT. Некоторые из них естественным образом лезут в UI. Многие живут в плагинах, которыми мы не управляем — поэтому просто взять и изменить модель выполнения в надежде, что всё продолжит работать, нельзя.

Задача была не только перенести само write-действие в фон, но и сделать это так, чтобы не сломать длинный хвост существующего плагинного кода.

Базовая идея проста: держать write-действие в фоне, но при необходимости совместимости возвращать конкретную работу листенеров на EDT. Swing даёт для этого синхронную передачу управления через invokeAndWait(...).

К сожалению, за этим на первый взгляд простым подходом прячется дедлок. Если фоновое write-действие пытается синхронно передать работу на EDT, а EDT в этот момент сам блокирован ожиданием lock — IDE может замереть.

Чтобы этого избежать, мы ввели внутренний механизм совместимости, который позволяет определённым UI-событиям продолжать продвигаться во время таких ожиданий. Это дало возможность мигрировать инкрементно: перенести тяжёлую write-работу с EDT, при этом сохранив совместимость для листенеров, которые ещё от него зависят.

Этот подход оказался одним из важнейших кусков проекта. Он позволил мигрировать листенеры постепенно, сохранить совместимость для внешних плагинов — и при этом получить большую часть выигрыша в производительности, перенося в первую очередь самые медленные участки.

После VFS refresh мы мигрировали и процесс document commit — тот, который перестраивает PSI из документов. Когда базовая инфраструктура фоновых write-действий была на месте, перенос оказался значительно проще.

А давайте потом

Фоновые write-действия — не панацея. Они сокращают время, которое EDT проводит за выполнением write-действий, но автоматически не убирают время, которое EDT тратит на ожидание блокировок.

Даже если write-действия выполняются в фоне, EDT всё ещё может попросить доступ на read или write-intent. Пока идёт write-действие — или пока оно ждёт write-lock — такие запросы способны зафризить UI. Отсюда вторая часть проекта: убрать с EDT как можно больше взятий блокировок.

Особенно проблемной была область редактора. Редактор отрисовывает контент на основании своих моделей — позиции курсора, фолдингов, текста документа. Но модификация документа защищена RW-lock, а редактору всё ещё нужен доступ к этим данным на EDT. Долгое время read-действия были в редакторе повсюду, включая пути отрисовки. Это серьёзная проблема, потому что отрисовка может происходить в любой момент — и редактор мог требовать read-доступ именно тогда, когда нам больше всего нужно, чтобы UI-поток был свободен.

Здесь мы пошли на прагматичный компромисс. Мы ослабили часть требований к блокировкам на EDT-путях редактора, но сохранили часть записей документа на EDT ради консистентности. Так отрисовка редактора стала менее зависимой от блокировок — хотя мы пока и не перенесли все модификации документа в фон. Это ещё предстоит.

Ещё одним источником давления на блокировки в EDT был наш API для асинхронных вычислений. Ради совместимости многие такие вычисления всё ещё были связаны с захватом write-intent, а значит, могли зафризить EDT в непредсказуемые моменты.

Наблюдение здесь простое: если кто-то планирует работу на выполнение асинхронно в UI-потоке — обычно его не волнует точная микросекунда начала. Значит, не всегда нужно блокировать EDT в ожидании write-intent. Во многих случаях можно просто отложить вычисление до момента, когда этот доступ станет доступным. После ряда изменений в планировке UI проблема стала намного менее актуальной.

Результаты, планы и благодарности

Фоновые write-действия сложны, потому что касаются фундаментальных контрактов IntelliJ Platform. Мы всё ещё строим API и инструменты, которые помогут плагинам отвязать свою логику от EDT. Работа не закончена, но вот где мы сейчас.

Как метрику мы отслеживаем, сколько времени EDT тратит на выполнение write-действий. Данные собираются через неделю после каждого релиза.

Например, в 2025.2 у 1% пользователей write-действия занимали 5% UI-времени. В 2025.3 тот же перцентиль — уже 3%. А ожидаемая общая доля времени UI на write-locks на EDT упала с 1,8276% в 2025.2 до 0,5298% в 2025.3 — примерно в три с половиной раза.

В будущем работа сосредоточится на том, чтобы убрать с EDT больше использования write-intent. Мы хотим убрать блокировки из таких частых взаимодействий, как набор текста. Это сложная цель, потому что она требует переосмысления фундаментальных структур — Actions, PSI, Documents. Сложно, но, как мы думаем, выполнимо.

Спасибо всем, кого мы ещё не упомянули, кто прямо или косвенно участвовал в проекте: Anna Saklakova, Dmitrii Batkovich, Vladimir Krivosheev, Moncef Slimani, Lev Serebryakov, Nikita Koval и другим. Пост начинался как внутренняя статья Konstantin Nisht и был адаптирован для публичного блога Patrick Scheibe — с меньшим количеством breaking changes, чем обычно.

Что это значит для разработчиков плагинов

Если вы разрабатываете плагин под IntelliJ Platform — изменения в модели блокировок затрагивают вас напрямую. Три практических вывода из статьи:

  • Листенеры на VFS refresh и document commit теперь могут выполняться в фоне. Если код листенера предполагал EDT — он будет перенесён через механизм совместимости, но ценой общей производительности. Лучше пройтись по своим листенерам и убрать из них прямую работу с Swing, где это возможно
  • Долгие некооперативные read-действия — самое больное место. Один плагин с неотменяемым read-действием может тормозить всю IDE. Если пишете код, читающий PSI — поддержите отмену (ProgressManager.checkCanceled) и уведомляйте платформу
  • Write-intent на EDT больше не безопасно захватывать «на всякий случай». Если вашему коду нужна запись — лучше явно запросить фоновое выполнение, чем держать write-intent на UI-потоке. Конкретные API меняются от релиза к релизу — следите за изменениями в документации IntelliJ Platform

Для обычных пользователей IDE итог простой: если вы обновитесь с 2025.2 на 2025.3 — фризов должно стать меньше, особенно на крупных Gradle- или Maven-проектах. Конкретный эффект зависит от набора плагинов: те, что ещё не адаптированы, могут работать в старом режиме совместимости и не дать полного выигрыша.

Часто задаваемые вопросы
1
Это перевод или вольный пересказ?

Это полный перевод оригинальной статьи с блога JetBrains Platform, с сохранением всех технических деталей, имён участников, дат и метрик. Добавлена только вступительная и заключительная секции с контекстом для российских разработчиков.

2
Кому это вообще полезно читать?

В первую очередь — разработчикам плагинов под IntelliJ Platform: изменения в контракте модели блокировок затрагивают написанный ими код напрямую. Во вторую — инженерам, которые интересуются, как крупные продукты проводят многолетние архитектурные миграции без поломки обратной совместимости.

3
В какой версии эти изменения?

Основной выигрыш виден в 2025.3 по сравнению с 2025.2. Работа продолжается: команда планирует вывести с EDT ещё больше взятий write-intent, включая операции ввода текста. Конкретный релиз с дальнейшим улучшением авторы оригинальной статьи не называют.

4
А фризы из-за индексации тут причём?

Это отдельная тема. Индексация — одна из крупных причин тормозов, но относится она к другой архитектурной области (Shared Indexes, storage). Описанные в статье изменения касаются RW-блокировки и EDT — то есть фризов, возникающих из-за работы write-действий на UI-потоке. JetBrains работают над обоими направлениями параллельно.

5
Можно ли следить за прогрессом?

Да. Команда публикует технические обзоры в блоге JetBrains Platform. Например, там же недавно вышла отдельная статья про UI-фризы из-за non-cancellable read actions в фоновых потоках.

Выводы

Главный урок статьи не технический, а архитектурный. JetBrains семь лет переделывают фундаментальный контракт платформы, и даже сейчас работа не закончена. Переход с модели «всё на EDT» на «write-действия в фоне» — это не оптимизация, а смена условий, на которых держалась вся плагинная экосистема. И они делают это, не ломая внешние плагины.

Для разработчиков это один из лучших публичных кейсов, как мигрировать большую систему с глубоко зашитыми допущениями. Для пользователей — маленькое утешение: фризы в IDE с новым релизом станут заметно короче.

Мы хотим убрать блокировки из таких частых взаимодействий, как набор текста. Это сложная цель, потому что она требует переосмысления фундаментальных структур — Actions, PSI, Documents. Сложно, но мы думаем, что это выполнимо.
Konstantin NishtJetBrains

Оригинал статьи — на блоге JetBrains Platform, авторы: Patrick Scheibe и Konstantin Nisht (внутренняя версия), академическая статья про cancellable lock — на arXiv.