Code refactor and update the implementation of isWaitingForNextEvent Change-Id: Ic6f4db7cc91c2e4c18d921704f638cfdc7299e2c
diff --git a/src/main/java/org/jdesktop/swinghelper/debug/DispatchInfo.java b/src/main/java/org/jdesktop/swinghelper/debug/DispatchInfo.java new file mode 100644 index 0000000..f675efc --- /dev/null +++ b/src/main/java/org/jdesktop/swinghelper/debug/DispatchInfo.java
@@ -0,0 +1,146 @@ +package org.jdesktop.swinghelper.debug; + +import static java.lang.Math.min; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +final class DispatchInfo { + + // Provides the index to each hang event. + private static final AtomicInteger hangCount = new AtomicInteger(0); + + // 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). + final Thread eventDispatchThread = Thread.currentThread(); + + // The last time in milliseconds at which we saw a dispatch on the above thread. + long lastDispatchTimeMillis = System.currentTimeMillis(); + + private final Consumer<String> logConsumer; + + DispatchInfo(Consumer<String> logConsumer) { + this.logConsumer = logConsumer; + } + + void checkForHang() { + if (timeSoFar() > EventDispatchThreadHangMonitor.UNREASONABLE_DISPATCH_DURATION_MS) { + examineHang(); + } + } + + 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)) { + // Skips reporting the same hang every time the timer goes off. + return; + } + + lastReportedStack = currentStack; + hangNumber = hangCount.addAndGet(/* delta= */ 1); + + logConsumer.accept( + "(hang #" + + hangNumber + + ") event dispatch thread stuck processing event for " + + timeSoFar() + + " ms:" + + stackTraceToString(currentStack)); + checkForDeadlock(); + } + + private long timeSoFar() { + return System.currentTimeMillis() - lastDispatchTimeMillis; + } + + public void dispose() { + if (lastReportedStack != null) { + logConsumer.accept( + "(hang #" + hangNumber + ") event dispatch thread unstuck after " + timeSoFar() + " ms."); + } + } + + // Checks whether the given stack looks like it's waiting for another event. + // This relies on JDK implementation details. + private static boolean isWaitingForNextEvent(StackTraceElement[] currentStack) { + for (int i = 0; i < min(5, currentStack.length); i++) { + if (stackTraceElementIs(currentStack[i], "java.awt.EventQueue", "getNextEvent", false)) { + return true; + } + } + return false; + } + + // 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; + } + + private 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").append(indentation).append(stackTraceElement); + } + return result.toString(); + } + + private 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])) { + return false; + } + } + return true; + } + + private void checkForDeadlock() { + ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); + long[] threadIds = threadBean.findMonitorDeadlockedThreads(); + if (threadIds == null) { + return; + } + logConsumer.accept("Deadlock detected involving the following threads:"); + for (ThreadInfo info : threadBean.getThreadInfo(threadIds, Integer.MAX_VALUE)) { + logConsumer.accept( + String.format( + "Thread #%d %s (%s) waiting on lock(%s) held by %s stack:%s", + info.getThreadId(), + info.getThreadName(), + info.getThreadState(), + info.getLockName(), + info.getLockOwnerName(), + stackTraceToString(info.getStackTrace()))); + } + } +} \ No newline at end of file
diff --git a/src/main/java/org/jdesktop/swinghelper/debug/EventDispatchThreadHangMonitor.java b/src/main/java/org/jdesktop/swinghelper/debug/EventDispatchThreadHangMonitor.java index 47541af..dbc8295 100644 --- a/src/main/java/org/jdesktop/swinghelper/debug/EventDispatchThreadHangMonitor.java +++ b/src/main/java/org/jdesktop/swinghelper/debug/EventDispatchThreadHangMonitor.java
@@ -1,463 +1,118 @@ -/* - * 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 static java.util.concurrent.TimeUnit.MILLISECONDS; -import javax.swing.*; +import java.awt.AWTEvent; +import java.awt.EventQueue; +import java.awt.Toolkit; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Consumer; /** - * Monitors the AWT event dispatch thread for events that take longer than - * a certain time to be dispatched. + * 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. + * 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> + * This is useful in determining what code is causing your Java application's GUI to be + * unresponsive. * - * @author Elliott Hughes <enh@jessies.org> - * - * Advice, bug fixes, and test cases from - * Alexander Potochkin and Oleg Sukhodolsky. - * - * https://swinghelper.dev.java.net/ + * <p>The original source code can be found here<br> + * <a href="https://github.com/floscher/swinghelper/blob/master/src/java/org/jdesktop/swinghelper/debug/EventDispatchThreadHangMonitor.java"> + * EventDispatchThreadHangMonitor.java</a> + * </p> */ -public final class EventDispatchThreadHangMonitor extends EventQueue { - private static final EventDispatchThreadHangMonitor INSTANCE = new EventDispatchThreadHangMonitor(); +public final class EventDispatchThreadHangMonitor extends EventQueue implements Runnable { - // Time to wait between checks that the event dispatch thread isn't hung. - private static final long CHECK_INTERVAL_MS = 100; + // 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; + // 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). + 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; + // The currently outstanding event dispatches. The implementation of + // modal dialogs is a common cause for multiple outstanding dispatches. + private final Deque<DispatchInfo> dispatches = new ArrayDeque<>(); - // Prevents us complaining about hangs during start-up, which are probably - // the JVM vendor's fault. - private boolean haveShownSomeComponent = false; + // The scheduler to regularly check whether a hang event is detected or not. + private final ScheduledExecutorService scheduler = + Executors.newSingleThreadScheduledExecutor( + runnable -> { + Thread t = new Thread(runnable); + t.setDaemon(true); + return t; + }); - // 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 final Consumer<String> logConsumer; - 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; + /** + * Constructs {@link EventDispatchThreadHangMonitor}. + */ + public EventDispatchThreadHangMonitor() { + this(System.out::println); + } - // 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(); + /** + * Constructs {@link EventDispatchThreadHangMonitor}. + * + * @param logConsumer the custom log consumer + */ + public EventDispatchThreadHangMonitor(Consumer<String> logConsumer) { + this.logConsumer = logConsumer; + } - // The last time in milliseconds at which we saw a dispatch on the above thread. - private long lastDispatchTimeMillis = System.currentTimeMillis(); + /** + * Sets up hang detection for the event dispatch thread. + */ + public void initMonitoring() { + Toolkit.getDefaultToolkit().getSystemEventQueue().push(this); + Future<?> unused = scheduler.scheduleWithFixedDelay(this, 0, CHECK_INTERVAL_MS, MILLISECONDS); + } - public DispatchInfo() { - // All initialization is done by the field initializers. + @Override + protected void dispatchEvent(AWTEvent event) { + DispatchInfo currentDispatchInfo = new DispatchInfo(logConsumer); + try { + synchronized (dispatches) { + dispatches.addLast(currentDispatchInfo); + } + super.dispatchEvent(event); + } finally { + synchronized (dispatches) { + // We've finished the most nested dispatch, and don't need it any longer. + dispatches.removeLast(); + currentDispatchInfo.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(); + } } - - 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(); + @Override + public void run() { + synchronized (dispatches) { + if (!dispatches.isEmpty()) { + // 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 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); - } - } -} + } +} \ No newline at end of file
diff --git a/src/main/java/org/jdesktop/swinghelper/debug/LICENSE b/src/main/java/org/jdesktop/swinghelper/debug/LICENSE new file mode 100644 index 0000000..de3016f --- /dev/null +++ b/src/main/java/org/jdesktop/swinghelper/debug/LICENSE
@@ -0,0 +1,13 @@ + 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 \ No newline at end of file
diff --git a/src/main/java/org/jdesktop/swinghelper/debug/Tests.java b/src/main/java/org/jdesktop/swinghelper/debug/Tests.java new file mode 100644 index 0000000..e9e314a --- /dev/null +++ b/src/main/java/org/jdesktop/swinghelper/debug/Tests.java
@@ -0,0 +1,190 @@ +package org.jdesktop.swinghelper.debug; + +import java.awt.EventQueue; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.SwingUtilities; + +final class Tests { + + public static void main(String[] args) { + if (args.length == 0) { + return; + } + String mode = args[0]; + + new EventDispatchThreadHangMonitor().initMonitoring(); + + if ("deadlock".equals(mode)) { + runDeadlockTest(); + return; + } + + EventQueue.invokeLater( + () -> { + final JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setLocationRelativeTo(null); + switch (mode) { + case "exception": + runExceptionTest(frame); + break; + case "focus": + runFocusTest(frame); + break; + case "modal-hang": + runModalTest(frame, true); + break; + case "modal-no-hang": + runModalTest(frame, false); + break; + default: + System.err.println("unknown regression test '" + mode + "'"); + System.exit(1); + } + frame.pack(); + frame.setVisible(true); + }); + } + + public static void runDeadlockTest() { + class Locker { + + private Locker locker; + + public void setLocker(Locker locker) { + this.locker = locker; + } + + public synchronized void tryToDeadlock() { + String unused = locker.toString(); + } + + @Override + @SuppressWarnings("CatchAndPrintStackTrace") + 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(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() { + @Override + public void actionPerformed(ActionEvent e) { + // This shouldn't cause us to report a hang. + throw new IllegalStateException("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() { + @Override + 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() { + @Override + 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() { + @Override + public void actionPerformed(ActionEvent e) { + if (shouldSleep) { + sleep(2500); // This is easy. + } + JDialog dialog = new JDialog(frame, "Modal dialog", /* modal= */ 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( + () -> { + for (int i = 0; i <= 100000; ++i) { + final int value = i; + EventQueue.invokeLater(() -> 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); + } + + @SuppressWarnings("CatchAndPrintStackTrace") + 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 (InterruptedException ex) { + ex.printStackTrace(); + } + } + + private Tests() { + } +} \ No newline at end of file