Changes in Threading Model in the IntelliJ Platform 2026.3

Hello everyone,

We would like to share some important updates to the threading framework in the IntelliJ Platform 2026.3.

This release focuses on major internal refactorings. Our efforts are directed at minimizing the impact on existing code, but nevertheless, there are some changes worth knowing about.

  1. PSI syntax trees are now persistent (see IJPL-236269).

    In the previous release (2026.2), we modified the internal representation of our PSI syntax trees to enable persistence (a.k.a. versioning). In this release (2026.3), we are enabling this functionality.
    This is a major change, and it has several implications:

    • PSI syntax trees are now heavier due to the bookkeeping overhead for persistent structures. This overhead affects both memory usage and performance. We took a close look at scenarios where trees are heavily used, and in our benchmarks, performance degradation was almost nonexistent. However, if a plugin interacts with syntax trees in an asymptotically suboptimal way (i.e., processing trees in O(n^2) time), the slowdown will be amplified by our changes.
    • The IDE is now able to work with a consistent snapshot of syntax trees without the read lock. The primary intended user of this functionality is the UI thread—we are moving towards eliminating the read/write lock from the UI thread, and having read-lock-free access to consistent syntax trees helps us achieve this without a significant rewrite of the existing business logic. If you are interested, you can explore PsiVersioningService#freezePsiVersion.
  2. We are prohibiting read lock access during editor painting (see IJPL-248910).

    Our research has shown that the main reason for acquiring the read lock during painting is the need to access PSI trees. If your plugin falls into this category, consider using PsiVersioningService#freezePsiVersion to avoid triggering the assertion.
    Note that we do not intend to break existing functionality—the error is simply logged; no exception is thrown.

  3. edtWriteAction no longer blocks the UI thread (see IJPL-236701).

    This is a subtle but important change that might break some assumptions in tests. Note that it is still preferable to use the regular writeAction, as it runs in the background.

  4. We have removed the write-intent lock from arrow key processing (see IJPL-231208).

    This change is a battle test for persistent syntax trees. You are welcome to use PsiVersioningService#freezePsiVersion if your functionality bound to arrow keys depends on having read access to PSI trees.

  5. The IDE is now resilient to thread starvation (see IJPL-214442).

    For years, the IDE was plagued by a specific kind of deadlock—thread starvation. This problem is rooted in the Kotlin coroutines machinery, where the default CPU-bound thread pool can be exhausted by continuous blocking. We have patched the coroutines library so that the IDE can recover from thread starvation. Note that this is still logged as an error, and the underlying cause is still worth fixing.

Thank you for your attention!

Hi, I was wondering if there are any guardrails against race conditions on fields in custom element implementations. For example lets look at PsiLocalVariableImpl.java:

public class PsiLocalVariableImpl extends CompositePsiElement implements … {
  …
  private volatile String myCachedName;
  …
  @Override
  public void clearCaches() { // other classes may override `subtreeChanged` instead
    super.clearCaches();
    myCachedName = null;
  }
  …
  @Override
  public final @NotNull String getName() {
    String cachedName = myCachedName;
    if (cachedName == null){
      myCachedName = cachedName = getNameIdentifier().getText();
    }
    return cachedName;
  }
  …
}

If some action is executed on an old version of the tree and calls PsiLocalVariableImpl.getName(), it may initialize myCachedName with an obsolete value. Actions executed on a later version may now read that obsolete name.

It is not a goal to enable resolve and mutation for old versions of syntax trees.
— IJPL-236269

I mean not resolving other elements and caching the result on old versions would prevent such issues, but are there any guardrails which would detact and fail early on any accedantal usage of methods that could lead to such race conditions?

Hi Johannes,

Good catch. As of now, there are no guardrails, but I am thinking about adding an inspection or bytecode instrumentation to capture such cases.

There is also a way of fixing such issues: we have a class com.intellij.psi.impl.source.tree.mvcc.VersionedPsiReference that is intended for such mutable fields. If we realize that we actually need myCachedName for usages within one version, the fix would be to use VersionedPsiReference.

I think VersionedPsiReference is internal, so plugins can’t really use it. However, getUserData and putUserData seems to be versioned, so I guess plugins can just use that when necessary.