Blog

Product news, updates, and insights from ej-technologies.

Read with RSS. Subscribe by email. Follow on .

The power of async tracking in JVM profiling

2025-07-25
Posted by Ingo Kegel

Async operations can speed up applications and improve responsiveness, but they also introduce complexity. Especially in the context of profiling, understanding what really happened and why can be surprisingly tricky. This post shows how JProfiler's async tracking feature helps fix hard performance problems in your application.

Why are async calls important?

Unless you are fortunate enough to have virtual threads at your disposal, making async calls is the only way to stop blocking the current thread while a task is being executed. Many systems and frameworks have a canonical way of doing this, such as submitting a CompletableFuture to an ExecutorService. In an async call, the task is executed by a different thread, and the result (if one is even needed) can be processed by a callback.

UI systems are typically built in such a way that there is a single event dispatch thread that can modify and repaint UI components. Web browsers are built this way, but also all common JVM UI toolkits, like Swing, SWT and JavaFX.

The problem

A common problem with async calls is that a stack trace from the deferred task has no connection to the thread that initiated the task. This makes debugging notoriously difficult ("callback hell"), and it's an even greater problem for profiling. In most cases, you don't look at methods in isolation, but you always want to attribute the cumulative measured time of any method to the parents in the call stack. In this way, you find out who is responsible for a performance problem which helps you to fix it.

Let's look at an example. The call tree below shows the install4j UI while the user hovers over a single item in the view selector without any visual change. The consumed times are quite a bit more than we would expect. The root node in the call tree is the event dispatch thread and contains unprofiled classes in its internal machinery. Below it, we directly see the NavigationTree.paintComponent method that runs on the event dispatch thread. There is no indication of who triggered this method, either via an EventQueue.invokeLater or an EventQueue.invokeAndWait call.

Blog figureBlog figure

Enter async tracking

With JProfiler, you have a tool at your disposal that can stitch together async call sites with async execution sites. In the profiled example, async tracking for AWT was active. This is off by default, so you need to switch on the particular kind of tracking that's useful for your application by clicking on the tracking icon in the status bar or showing it in the "Profiling" menu.

Blog figureBlog figure

When async tracking is enabled, a hyperlink label is shown above the call tree. When you click on it, JProfiler calculates a call tree where all tracked async calls are inlined.

Blog figureBlog figure

The "Add async execution time to tree" checkbox above the call tree is important. If you select it, the time spent in the async execution is added to the parent node in the call tree. It then effectively treats async calls as if they were blocking calls. This is very useful to find hot paths in the async execution sites by starting from the top of the tree.

Fixing the problem

In the async call tree, we see that the mouseMoved method of the MouseMotionListener in NavigationComponentHelper issues a lot of repaints. We can now check the source code of that method and prevent unneeded repaints where the visual representation of the view selector does not change.

This example translates to many real-world scenarios with the other supported tracking types. Async support in your profiling tool is a crucial feature that enables you to productively fix performance problems in your application. Try it out with the latest JProfiler version and convince yourself of the power of async tracking!

Archive