Debug asynchronous code
Enable/disable: Settings/Preferences | Build, execution, deployment | Debugger | Async stack traces | Instrumenting agent
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)
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.
note
In order to use async annotations, you need to include the annotations library to your project. If adding a dependency is not an option, you can create your own 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 for some reason 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.
Create your own annotations for the capture point and the insertion point (you can use Async.java for reference).
Press Ctrl+Alt+S to open IDE settings and select Build | Execution | Deployment | Debugger | Async Stack Traces.
Click Configure annotations.
In the Async Annotations Configuration dialog, click 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
tip
Under the hood, this approach uses invisible breakpoints instead of annotations. When such invisible 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.
Press Ctrl+Alt+S to open IDE settings and select Build | Execution | Deployment | Debugger | Async Stack Traces.
- Click 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
, whereN
is the zero-based number of the parameter. Example:doRun
orparam_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
, whereN
is the zero-based number of the parameter. Example:runnable
note
Evaluating complex expressions and method calls may affect performance.
(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