IntelliJ Platform Plugin SDK Help

Threading Model

The IntelliJ Platform is a highly concurrent environment. Code is executed in many threads simultaneously. In general, as in a regular Swing application, threads can be categorized into two main groups:

  • Event Dispatch Thread (EDT) – also known as the UI thread. Its main purpose is handling UI events (such as reacting to clicking a button or updating the UI), but the platform uses it also for writing data. EDT executes events taken from the Event Queue. Operations performed on EDT must be as fast as possible to not block other events in the queue and freeze the UI. There is only one EDT in the running application.

  • background threads (BGT) – used for performing long-running and costly operations, or background tasks

It is possible to switch between BGT and EDT in both directions. Operations can be scheduled to execute on EDT from BGT (and EDT) with invokeLater() methods (see the rest of this page for details). Executing on BGT from EDT can be achieved with background processes.

Read-Write Lock

The IntelliJ Platform data structures (such as Program Structure Interface, Virtual File System, or Project root model) aren't thread-safe. Accessing them requires a synchronization mechanism ensuring that all threads see the data in a consistent and up-to-date state. This is implemented with a single application-wide read-write (RW) lock that must be acquired by threads requiring reading or writing to data models.

If a thread requires accessing a data model, it must acquire one of the locks:

Read Lock

Write Intent Lock

Write Lock

Allows a thread for reading data.

Allows a thread for reading data and potentially upgrade to the write lock.

Allows a thread for reading and writing data.

Can be acquired from any thread concurrently with other read locks and write intent lock.

Can be acquired from any thread concurrently with read locks.

Can be acquired only from EDT concurrently with a write intent lock acquired on EDT.

Can't be acquired if write lock is held on another thread.

Can't be acquired if another write intent lock or write lock is held on another thread.

Can't be acquired if any other lock is held on another thread.

The following table shows compatibility between locks in a simplified form:

Read Lock

Write Intent Lock

Write Lock

Read Lock

+

+

-

Write Intent Lock

+

-

-

Write Lock

-

-

-

The described lock characteristics conclude the following:

  • multiple threads can read data at the same time

  • once a thread acquires the write lock, no other threads can read or write data

Acquiring and releasing locks explicitly in code would be verbose and error-prone and must never be done by plugins. The IntelliJ Platform enables write intent lock implicitly on EDT (see Locks and EDT for details) and provides an API for accessing data under read or write locks.

Locks and EDT

Although acquiring all types of locks can be, in theory, done from any threads, the platform implicitly acquires write intent lock and allows acquiring the write lock only on EDT. It means that writing data can be done only on EDT.

The scope of implicitly acquiring the write intent lock on EDT differs depending on the platform version:

Write intent lock is acquired automatically when operation is invoked on EDT with Application.invokeLater().

Write intent lock is acquired automatically when operation is invoked on EDT with methods such as:

It is recommended to use Application.invokeLater() if the operation is supposed to write data. Use other methods for pure UI operations.

Accessing Data

The IntelliJ Platform provides a simple API for accessing data under read or write locks in a form of read and write actions.

Read and write actions allow executing a piece of code under a lock, automatically acquiring it before an action starts, and releasing it after the action is finished.

Read Actions

API

  • ReadAction run() or compute():

    val psiFile = ReadAction.compute<PsiFile, Throwable> { // read and return PsiFile }
    PsiFile psiFile = ReadAction.compute(() -> { // read and return PsiFile });
Alternative APIs
  • Application.runReadAction():

    val psiFile = ApplicationManager.application.runReadAction { // read and return PsiFile }
    PsiFile psiFile = ApplicationManager.getApplication() .runReadAction((Computable<PsiFile>)() -> { // read and return PsiFile });

    Note that this API is considered low-level and should be avoided.

  • Kotlin runReadAction():

    val psiFile = runReadAction { // read and return PsiFile }

    Note that this API is obsolete since 2024.1. Plugins implemented in Kotlin and targeting versions 2024.1+ should use suspending readAction(). See also Coroutine Read Actions.

Rules

Reading data is allowed from any thread.

Reading data on EDT invoked with Application.invokeLater() doesn't require an explicit read action, as the write intent lock allowing to read data is acquired implicitly.

Reading data is allowed from any thread.

Reading data on EDT doesn't require an explicit read action, as the write intent lock allowing to read data is acquired implicitly.

In all other cases, it is required to wrap read operation in a read action with one of the API methods.

Objects Validity

The read objects aren't guaranteed to survive between several consecutive read actions. Whenever starting a read action, check if the PSI/VFS/project/module is still valid. Example:

val virtualFile = runReadAction { // read action 1 // read a virtual file } // do other time-consuming work... val psiFile = runReadAction { // read action 2 if (virtualFile.isValid()) { // check if the virtual file is valid PsiManager.getInstance(project).findFile(virtualFile) } else null }

Between executing first and second read actions, another thread could invalidate the virtual file:

read action 1 time-consuming work delete virtual file read action 2 Thread 1Thread 2

Write Actions

API

  • WriteAction run() or compute():

    WriteAction.run<Throwable> { // write data }
    WriteAction.run(() -> { // write data });
Alternative APIs
  • Application.runWriteAction():

    ApplicationManager.application.runWriteAction { // write data }
    ApplicationManager.getApplication().runWriteAction(() -> { // write data });

    Note that this API is considered low-level and should be avoided.

  • Kotlin runWriteAction():

    runWriteAction { // write data }

    Note that this API is obsolete since 2024.1. Plugins implemented in Kotlin and targeting versions 2024.1+ should use suspending writeAction().

Rules

Writing data is only allowed on EDT invoked with Application.invokeLater().

Write operations must always be wrapped in a write action with one of the API methods.

Writing data is only allowed on EDT.

Write operations must always be wrapped in a write action with one of the API methods.

Modifying the model is only allowed from write-safe contexts, including user actions and SwingUtilities.invokeLater() calls from them (see Invoking Operations on EDT and Modality).

Modifying PSI, VFS, or project model from inside UI renderers or SwingUtilities.invokeLater() calls is forbidden.

Invoking Operations on EDT and Modality

Operations that write data on EDT should be invoked with Application.invokeLater() because it allows specifying the modality state (ModalityState) for the scheduled operation. This is not supported by SwingUtilities.invokeLater() and similar APIs.

ModalityState represents the stack of active modal dialogs and is used in calls to Application.invokeLater() to ensure the scheduled runnable can execute within the given modality state, meaning when the same set of modal dialogs or a subset is present.

To better understand what problem ModalityState solves, consider the following scenario:

  1. A user action is started.

  2. In the meantime, another operation is scheduled on EDT with SwingUtilities.invokeLater() (without modality state support).

  3. The action from 1. now shows a dialog asking a Yes/No question.

  4. While the dialog is shown, the operation from 2. is now processed and does changes to the data model, which invalidates PSI.

  5. The user clicks Yes or No in the dialog, and it executes some code based on the answer.

  6. Now, the code to be executed as the result of the user's answer has to deal with the changed data model it was not prepared for. For example, it was supposed to execute changes in the PSI that might be already invalid.

1. Start action 2. invokeLater() 3. Show dialog 4. Modify data 5. Answer dialog 6. Work on invalid data EDTBGT

Passing the modality state solves this problem:

  1. A user action is started.

  2. In the meantime, another operation is scheduled on EDT with Application.invokeLater() (supporting modality state). The operation is scheduled with ModalityState.defaultModalityState() (see the table below for other helper methods).

  3. The action from 1. now shows a dialog asking a Yes/No question. This adds a modal dialog to the modality state stack.

  4. While the dialog is shown, the scheduled operation waits as it was scheduled with a "lower" modality state than the current state with an additional dialog.

  5. The user clicks Yes or No in the dialog, and it executes some code based on the answer.

  6. The code is executed on data in the same state as before the dialog was shown.

  7. The operation from 1. is executed now without interfering with the user's action.

1. Start action 2. invokeLater() 3. Show dialog 4. Wait for dialog close 5. Answer dialog 6. Work on correct data 7. Modify data EDTBGT

The following table presents methods providing useful modality states to be passed to Application.invokeLater():

ModalityState

Description

defaultModalityState()

Used if none specified

If invoked from EDT, it uses the ModalityState.current().

If invoked from a background process started with ProgressManager, the operation can be executed in the same dialog that the process started.

This is the optimal choice in most cases.

current()

The operation can be executed when the modality state stack doesn't grow since the operation was scheduled.

stateForComponent()

The operation can be executed when the topmost shown dialog is the one that contains the specified component or is one of its parent dialogs.

nonModal() or

NON_MODAL

The operation will be executed after all modal dialogs are closed. If any of the open (unrelated) projects displays a per-project modal dialog, the operation will be performed after the dialog is closed.

any()

The operation will be executed as soon as possible regardless of modal dialogs (the same as with SwingUtilities.invokeLater()). It can be used for scheduling only pure UI operations. Modifying PSI, VFS, or project model is prohibited.

Don't use it unless absolutely needed.

Read Action Cancellability

BGT shouldn't hold read locks for a long time. The reason is that if EDT needs a write action (for example, the user types something in the editor), it must be acquired as soon as possible. Otherwise, the UI will freeze until all BGTs have released their read actions. The following diagram presents this problem:

very long read action write action (waiting for the lock) UI freeze write action (executing) BGTEDT

Sometimes, it is required to run a long read action, and it isn't possible to speed it up. In such a case, the recommended approach is to cancel the read action whenever there is a write action about to occur and restart that read action later from scratch:

very long read action RA canceled write action very long read action (2nd attempt) RA restarted from scratch BGTEDT

In this case, the EDT won't be blocked and the UI freeze is avoided. The total execution time of the read action will be longer due to multiple attempts, but not affecting the UI responsiveness is more important.

The canceling approach is widely used in various areas of the IntelliJ Platform: editor highlighting, code completion, "go to class/file/…" actions all work like this. Read the Background Processes section for more details.

Cancellable Read Actions API

To run a cancellable read action, use one of the available APIs:

In both cases, when a read action is started and a write action occurs in the meantime, the read action is marked as canceled. Read actions must check for cancellation often enough to trigger actual cancellation. Although the cancellation mechanism may differ under the hood (Progress API or Kotlin Coroutines), the cancellation handling rules are the same in both cases.

Always check at the start of each read action if the objects are still valid, and if the whole operation still makes sense. With ReadAction.nonBlocking(), use expireWith() or expireWhen() for that.

Avoiding UI Freezes

Don't Perform Long Operations on EDT

In particular, don't traverse VFS, parse PSI, resolve references, or query indexes.

There are still some cases when the platform itself invokes such expensive code (for example, resolve in AnAction.update()), but these are being worked on. Meanwhile, try to speed up what you can in your plugin as it will be generally beneficial and will also improve background highlighting performance.

Action Update

For implementations of AnAction, plugin authors should specifically review the documentation of AnAction.getActionUpdateThread() in the Actions section as it describes how threading works for actions.

Minimize Write Actions Scope

Write actions currently have to happen on EDT. To speed them up, as much as possible should be moved out of the write action into a preparation step which can be then invoked in the background or inside an NBRA.

Slow Operations on EDT Assertion

Some of the long operations are reported by SlowOperations.assertSlowOperationsAreAllowed(). According to its Javadoc, they must be moved to BGT. This can be achieved with the techniques mentioned in the Javadoc, background processes, Application.executeOnPooledThread(), or coroutines (recommended for plugins targeting 2024.1+). Note that the assertion is enabled in IDE EAP versions, internal mode, or development instance, and regular users don't see them in the IDE. This will change in the future, so fixing these exceptions is required.

Event Listeners

Listeners mustn't perform any heavy operations. Ideally, they should only clear some caches.

It is also possible to schedule background processing of events. In such cases, be prepared that some new events might be delivered before the background processing starts – and thus the world might have changed by that moment or even in the middle of background processing. Consider using MergingUpdateQueue and NBRA to mitigate these issues.

VFS Events

Massive batches of VFS events can be pre-processed in the background with AsyncFileListener.

FAQ

How to check whether the current thread is the EDT/UI thread?

Use Application.isDispatchThread().

Why write actions are currently allowed only on EDT?

Reading data model was often performed on EDT to display results in the UI. The IntelliJ Platform is more than 20 years old, and in its beginnings Java didn't offer features like generics and lambdas. Code that acquired read locks was very verbose. For convenience, it was decided that reading data can be done on EDT without read locks (even implicitly acquired).

The consequence of this was that writing had to be allowed only on EDT to avoid read/write conflicts. The nature of EDT provided this possibility out-of-the-box due to being a single thread. Event queue guaranteed that reads and writes were ordered and executed one by one and couldn't interweave.

Why can write intent lock be acquired from any thread but write lock only from EDT?

In the current platform state, technically, write intent lock can be acquired on any thread (it is done only on EDT in practice), but write lock can be acquired only on EDT.

Write intent lock was introduced as a "replacement" for EDT in the context of acquiring write lock. Instead of allowing to acquire write lock on EDT only, it was planned to make it possible to acquire it from under write intent lock on any thread. Write intent lock provides read access that was also available on EDT. This behavior wasn't enabled in production, and the planned locking mechanism has changed. It is planned to allow for acquiring write lock from any thread, even without a write intent lock. Write intent lock will be still available and will allow performing read sessions finished with data writing.

Last modified: 15 August 2024