IntelliJ IDEA 2023.2 Help

Tutorial: Debug unresponsive apps

There are a lot of debugger guides that teach you how to set line breakpoints, log values, or evaluate expressions. While this knowledge alone gives you a lot of tools for debugging your application, real-world scenarios may be somewhat trickier and require a more advanced approach.

In this article, we will look at how to locate code without much prior knowledge of the project structure and APIs, debug hanging applications, and fix faulty code on the fly.

The problem

Suppose you have a complex application that hangs when you perform some action. You know how to reproduce the bug, but the difficulty is that you don’t know which part of the code is in charge of this functionality.

The example project for this tutorial has exactly this problem. Let's start with cloning it: https://github.com/flounder4130/debugger-example.

Run the application

  1. Open debugger-example/src/main/java/App.java.

  2. In the gutter, click App actions execute Run Shift+F10, then select Run 'App.main()'.

    A menu appears on clicking Run in the gutter

The user interface of the program has a lot of buttons. If you try clicking them, you might discover that the application hangs for a while when you click Button N.

The example app UI with a lot of buttons

The first thing to do would be to find the code that handles events from this button, but it may be not so easy:

Searching for 'Button N' with Find in Files yields no results.

Let’s see how we can use the debugger to find it.

Method breakpoints

The advantage of method breakpoints over line breakpoints is that they can be used on entire hierarchies of classes. How is this useful in our case?

If you look at the example project, you’ll see that all action classes are derived from the Action interface with a single method: perform(). Setting a method breakpoint in this interface method will suspend the application whenever one of the derived methods is called.

Set a method breakpoint

  • In the Action interface, click the gutter at the perform() method declaration.

    Method breakpoint in the gutter

Let's run the debug session and see where it takes us.

  1. In the gutter, click App actions execute Run, then select Debug 'App.main()'.

  2. In the appication UI, click Button N.

    Clicking Button N in the application UI

The application gets suspended in ActionImpl14. Now we know where the code corresponding to this button is located.

The application gets suspended in an implementation of the method where a method breakpoint was set

Pause application

The approach using method breakpoints works well, but it is based on the assumption that we know something about the parent interface. What if this assumption is wrong, or we cannot use this approach for some other reason? Well, we can even do it without breakpoints.

Pause

  1. Start the debugging session.

  2. Click Button N, and while the application is hanging, go to IntelliJ IDEA.

  3. From the main menu, select Run | Debugging Actions | Pause Program.

The application will be suspended, and we can examine the current state of the threads in the Debug tool window. This gives us an understanding of what the application is doing at the moment. Since it is hanging, we can identify the hanging method and trace back to the call site.

The app is suspended using Pause instead of a breakpoint

This approach has some advantages over a more traditional thread dump, which we’ll cover shortly. For example, it gives you information about variables and reflects the current state of the application, allowing you to resume it at any time.

Thread dumps

Finally, we can use a thread dump, which is not strictly a debugger feature. It is also available when just running the application.

Get a thread dump

  1. Start the debugging session.

  2. Click Button N, and while the application is hanging, go to IntelliJ IDEA.

  3. From the main menu, select Run | Debugging Actions | Get Thread Dump. The captured thread dump opens in the Debug tool window.

  4. Scan through the available threads on the left, and in AWT-EventQueue you’ll see what is causing the problem.

    Thread dump opens in the Debug tool window

The downside of thread dumps is that they only provide a snapshot of the program state at the time when they were made. You can’t use thread dumps to explore variables or control the program’s execution.

In our example, we don’t need to resort to a thread dump. However, we still wanted to mention this technique as it may be useful in other cases, like when you are trying to debug an application that has been launched without the debug agent.

Understanding the issue

Regardless of the method used, we arrive at ActionImpl14. In this class, someone intended to perform the work in a separate thread, but confused Thread.start() with Thread.run(), which runs the code in the same thread as the calling code.

IntelliJ IDEA’s static analyzer even warns us about this at design time:

IntelliJ IDEA warns about suspicious call to run() and suggests to replace it with start()

A method that does heavy lifting (or heavy sleeping in this case) is called on the UI thread and blocks it until it finishes. That’s why we cannot do anything in the UI for some time after we click Button N.

HotSwap

Now that we’ve discovered the cause of the bug, let’s fix the issue.

We could stop the program, recompile the code, and then rerun it. However, it is not always convenient to redeploy the entire application just because of a small change.

Reload classes at runtime

  1. Correct the code:

    package actions; public class ActionImpl14 implements Action { @Override public void perform() { new Thread(() -> { try { // intense calculation Thread.sleep(15000); } catch (InterruptedException ignored) { } }).start(); } }
  2. After the code is good to go, click Run | Debugging Actions | Reload Changed Classes. A balloon appears, confirming that the new code has made its way to the VM.

    HotSwap confirmation balloon
  3. Return to the application and check the fix. Clicking Button N should no longer hangs the app.

Keep in mind that HotSwap has its limitations. There are tools like DCEVM or JRebel that offer extended HotSwap capabilities. If you are interested, you can read more about them.

Summary

Using our reasoning and a couple of debugger features, we were able to locate the code that was causing a UI freeze in our project. We also learned how the Pause feature and thread dumps can be useful in cases where an app is not responding. Finally, we fixed the running application without wasting any time on recompilation and redeployment, which can be lengthy in big projects.

Of course, we have used a simplified project keep things brief, however, the same techniques can help you in real-life projects, and we hope the gained knowledge will come in handy in your developer journey.

Last modified: 21 September 2023