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:
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.
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:
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 the 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
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 the 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
(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