2004-07-20

Diagnosing AWT thread issues

There are two ways a Java GUI program can exit. There's the right way, and there's the wrong way. Sadly, the wrong way is easy, and the right way often fails to work.

The wrong way is to call System.exit. This always works, but the first time your code gets embedded inside some larger application, you'll lose someone their work. "Crash only" programming is all well and good, but you don't know the outer application is written like that, even if yours is. So don't do that.

The right way is to have no displayable components, no native events in the native event queue, and no AWT events in Java event queues. See AWT Thread Issues. The trouble with this is that it requires that you write a nice clean program. And that everything you use is equally nice and clean. Which is often not the case.

When things go wrong, if you're lucky, it's your mistake, and it's in code you've just added. Insert the missing call to dispose, and you're laughing.

This one was a bit harder though, and in the end I wrote a class to automate the work.

Jumping to the money shot, here's the output from my new debugging aid when I close the last Frame in the program in question:


mercury:~/Projects/scm$ revisiontool src/e/scm/RevisionTool.java
*** Examining Frames...
Extant frames: 1
Problem (displayable) frames: 0
*** Examining Swing TimerQueue...
javax.swing.Timer@b30913
listener #0 apple.laf.AquaProgressBarUI$Animator@18f51f
Problem (extant) timers: 1


That seems a pretty clear indication of the source of the trouble to me.

I know the output makes it look like an Apple problem, but it was originally in Sun code. The bug turns out to have been fixed in Java 1.5.0-beta2, which explains why other people were seeing it, but I only saw it on Mac OS (with 1.4.2) and not on Linux (with 1.5.0-beta2). Indeterminate progress bars are the cause of the problem. (Bug 4995929, if you're interested. There's a work-around there, too, if you can't move to 1.5.0-beta2.)

All I had to do was add the following line to the constructor of the application's JFrame subclass:

e.debug.HungAwtExit.explain(this);

The implementation is pretty straightforward. Frame.getFrames takes care of the Frames, and a little bit of reflection lets us wander down the TimerQueue's list. The latter is, of course, pretty brittle, but what can you do when the class is default access?

Anyway, for those who don't want to follow the link to my home page, and check out my library of useful stuff, here's the class:

package e.debug;

import java.awt.*;
import java.awt.event.*;
import java.lang.reflect.*;

/**
* Diagnoses a GUI application that doesn't exit when you close what
* you think is the last Frame.
*
* The two most common problems in my experience involve Frames that
* haven't had Window.dispose invoked on them, and timers. This class
* offers a static method "explain" that you should use to register
* the Frame that you'll be closing last. (I'd guess it's easiest to
* register them all, but there's no need to.)
*
* In a constructor, something like e.debug.HungAwtExit.explain(this);
* would do it.
*/
public class HungAwtExit {
public static void showDisplayableFrames() {
System.err.println("*** Examining Frames...");
Frame[] frames = Frame.getFrames();
System.err.println("Extant frames: " + frames.length);
int displayableFrameCount = 0;
for (int i = 0; i < frames.length; ++i) {
if (frames[i].isDisplayable()) {
System.err.println("Displayable frame: " + frames[i]);
++displayableFrameCount;
}
}
System.err.println("Problem (displayable) frames: " +
displayableFrameCount);
}

public static void showSwingTimerQueue() {
System.err.println("*** Examining Swing TimerQueue...");
try {
Class timerQueueClass = Class.forName("javax.swing.TimerQueue");
Method sharedInstanceMethod =
timerQueueClass.getDeclaredMethod("sharedInstance", null);
sharedInstanceMethod.setAccessible(true);
Object sharedInstance = sharedInstanceMethod.invoke(null, null);

Field firstTimerField =
timerQueueClass.getDeclaredField("firstTimer");
firstTimerField.setAccessible(true);
Field nextTimerField =
javax.swing.Timer.class.getDeclaredField("nextTimer");
nextTimerField.setAccessible(true);

int extantTimers = 0;
javax.swing.Timer nextTimer =
(javax.swing.Timer) firstTimerField.get(sharedInstance);
while (nextTimer != null) {
++extantTimers;
System.err.println(nextTimer);
showActionListeners(nextTimer.getActionListeners());

nextTimer = (javax.swing.Timer) nextTimerField.get(nextTimer);
}
System.err.println("Problem (extant) timers: " + extantTimers);
} catch (Exception ex) {
ex.printStackTrace();
}
}

private static void showActionListeners(ActionListener[] listeners) {
for (int i = 0; i < listeners.length; ++i) {
System.err.println(" listener #" + i + " " + listeners[i]);
}
}

public static void explain(Frame f) {
f.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
showDisplayableFrames();
showSwingTimerQueue();
}
});
}

private HungAwtExit() {
// Prevents instantiation.
}
}