IntelliJ IDEA 2023.2 Help

Debug asynchronous code

Debugging asynchronous code is a challenge because the tasks are often scheduled in one thread and executed in another. Every thread has its own stacktrace, making it difficult to figure out what happened before the thread started.

IntelliJ IDEA makes it easier by establishing a connection between frames in different threads. This allows you to look back from a worker thread to the place where the task was scheduled and debug the program as if the execution was all in the same thread.

To try async stack traces, debug the following example:

import java.util.*; import java.util.concurrent.*; public class AsyncExample { static List<Task> tasks = new ArrayList<>(); static ExecutorService executor = Executors.newScheduledThreadPool(4); public static void main(String[] args) { createTasks(); executeTasks(); } private static void createTasks() { for (int i = 0; i < 20; i++) { tasks.add(new Task(i)); } } private static void executeTasks() { for (Task task : tasks) { executor.submit(task); } } static class Task extends Thread { int num; public void run() { try { Thread.sleep(new Random().nextInt(2000)); } catch (InterruptedException e) { e.printStackTrace(); } printNum(); } private void printNum() { // Set a breakpoint at the following line System.out.print(num + " "); } public Task(int num) { this.num = num; } } }

When we stop at the breakpoint in the printNum() method, there are two stacktraces available to us:

  • The current thread (worker)

  • The main thread (where the task was scheduled)

Async stacktraces

Use async annotations

Async stacktraces work out of the box with Swing and Java Concurrency API, but can also be manually extended to work with your own custom classes. This is done using special annotations.

Annotations are used to define capture and insertion points:

  • A capture point is a method where the stack trace is captured. At capture points, stack traces are stored and assigned a key. Capture points are marked with the @Async.Schedule annotation.

  • An insertion point is a method where one of the previously stored stack traces is attached to the current stack. The stacks are matched by key. Insertion points are marked with the @Async.Execute annotation.

  • A key is a parameter or object reference that serves as a unique identifier for a captured stack trace.

Define capture and insertion points

You can annotate either methods or their parameters:

  • If you want the object reference (this) to be used as the key, annotate the method itself, for example:

    @Async.Schedule private static void schedule(Integer i) { System.out.println("Scheduling " + i); queue.put(i); }
  • If you want the parameter value to be used as the key, annotate the method parameter, for example:

    private static void schedule(@Async.Schedule Integer i) { System.out.println("Scheduling " + i); queue.put(i); }

To test how annotations work, let's use the following example:

import org.jetbrains.annotations.Async; import java.util.concurrent.*; public class AsyncSchedulerExample { private static final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(); public static void main(String[] args) throws InterruptedException { new Thread(() -> { try { while (true) { process(queue.take()); } } catch (InterruptedException e) { e.printStackTrace(); } }).start(); schedule(1); schedule(2); schedule(3); } private static void schedule(@Async.Schedule Integer i) throws InterruptedException { System.out.println("Scheduling " + i); queue.put(i); } private static void process(@Async.Execute Integer i) { // Set a breakpoint at the following line System.out.println("Processing " + i); } }

Define custom annotations

If you don't want to add the JetBrains Maven repository as a dependency for your project, you can define your own annotations and use them instead of the default ones.

  1. Create your own annotations for the capture point and the insertion point (you can use Async.java for reference).

  2. Press Control+Alt+S to open the IDE settings and then select Build | Execution | Deployment | Debugger | Async Stack Traces.

  3. Click Configure annotations.

  4. In the Async Annotations Configuration dialog, click add to add your custom annotations to Async Schedule annotations and Async Execute annotations.

Advanced configuration

Annotation-based approach relies on the instrumenting agent and works in most cases. There is a way to delegate all the work solely to the debugger. This may be required if you:

  • need to capture local variables

  • cannot use annotations

  • cannot use the instrumenting agent

There is a tradeoff between flexibility and performance. This option is not recommended for highly concurrent projects where performance is critical.

  1. Press Control+Alt+S to open the IDE settings and then select Build | Execution | Deployment | Debugger | Async Stack Traces.

  2. Click add and provide the following information:

    • Capture class name: fully-qualified name of the class where stack traces should be captured, for example, javax.swing.SwingUtilities

    • Capture method name: method name without parameter list and parentheses, for example, invokeLater

    • Capture key expression: the expression whose result will be used as the key. In expressions, you can use everything that is reachable in the context of the frame. Method parameters can be specified as param_N, where N is the zero-based number of the parameter. Example: doRun or param_0

    • Insert class name: fully-qualified name of the class where stack traces should be matched, for example, java.awt.event.InvocationEvent

    • Insert method name: method name without parameter list and parentheses, for example, dispatch

    • Insert key expression: the expression whose result will be used as the key. In expressions, you can use everything that is reachable in the context of the frame. Method parameters can be specified as param_N, where N is the zero-based number of the parameter. Example: runnable

  3. (Optional) If you also want to capture local variables (primitives and String values together with the call stack, select the Capture local variables option. Note that this may slow down the debugging process.

You can download additional capture settings from the following repository: IntelliJ IDEA debugger Capture Points

View async stack traces in remote JVMs

If you are debugging a remote process, for example, managed within a Docker container, you can still use the JVM Instrumenting Agent to display Async Stack Traces as if it were started from the IDE.

To use the agent remotely, do the following:

  • copy <IDEA installation folder>/lib/rt/debugger-agent.jar to any location on the remote machine

  • add -javaagent:<path to debugger-agent.jar> to the remote JVM options

Last modified: 23 August 2023