| /* |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2.1 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| */ |
| |
| package org.jdesktop.swinghelper.debug; |
| |
| import java.awt.*; |
| import java.awt.event.*; |
| import java.lang.management.*; |
| import java.util.*; |
| import java.util.Timer; |
| |
| import javax.swing.*; |
| |
| /** |
| * Monitors the AWT event dispatch thread for events that take longer than |
| * a certain time to be dispatched. |
| * <p/> |
| * The principle is to record the time at which we start processing an event, |
| * and have another thread check frequently to see if we're still processing. |
| * If the other thread notices that we've been processing a single event for |
| * too long, it prints a stack trace showing what the event dispatch thread |
| * is doing, and continues to time it until it finally finishes. |
| * <p/> |
| * This is useful in determining what code is causing your Java application's |
| * GUI to be unresponsive. |
| * |
| * <p>The original blog can be found here<br> |
| * <a href="http://elliotth.blogspot.com/2005/05/automatically-detecting-awt-event.html"> |
| * Automatically detecting AWT event dispatch thread hangs</a> |
| * </p> |
| * |
| * @author Elliott Hughes <enh@jessies.org> |
| * |
| * Advice, bug fixes, and test cases from |
| * Alexander Potochkin and Oleg Sukhodolsky. |
| * |
| * https://swinghelper.dev.java.net/ |
| */ |
| public final class EventDispatchThreadHangMonitor extends EventQueue { |
| private static final EventDispatchThreadHangMonitor INSTANCE = new EventDispatchThreadHangMonitor(); |
| |
| // Time to wait between checks that the event dispatch thread isn't hung. |
| private static final long CHECK_INTERVAL_MS = 100; |
| |
| // Maximum time we won't warn about. This used to be 500 ms, but 1.5 on |
| // late-2004 hardware isn't really up to it; there are too many parts of |
| // the JDK that can go away for that long (often code that has to be |
| // called on the event dispatch thread, like font loading). |
| private static final long UNREASONABLE_DISPATCH_DURATION_MS = 1000; |
| |
| // Help distinguish multiple hangs in the log, and match start and end too. |
| // Only access this via getNewHangNumber. |
| private static int hangCount = 0; |
| |
| // Prevents us complaining about hangs during start-up, which are probably |
| // the JVM vendor's fault. |
| private boolean haveShownSomeComponent = false; |
| |
| // The currently outstanding event dispatches. The implementation of |
| // modal dialogs is a common cause for multiple outstanding dispatches. |
| private final LinkedList<DispatchInfo> dispatches = new LinkedList<DispatchInfo>(); |
| |
| private static class DispatchInfo { |
| // The last-dumped hung stack trace for this dispatch. |
| private StackTraceElement[] lastReportedStack; |
| // If so; what was the identifying hang number? |
| private int hangNumber; |
| |
| // The EDT for this dispatch (for the purpose of getting stack traces). |
| // I don't know of any API for getting the event dispatch thread, |
| // but we can assume that it's the current thread if we're in the |
| // middle of dispatching an AWT event... |
| // We can't cache this because the EDT can die and be replaced by a |
| // new EDT if there's an uncaught exception. |
| private final Thread eventDispatchThread = Thread.currentThread(); |
| |
| // The last time in milliseconds at which we saw a dispatch on the above thread. |
| private long lastDispatchTimeMillis = System.currentTimeMillis(); |
| |
| public DispatchInfo() { |
| // All initialization is done by the field initializers. |
| } |
| |
| public void checkForHang() { |
| if (timeSoFar() > UNREASONABLE_DISPATCH_DURATION_MS) { |
| examineHang(); |
| } |
| } |
| |
| // We can't use StackTraceElement.equals because that insists on checking the filename and line number. |
| // That would be version-specific. |
| private static boolean stackTraceElementIs(StackTraceElement e, String className, String methodName, boolean isNative) { |
| return e.getClassName().equals(className) && e.getMethodName().equals(methodName) && e.isNativeMethod() == isNative; |
| } |
| |
| // Checks whether the given stack looks like it's waiting for another event. |
| // This relies on JDK implementation details. |
| private boolean isWaitingForNextEvent(StackTraceElement[] currentStack) { |
| return stackTraceElementIs(currentStack[0], "java.lang.Object", "wait", true) && stackTraceElementIs(currentStack[1], "java.lang.Object", "wait", false) && stackTraceElementIs(currentStack[2], "java.awt.EventQueue", "getNextEvent", false); |
| } |
| |
| private void examineHang() { |
| StackTraceElement[] currentStack = eventDispatchThread.getStackTrace(); |
| |
| if (isWaitingForNextEvent(currentStack)) { |
| // Don't be fooled by a modal dialog if it's waiting for its next event. |
| // As long as the modal dialog's event pump doesn't get stuck, it's okay for the outer pump to be suspended. |
| return; |
| } |
| |
| if (stacksEqual(lastReportedStack, currentStack)) { |
| // Don't keep reporting the same hang every time the timer goes off. |
| return; |
| } |
| |
| hangNumber = getNewHangNumber(); |
| String stackTrace = stackTraceToString(currentStack); |
| lastReportedStack = currentStack; |
| Log.warn("(hang #" + hangNumber + ") event dispatch thread stuck processing event for " + timeSoFar() + " ms:" + stackTrace); |
| checkForDeadlock(); |
| } |
| |
| private static boolean stacksEqual(StackTraceElement[] a, StackTraceElement[] b) { |
| if (a == null) { |
| return false; |
| } |
| if (a.length != b.length) { |
| return false; |
| } |
| for (int i = 0; i < a.length; ++i) { |
| if (a[i].equals(b[i]) == false) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Returns how long this dispatch has been going on (in milliseconds). |
| */ |
| private long timeSoFar() { |
| return (System.currentTimeMillis() - lastDispatchTimeMillis); |
| } |
| |
| public void dispose() { |
| if (lastReportedStack != null) { |
| Log.warn("(hang #" + hangNumber + ") event dispatch thread unstuck after " + timeSoFar() + " ms."); |
| } |
| } |
| } |
| |
| private EventDispatchThreadHangMonitor() { |
| initTimer(); |
| } |
| |
| /** |
| * Sets up a timer to check for hangs frequently. |
| */ |
| private void initTimer() { |
| final long initialDelayMs = 0; |
| final boolean isDaemon = true; |
| Timer timer = new Timer("EventDispatchThreadHangMonitor", isDaemon); |
| timer.schedule(new HangChecker(), initialDelayMs, CHECK_INTERVAL_MS); |
| } |
| |
| private class HangChecker extends TimerTask { |
| @Override |
| public void run() { |
| synchronized (dispatches) { |
| if (dispatches.isEmpty() || !haveShownSomeComponent) { |
| // Nothing to do. |
| // We don't destroy the timer when there's nothing happening |
| // because it would mean a lot more work on every single AWT |
| // event that gets dispatched. |
| return; |
| } |
| // Only the most recent dispatch can be hung; nested dispatches |
| // by their nature cause the outer dispatch pump to be suspended. |
| dispatches.getLast().checkForHang(); |
| } |
| } |
| } |
| |
| /** |
| * Sets up hang detection for the event dispatch thread. |
| */ |
| public static void initMonitoring() { |
| Toolkit.getDefaultToolkit().getSystemEventQueue().push(INSTANCE); |
| } |
| |
| /** |
| * Overrides EventQueue.dispatchEvent to call our pre and post hooks either |
| * side of the system's event dispatch code. |
| */ |
| @Override |
| protected void dispatchEvent(AWTEvent event) { |
| try { |
| preDispatchEvent(); |
| super.dispatchEvent(event); |
| } finally { |
| postDispatchEvent(); |
| if (!haveShownSomeComponent && |
| event instanceof WindowEvent && event.getID() == WindowEvent.WINDOW_OPENED) { |
| haveShownSomeComponent = true; |
| } |
| } |
| } |
| |
| private void debug(String which) { |
| if (false) { |
| for (int i = dispatches.size(); i >= 0; --i) { |
| System.out.print(' '); |
| } |
| System.out.println(which); |
| } |
| } |
| |
| /** |
| * Starts tracking a dispatch. |
| */ |
| private synchronized void preDispatchEvent() { |
| debug("pre"); |
| synchronized (dispatches) { |
| dispatches.addLast(new DispatchInfo()); |
| } |
| } |
| |
| /** |
| * Stops tracking a dispatch. |
| */ |
| private synchronized void postDispatchEvent() { |
| synchronized (dispatches) { |
| // We've finished the most nested dispatch, and don't need it any longer. |
| DispatchInfo justFinishedDispatch = dispatches.removeLast(); |
| justFinishedDispatch.dispose(); |
| |
| // The other dispatches, which have been waiting, need to be credited extra time. |
| // We do this rather simplistically by pretending they've just been redispatched. |
| Thread currentEventDispatchThread = Thread.currentThread(); |
| for (DispatchInfo dispatchInfo : dispatches) { |
| if (dispatchInfo.eventDispatchThread == currentEventDispatchThread) { |
| dispatchInfo.lastDispatchTimeMillis = System.currentTimeMillis(); |
| } |
| } |
| } |
| debug("post"); |
| } |
| |
| private static void checkForDeadlock() { |
| ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); |
| long[] threadIds = threadBean.findMonitorDeadlockedThreads(); |
| if (threadIds == null) { |
| return; |
| } |
| Log.warn("deadlock detected involving the following threads:"); |
| ThreadInfo[] threadInfos = threadBean.getThreadInfo(threadIds, Integer.MAX_VALUE); |
| for (ThreadInfo info : threadInfos) { |
| Log.warn("Thread #" + info.getThreadId() + " " + info.getThreadName() + |
| " (" + info.getThreadState() + ") waiting on " + info.getLockName() + |
| " held by " + info.getLockOwnerName() + stackTraceToString(info.getStackTrace())); |
| } |
| } |
| |
| private static String stackTraceToString(StackTraceElement[] stackTrace) { |
| StringBuilder result = new StringBuilder(); |
| // We used to avoid showing any code above where this class gets |
| // involved in event dispatch, but that hides potentially useful |
| // information when dealing with modal dialogs. Maybe we should |
| // reinstate that, but search from the other end of the stack? |
| for (StackTraceElement stackTraceElement : stackTrace) { |
| String indentation = " "; |
| result.append("\n" + indentation + stackTraceElement); |
| } |
| return result.toString(); |
| } |
| |
| private synchronized static int getNewHangNumber() { |
| return ++hangCount; |
| } |
| |
| public static void main(String[] args) { |
| initMonitoring(); |
| //special case for deadlock test |
| if (args.length > 0 && "deadlock".equals(args[0])) { |
| EventDispatchThreadHangMonitor.INSTANCE.haveShownSomeComponent = true; |
| Tests.runDeadlockTest(); |
| return; |
| } |
| Tests.main(args); |
| } |
| |
| private static class Tests { |
| public static void main(final String[] args) { |
| |
| java.awt.EventQueue.invokeLater(new Runnable() { |
| public void run() { |
| for (String arg : args) { |
| final JFrame frame = new JFrame(); |
| frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); |
| frame.setLocationRelativeTo(null); |
| if (arg.equals("exception")) { |
| runExceptionTest(frame); |
| } else if (arg.equals("focus")) { |
| runFocusTest(frame); |
| } else if (arg.equals("modal-hang")) { |
| runModalTest(frame, true); |
| } else if (arg.equals("modal-no-hang")) { |
| runModalTest(frame, false); |
| } else { |
| System.err.println("unknown regression test '" + arg + "'"); |
| System.exit(1); |
| } |
| frame.pack(); |
| frame.setVisible(true); |
| } |
| } |
| }); |
| } |
| |
| private static void runDeadlockTest() { |
| class Locker { |
| private Locker locker; |
| |
| public void setLocker(Locker locker) { |
| this.locker = locker; |
| } |
| |
| public synchronized void tryToDeadlock() { |
| locker.toString(); |
| } |
| |
| public synchronized String toString() { |
| try { |
| Thread.sleep(50); |
| } catch (InterruptedException e) { |
| e.printStackTrace(); |
| } |
| return super.toString(); |
| } |
| } |
| final Locker one = new Locker(); |
| final Locker two = new Locker(); |
| one.setLocker(two); |
| two.setLocker(one); |
| |
| //Deadlock expected here: |
| for (int i = 0; i < 100; i++) { |
| SwingUtilities.invokeLater(new Runnable() { |
| public void run() { |
| one.tryToDeadlock(); |
| } |
| }); |
| two.tryToDeadlock(); |
| } |
| } |
| |
| // If we don't do our post-dispatch activity in a finally block, we'll |
| // report bogus hangs. |
| private static void runExceptionTest(final JFrame frame) { |
| JButton button = new JButton("Throw Exception"); |
| button.addActionListener(new ActionListener() { |
| public void actionPerformed(ActionEvent e) { |
| // This shouldn't cause us to report a hang. |
| throw new RuntimeException("Nobody expects the Spanish Inquisition!"); |
| } |
| }); |
| frame.add(button); |
| } |
| |
| // A demonstration of nested calls to dispatchEvent caused by SequencedEvent. |
| private static void runFocusTest(final JFrame frame) { |
| final JDialog dialog = new JDialog(frame, "Non-Modal Dialog"); |
| dialog.add(new JLabel("Close me!")); |
| dialog.pack(); |
| dialog.setLocationRelativeTo(frame); |
| dialog.addWindowFocusListener(new WindowAdapter() { |
| public void windowGainedFocus(WindowEvent e) { |
| System.out.println("FocusTest.windowGainedFocus"); |
| // If you don't cope with nested calls to dispatchEvent, you won't detect this. |
| // See java.awt.SequencedEvent for an example. |
| sleep(2500); |
| } |
| }); |
| JButton button = new JButton("Show Non-Modal Dialog"); |
| button.addActionListener(new ActionListener() { |
| public void actionPerformed(ActionEvent e) { |
| dialog.setVisible(true); |
| } |
| }); |
| frame.add(button); |
| } |
| |
| // A demonstration of the problems of dealing with modal dialogs. |
| private static void runModalTest(final JFrame frame, final boolean shouldSleep) { |
| System.out.println(shouldSleep ? "Expect hangs!" : "There should be no hangs..."); |
| JButton button = new JButton("Show Modal Dialog"); |
| button.addActionListener(new ActionListener() { |
| public void actionPerformed(ActionEvent e) { |
| if (shouldSleep) { |
| sleep(2500); // This is easy. |
| } |
| JDialog dialog = new JDialog(frame, "Modal dialog", true); |
| dialog.setLayout(new FlowLayout()); |
| dialog.add(new JLabel("Close this dialog!")); |
| final JLabel label = new JLabel(" "); |
| dialog.add(label); |
| dialog.pack(); |
| dialog.setLocation(frame.getX() - 100, frame.getY()); |
| |
| // Make sure the new event pump has some work to do, each unit of which is insufficient to cause a hang. |
| new Thread(new Runnable() { |
| public void run() { |
| for (int i = 0; i <= 100000; ++i) { |
| final int value = i; |
| EventQueue.invokeLater(new Runnable() { |
| public void run() { |
| label.setText(Integer.toString(value)); |
| } |
| }); |
| } |
| } |
| }).start(); |
| |
| dialog.setVisible(true); |
| |
| if (shouldSleep) { |
| sleep(2500); // If you don't distinguish different stack traces, you won't report this. |
| } |
| } |
| }); |
| frame.add(button); |
| } |
| |
| private static void sleep(long ms) { |
| try { |
| System.out.println("Sleeping for " + ms + " ms on " + Thread.currentThread() + "..."); |
| Thread.sleep(ms); |
| System.out.println("Finished sleeping..."); |
| } catch (Exception ex) { |
| ex.printStackTrace(); |
| } |
| } |
| } |
| |
| private static class Log { |
| public static void warn(String str) { |
| System.out.println(str); |
| } |
| } |
| } |