IntelliJ IDEA 2024.3 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 stack trace, 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)

Threads tab shows two parts of the stack trace – one for the worker thread and another for the scheduling thread

Async stack traces in the console

By default, async stack traces are shown in the console when you debug code or run JUnit/TestNG unit tests.

Async stack trace printed in the console for a failed test

You can disable async stack traces for tests by clearing the Print async stack trace for exceptions checkbox in the corresponding run configuration.

'Print async stack trace for exceptions' checkbox in run configuration dialog

For turning async stack traces on or off for debug sessions, use the Instrumenting agent option in the Settings | Build, execution, deployment | Debugger | Async stack traces.

Async annotations

Async stack traces 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. 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. It is required for matching the parts of an async 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, you can 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 annotations 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. For reference, see Async.java.

  2. Press Ctrl+Alt+S to open 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

Under the hood, this approach uses hidden breakpoints instead of annotations. When such a hidden breakpoint is reached, the specified expression is evaluated, and its result is then used to match the parts of the async stack trace.

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

  1. Press Ctrl+Alt+S to open 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, for example, 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, for example, param_0.

  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: 06 December 2024