|
The Java Specialists' Newsletter
Issue 075 2003-07-29
Category:
GUI
Java version: An Automatic Wait Cursor: WaitCursorEventQueueby Nathan Arthur
Please note:
There's a known problem with clearQueueOfInputEvents() causing
deadlocks. It's grabbing a lock around a bit of Sun's code that
it shouldn't be, and that causes the deadlocks. I haven't ever
implemented a fix, but most people are happy to just comment
out that method call. (It filters out any mouse clicks &
keyboard presses that happen while the cursor is up - if you
comment it out, those clicks/presses will all happen after
the cursor goes down.) It should be fixable by just putting
a marker event on the queue and clearing everything up to the
marker, instead.
Welcome to the 75th edition of The Java(tm) Specialists' Newsletter. This week we are re-looking
at the problem of wait cursors. I am very grateful to Nathan Arthur
and his company for allowing us to publish this article. They have
been through a lot of effort in producing this code, and I am sure
it will help you as well. There are two things that I would like you
to note in this article. First, it is practical, real-world code.
There might be bugs, if you find any, please let us know. It is
extremely useful, well-written code. Second, Nathan is passionate
about unit testing. The more I work as a programmer, the more I
feel the same passion as Nathan.
There are many ways to skin a cat. [I will not describe them in this
newsletter, since that would be slightly off-topic.] In the
same way, there are many waits t'askin a User to wait. [In the unlikely
event that one of my old English teachers is reading this newsletter -
the previous sentence construction was intentional.]
Enough of me, let us listen to what Nathan has to say (you at the back
of the class, please be quiet and listen as well. Yes, you.)
If you would like to send thanks and comments to Nathan, please email
to truist-waitcursor@truist.com.
Would you like to really understand Java concurrency? Join us for an
in-depth study of how threading works in Java. During the course,
you will learn how to write correct and fast multi-threaded Java code.
Please
click here if you would like to learn more. An Automatic Wait Cursor: WaitCursorEventQueue
A few months ago, I received the "Wait, Cursor, Wait!" edition of
this newsletter.
I had just spent man-weeks trying to discover better approaches
to using wait cursors in my application, and I had two immediate reactions:
First, it amused me that it came out only a few days after I had just
done all that work. Second, it worried me that it might present a
simpler solution than the one I found, thus making all the time I had
spent a waste. Thankfully, it did not. It presented a solution very
similar to the solution my application had started with. You
will therefore benefit from my weeks of hard labour :-)
For some background, you should probably read issue #16 ("Blocking Queue") and issue #7 ("java.awt.EventQueue") of The Java(tm) Specialists' Newsletter.
At the end of this article, we will have implemented our own EventQueue.
Analysis of "Wait, Cursor, Wait!"
Before I get into the background, I would like to discuss a few things I
noted in "Wait, Cursor, Wait!"
First, the use of the GlassPane is not
quite enough in that example - you will notice that you can still use the
keyboard to tab around to components in the parent frame.
Second, Java modal dialogs on Windows are "natively" modal,
which you can see by clicking on the frame behind a modal dialog - the
title bar of the modal dialog will flash. This does not happen if the
dialog is not modal, and there is no way to mimic the behavior in java.
Third, at the end of "Solution 1," there is a paragraph explaining that
the strange wait cursor behavior is caused by the first event pump
being blocked while the modal dialog is open. This is not actually the
cause of the problem - the cause is explained in
the Sun bug #4282540:
the wait cursor
does not paint on the frame because that is the correct Windows behavior
for frames behind modal dialogs. Presumably, that is implemented in
the frame's native peer. I would be interested to see what happens on
other operating systems with different modal dialog / frame cursor behaviors.
Because of all these, I prefer to use actual modal dialogs, not use
the glass pane for the wait cursor, and to live with the wait cursor
not showing on the parent frame. I also would really prefer to have
an automatic wait cursor solution, so I do not have to explicitly deal
with it everywhere, and so the application always shows it even if I
forgot to turn it on.
My Original Solution
The original wait cursor implementation I set out to improve was this:
import java.awt.*;
import java.awt.event.InputEvent;
import java.util.*;
import javax.swing.SwingUtilities;
public class WaitControl {
private static int waiting = 0;
private static ArrayList events = new ArrayList();
private static final Cursor DEFAULT_CURSOR =
Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
private static final Cursor WAIT_CURSOR =
Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
public static Window startWait(Component componentInWindow) {
Window window = getEnclosingWindow(componentInWindow);
if (window == null) {
return null;
}
waiting++;
// Only wait if we are not already
if (waiting == 1) {
window.setCursor(WAIT_CURSOR);
EventQueue q = window.getToolkit().getSystemEventQueue();
try {
while (q.peekEvent() != null) {
events.add(q.getNextEvent());
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
return window;
}
public static void endWait(Component componentInWindow) {
Window window = getEnclosingWindow(componentInWindow);
if (window == null) {
return;
}
if (waiting > 0) {
waiting--;
// Only stop when all waiting is done
if (waiting == 0) {
EventQueue q = window.getToolkit().getSystemEventQueue();
try {
while (q.peekEvent() != null) {
AWTEvent event = q.getNextEvent();
if (!(event instanceof InputEvent)) {
events.add(event);
}
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
for (Iterator it = events.iterator(); it.hasNext();) {
q.postEvent((AWTEvent) it.next());
}
window.setCursor(DEFAULT_CURSOR);
events.clear();
}
}
}
public static void fullEndWait(Window window) {
if (waiting > 0) {
waiting = 1;
endWait(window);
}
}
public static Window getEnclosingWindow(Component componentInWindow) {
if (componentInWindow instanceof Window) {
return (Window) componentInWindow;
} else if (componentInWindow != null) {
return SwingUtilities.windowForComponent(componentInWindow);
} else {
return null;
}
}
}
At a high level, this implementation is simple.
It filters input events by taking them directly off the
system event queue, which means we do not have to use the
GlassPane, and can use modal dialogs.
One of the many problems with this solution was that we always
had to remember to call endWait() with the correct
component and the right number of times. Ideally we would have
preferred for the wait cursor to appear (and disappear) automagically.
The First EventQueue-based Attempt
Also, as it happened, we though we had found a way to make
it work automatically. We found it online, in an article
on JavaWorld.com, titled "Automate the hourglass cursor."
There were some problems with that approach:
The first flaw is subtle does not cause much noticeable bad
behavior. The problem lies in the
assumption made about how wait() and interrupt() work together.
Specifically, line 51 of the code assumes that if interrupt()
is called during the wait(), an InterruptedException will
be thrown. However, the InterruptedException will only be thrown if the delay timeout
has not ended yet. If the timeout has ended, but the thread
is still in contention for the object's monitor, interrupt()
will simply set the interrupt status of the thread to true,
without throwing the exception.
If you install the code as written, and use the application,
you will occasionally see little flashes of the wait cursor that
do not make sense. In addition, if you run the unit test
at the end of the article, nearly all the tests will fail.
The flaw is fixable, however. Simply add these two lines of
code after line 51:
if (Thread.interrupted())
continue;
This performs essentially the same function as the
exception would have, and fixes the problem. Note that we
use Thread.interrupted() and not isInterrupted() because
Thread.interrupted() resets the flag, while isInterrupted()
does not.
Another problem is that modal
dialogs cause the dispatchEvent() method to be recursive.
I will explain why this is true below, but for the moment,
just take my word for it - opening a modal dialog in
the call to super.dispatchEvent() will cause dispatchEvent()
to get called again before super.dispatchEvent() returns.
This causes a problem.
The problem is fairly obvious - two calls to
waitTimer.startTimer() in a row, without an intervening call
to waitTimer.stopTimer(), will cause both wait() calls in the
run() loop to be passed, resulting in a wait cursor being set
on the dialog, and the state tracking to be broken. As a user,
you will not notice this much, because usually the cursor will
get immediately reset, but this does cause problems if the code
that was showing the dialog takes a long time after the dialog
is closed. No cursor will get set, because the stopTimer()
call from the first call to dispatchEvent will then execute,
canceling any cursor that would have otherwise been set.
In addition, the tracking is at this point out of sync, and
can cause further problems (although it usually resets itself
correctly).
This is a common case - imagine starting some long operation,
at the beginning of which you open a dialog to ask a user
to make a choice. In this case, the long operation will not
(reliably) have a wait cursor, because of this bug in the
code. The flaw is not simple to fix, and requires a detailed
understanding of how modal dialogs and the event queue mechanism
work.
The System Event Queue, System Event Pump, and Modal Dialogs
Since this is The Java(tm) Specialists' Newsletter, I can probably assume that you have a
basic understanding of the system event queue.
However, I have found that very few Java developers have a clear
idea of what happens when a modal dialog is opened.
First, all events generated by the user (mouse, keyboard,
system, etc.) are passed through the VM, and all program
events (paints, actions, etc.) are placed on the system event
queue. (Note that there is usually exactly one event queue
per program, but sometimes an event queue will be shared across
applets.) These events
are generated asynchronously, and are simply placed on the
queue from whichever thread they were generated in. This is
happening for the entire life of the AWT program.
Second, and at the same time, there is a special thread
(commonly called the "AWT event thread" and named
"AWT-EventQueue-0") that is constantly removing events
(in FIFO order) from the event queue, and dispatching them.
Note that the currently dispatching event is not on the queue
- it is removed before it is dispatched. All GUI operations
should be performed on this thread, because the AWT and Swing
code is not thread safe. There is always exactly one active
event thread per event queue.
Third, when a modal dialog is shown, a new event pump is
(normally) started, which takes over for the previous event
pump and starts pulling events off the event queue and
dispatching them. This new pump shuts down when the modal
dialog is closed.
The confusion usually happens because people believe that
starting a new pump means starting a new thread, but that is
not true. (In fact, it would not work.) What really happens
is that Dialog.show() simply takes over the job of pumping the
event queue. This is why calling setVisible() on a modal dialog
does not return until the dialog is closed - setVisible()
continues pumping events.
There is only ever one active event thread. When a modal
dialog is opened, this happens as part of an event on the event
thread, and that event simply does not quit, and starts pumping
events so that events continue to happen.
This explains why EventQueue.dispatchEvent() is recursive
with modal dialogs - the dialog itself will start pumping
events (and call dispatchEvent()) before the event that showed
it returns.
If you are interested in the details of all this, I would
suggest reading the code. Start with java.awt.EventQueue,
and pay attention to postEvent(), postEventPrivate(), and the
other version of postEvent(). Pay particular attention to
getNextEvent() and dispatchEvent(). Also, look at the push()
and pop() methods.
Then look at java.awt.EventDispatchThread, specifically
at run(), pumpEvents(), the other pumpEvents(),
pumpEventsForHierarchy(), and pumpOneEventForHierarchy().
I do not really understand why this code filters events for
the modal dialog, because there should not ever be events
outside of the modal dialog, but perhaps it has some use on
another platform. If somebody knows, I would love to hear it!
Finally, look at java.awt.Dialog, specifically at show().
That will bring it all together.
A New WaitCursorEventQueue
We have now seen three implementations of a wait cursor manager
- the one presented in issue #65, the one presented at the top
of this document, and the one on JavaWorld.com. None of them is
perfect, but they all have some good features. We will take the
best from each of them, and add a few improvements of our own.
Specifically, we want to build a cursor manager that:
Automatically displays and resets the cursor, remembering the
original cursor
Works with modal dialogs (recursive calls to dispatchEvent(),
and correct filtering of events)
Correctly displays the cursor for all long events, even after
a modal dialog is closed
Is thread safe where it needs to be
does not impose a significant performance overhead
Is thoroughly unit tested
The final implementation of these "requirements" relies on
four classes and an interface, and two unit tests. The primary
class is WaitCursorEventQueue, and it works with a CursorManager
class, which uses a DispatchedEvent class. The remaining class
is DelayTimer, which, with the DelayTimerCallback interface,
implements a generic delay timer that can be used for other
purposes, if needed.
DelayTimer
I first present the DelayTimer. It is thread safe,
and supports any series of calls to startTimer() and
stopTimer().
[HK: I personally prefer to use the interrupted flag to
indicate that I want to quit a thread. However, I did
not want to break anything, so here is the code as it
was :-]
/**
* This class implements a delay timer that will call trigger()
* on the DelayTimerCallback delay milliseconds after
* startTimer() was called, if stopTimer() was not called first.
* The timer will only throw events after startTimer() is called.
* Until then, it does nothing. It is safe to call stopTimer()
* and startTimer() repeatedly.
*
* Note that calls to trigger() will happen on the timer thread.
*
* This class is multiple-thread safe.
*/
public class DelayTimer extends Thread {
private final DelayTimerCallback callback;
private final Object mutex = new Object();
private final Object triggeredMutex = new Object();
private final long delay;
private boolean quit;
private boolean triggered;
private long waitTime;
public DelayTimer(DelayTimerCallback callback, long delay) {
this.callback = callback;
this.delay = delay;
setDaemon(true);
start();
}
/**
* Calling this method twice will reset the timer.
*/
public void startTimer() {
synchronized (mutex) {
waitTime = delay;
mutex.notify();
}
}
public void stopTimer() {
try {
synchronized (mutex) {
synchronized (triggeredMutex) {
if (triggered) {
triggeredMutex.wait();
}
}
waitTime = 0;
mutex.notify();
}
} catch (InterruptedException ie) {
System.err.println("trigger failure");
ie.printStackTrace(System.err);
}
}
public void run() {
try {
while (!quit) {
synchronized (mutex) {
//we rely on wait(0) being implemented to wait forever here
if (waitTime < 0) {
triggered = true;
waitTime = 0;
} else {
long saveWaitTime = waitTime;
waitTime = -1;
mutex.wait(saveWaitTime);
}
}
try {
if (triggered) {
callback.trigger();
}
} catch (Exception e) {
System.err.println(
"trigger() threw exception, continuing");
e.printStackTrace(System.err);
} finally {
synchronized (triggeredMutex) {
triggered = false;
triggeredMutex.notify();
}
}
}
} catch (InterruptedException ie) {
System.err.println("interrupted in run");
ie.printStackTrace(System.err);
}
}
public void quit() {
synchronized (mutex) {
this.quit = true;
mutex.notify();
}
}
}
The DelayTimer relies on DelayTimerCallback:
public interface DelayTimerCallback {
public void trigger();
}
We rely on the correct functioning of this class for the
WaitCursorEventQueue, so it is very important that it have a
unit test:
import junit.framework.TestCase;
public class DelayTimerTest extends TestCase {
private static final int TIMEOUT = 100;
private static final int BUFFER = 20;
private static final int MORE_THAN_HALF = 60;
private DelayTimer timer;
private TestDelayTimerCallback callback;
public DelayTimerTest(String name) {
super(name);
}
public void setUp() {
callback = new TestDelayTimerCallback();
timer = new DelayTimer(callback, TIMEOUT);
}
public void tearDown() {
timer.quit();
}
private void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void testNotStarted() {
sleep(TIMEOUT + BUFFER);
assertEquals("no trigger without start()", 0,
callback.getTriggerCount());
}
public void testNoTriggerIfTooShort() {
timer.startTimer();
assertEquals("no trigger if too fast", 0,
callback.getTriggerCount());
timer.stopTimer();
sleep(TIMEOUT + BUFFER);
assertEquals("no trigger after stop", 0,
callback.getTriggerCount());
}
public void testTimerRestarts() {
timer.startTimer();
sleep(MORE_THAN_HALF);
timer.startTimer();
sleep(MORE_THAN_HALF);
timer.stopTimer();
assertEquals("timer is restarted on calls to start()", 0,
callback.getTriggerCount());
}
public void testTimerTriggersThenStops() {
timer.startTimer();
sleep(TIMEOUT + BUFFER);
timer.stopTimer();
sleep(TIMEOUT + BUFFER);
assertEquals("timer triggered event", 1,
callback.getTriggerCount());
}
public void testTimerOnlyTriggersOneEvent() {
timer.startTimer();
sleep(TIMEOUT + BUFFER);
assertEquals("timer triggered event", 1,
callback.getTriggerCount());
sleep(TIMEOUT + BUFFER);
assertEquals("timer did not trigger another event", 1,
callback.getTriggerCount());
timer.stopTimer();
}
public void testTimerStopsTwice() {
timer.startTimer();
timer.stopTimer();
timer.stopTimer();
sleep(TIMEOUT + BUFFER);
assertEquals("timer did not trigger event", 0,
callback.getTriggerCount());
}
public void testTriggers() {
timer.startTimer();
sleep(TIMEOUT + BUFFER);
timer.stopTimer();
timer.stopTimer();
assertEquals("timer triggered only 1 event", 1,
callback.getTriggerCount());
}
public void testTriggerTrigger() {
timer.startTimer();
sleep(TIMEOUT + BUFFER);
assertEquals("timer triggered first event", 1,
callback.getTriggerCount());
timer.startTimer();
sleep(TIMEOUT + BUFFER);
assertEquals("timer triggered second event", 2,
callback.getTriggerCount());
sleep(TIMEOUT + BUFFER);
timer.stopTimer();
assertEquals("timer did not trigger another event", 2,
callback.getTriggerCount());
}
public void testStopTimerHappensAfterTrigger() {
FancyTestDelayTimerCallback callback = new FancyTestDelayTimerCallback();
timer = new DelayTimer(callback, TIMEOUT);
timer.startTimer();
sleep(TIMEOUT + BUFFER);
assertTrue("timer is in trigger", callback.inTrigger);
assertTrue("timer has not thrown exception",
!callback.exception);
assertTrue("timer is not out of trigger",
!callback.outOfTrigger);
Runnable runnable = new Runnable() {
public void run() {
timer.stopTimer();
}
};
Thread testThread = new Thread(runnable, "test thread");
testThread.start();
sleep(TIMEOUT + BUFFER);
assertTrue("stopTimer() has not returned",
testThread.isAlive());
synchronized (callback) {
callback.notify();
}
sleep(TIMEOUT + BUFFER);
assertTrue("timer has not thrown exception",
!callback.exception);
assertTrue("timer is out of trigger", callback.outOfTrigger);
assertTrue("stopTimer() has returned", !testThread.isAlive());
}
public void testMishMash() {
timer.startTimer();
sleep(MORE_THAN_HALF);
timer.startTimer();
sleep(MORE_THAN_HALF);
timer.stopTimer();
sleep(MORE_THAN_HALF);
timer.startTimer();
timer.stopTimer();
timer.stopTimer();
sleep(MORE_THAN_HALF);
timer.startTimer();
timer.startTimer();
assertEquals("no event yet", 0, callback.getTriggerCount());
sleep(TIMEOUT + BUFFER);
timer.stopTimer();
assertEquals("got event yet", 1, callback.getTriggerCount());
}
private class FancyTestDelayTimerCallback
implements DelayTimerCallback {
public boolean exception;
public boolean inTrigger;
public boolean outOfTrigger;
public synchronized void trigger() {
inTrigger = true;
try {
wait();
} catch (InterruptedException e) {
exception = true;
e.printStackTrace();
} finally {
outOfTrigger = true;
}
}
}
private class TestDelayTimerCallback
implements DelayTimerCallback {
private int triggerCount;
public int getTriggerCount() {
return triggerCount;
}
public void trigger() {
triggerCount++;
}
}
}
WaitCursorEventQueue
Now that we have seen the DelayTimer, I am going to skip ahead
now to WaitCursorEventQueue itself, which is a fairly simple class:
import java.awt.*;
/**
* Suggested serving size:
* Toolkit.getDefaultToolkit().getSystemEventQueue().push(new WaitCursorEventQueue(70));
*/
public class WaitCursorEventQueue extends EventQueue
implements DelayTimerCallback {
private final CursorManager cursorManager;
private final DelayTimer waitTimer;
public WaitCursorEventQueue(int delay) {
this.waitTimer = new DelayTimer(this, delay);
this.cursorManager = new CursorManager(waitTimer);
}
public void close() {
waitTimer.quit();
pop();
}
protected void dispatchEvent(AWTEvent event) {
cursorManager.push(event.getSource());
waitTimer.startTimer();
try {
super.dispatchEvent(event);
} finally {
waitTimer.stopTimer();
cursorManager.pop();
}
}
public AWTEvent getNextEvent() throws InterruptedException {
waitTimer.stopTimer(); //started by pop(), this catches modal dialogs
//closing that do work afterwards
return super.getNextEvent();
}
public void trigger() {
cursorManager.setCursor();
}
}
The most significant method in this class is dispatchEvent().
CursorManager manages a stack of events (and their sources)
so it can handle modal dialogs and set the cursor on the right
window.
If a modal dialog opened during the call to
super.dispatchEvent(), we will get another call to
dispatchEvent(). This will tell the CursorManager that a new
event is started and call startTimer() again. We rely on the
fact that a second call to startTimer() will restart the timer,
resetting the delay. (This way, a modal dialog does not
immediately get a wait cursor set.) Once this new event is over,
we will stop
the timer, which stops it completely, even though startTimer()
was called twice. The CursorManager will still have the event
that opened the modal dialog on its stack, which is correct,
because that event has not finished yet. Once the modal dialog
is closed, that event will finish, and take the event off the
CursorManager's stack.
CursorManager
The CursorManager class might be more appropriately named
"WindowManager" or "EventManager", but none of those names
are quite right either, and CursorManager is what it started
as, so I will leave it that way. This class' primary job
is to assist the WaitCursorEventQueue in managing the events,
by handling modal dialogs (recursiveness), filtering events,
and helping to manage the WaitTimer. Here is the code:
import java.awt.*;
import java.awt.event.InputEvent;
import java.util.*;
class CursorManager {
private final DelayTimer waitTimer;
private final Stack dispatchedEvents;
private boolean needsCleanup;
public CursorManager(DelayTimer waitTimer) {
this.dispatchedEvents = new Stack();
this.waitTimer = waitTimer;
}
private void cleanUp() {
if (((DispatchedEvent) dispatchedEvents.peek()).resetCursor()) {
clearQueueOfInputEvents();
}
}
private void clearQueueOfInputEvents() {
EventQueue q = Toolkit.getDefaultToolkit().getSystemEventQueue();
synchronized (q) {
ArrayList nonInputEvents = gatherNonInputEvents(q);
for (Iterator it = nonInputEvents.iterator(); it.hasNext();)
q.postEvent((AWTEvent)it.next());
}
}
private ArrayList gatherNonInputEvents(EventQueue systemQueue) {
ArrayList events = new ArrayList();
while (systemQueue.peekEvent() != null) {
try {
AWTEvent nextEvent = systemQueue.getNextEvent();
if (!(nextEvent instanceof InputEvent)) {
events.add(nextEvent);
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
return events;
}
public void push(Object source) {
if (needsCleanup) {
waitTimer.stopTimer();
cleanUp(); //this corrects the state when a modal dialog
//opened last time round
}
dispatchedEvents.push(new DispatchedEvent(source));
needsCleanup = true;
}
public void pop() {
cleanUp();
dispatchedEvents.pop();
if (!dispatchedEvents.isEmpty()) {
//this will be stopped if getNextEvent() is called -
//used to watch for modal dialogs closing
waitTimer.startTimer();
} else {
needsCleanup = false;
}
}
public void setCursor() {
((DispatchedEvent) dispatchedEvents.peek()).setCursor();
}
}
Most of the code is self-explanatory.
Only pop() is a bit complicated. Once it has unset the cursor
and popped the event, it checks to see if there are any
outstanding events on the stack. If so, it knows that there
must be a modal dialog open and needs to take special precautions.
Yes, it is a bit heavy-handed to start the timer on every single
event when a modal dialog is up - but it works, and it does not
actually impose much overhead.
DispatchedEvent
The last interesting class is DispatchedEvent. If you recall,
we already know that it represents an event on the EventQueue,
and that it can set and unset the cursor on the appropriate
window.
import java.awt.*;
import javax.swing.SwingUtilities;
class DispatchedEvent {
private final Object mutex = new Object();
private final Object source;
private Component parent;
private Cursor lastCursor;
public DispatchedEvent(Object source) {
this.source = source;
}
public void setCursor() {
synchronized (mutex) {
parent = findVisibleParent();
if (parent != null) {
lastCursor = (parent.isCursorSet() ? parent.getCursor() : null);
parent.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
}
}
}
public boolean resetCursor() {
synchronized (mutex) {
if (parent != null) {
parent.setCursor(lastCursor);
parent = null;
return true;
}
return false;
}
}
private Component findVisibleParent() {
Component result = null;
if (source instanceof Component) {
result = SwingUtilities.getRoot((Component) source);
} else if (source instanceof MenuComponent) {
MenuContainer mParent = ((MenuComponent) source).getParent();
if (mParent instanceof Component) {
result = SwingUtilities.getRoot((Component) mParent);
}
}
if ((result != null) && result.isVisible()) {
return result;
} else {
return null;
}
}
}
WaitCursorEventQueueTest
We have finally gotten through all the code, but how do we know
it works? That is where unit testing comes in. Some people
argue that you cannot unit test multi-threaded code, but I
disagree - you can unit test it, so long as you are willing to
live with an occasional false negative. What you do is write
the test so that if there is a bug in the algorithm, the unit
test will show it. This can be done by writing tests that will
pass only if all the threads line up the way you want them to,
and all the events happen in the right order and right way.
If you have written a unit test that way then a run of the unit
test might go three possible ways. First, it might hang,
in which case you know you know there is a bug. Sometimes
tracking it down can be extremely difficult, but it can be done.
Second,
the test might fail. In that case, it is usually a good idea
to rerun the test and see if it fails repeatedly. If there
really is something broken in the code, then the test should
fail consistently. If it does not always fail, then you have
to analyze why it is sometimes failing. If there is no bug,
but just a race condition in the test, ignore it. If you find
a bug, you can write a test that will reliably demonstrate it,
and then fix the problem. Finally, all the tests might pass.
If that is the case, then you either have working code or not
enough tests. If you are not sure if you have enough tests,
try going through all the code commenting out one line at a
time, and running the tests. Every single commented out line
should cause a repeatable failure. [HK: I particularly like
the last two sentences, excluding my own. Ok, let us include mine.
Make it the last five sentences. :-]
This unit test was designed and built with all of this in
mind. Commenting out lines of code causes a test to fail.
The tests do occasionally fail unreliably, but every time that
has happened, I have been able to understand why. They have
never locked up. Finally, two of the tests exist because of
bugs found in the initial implementation of this code that
the unit tests were occasionally catching.
Note also that this test relies on two timing parameters - how
long the delay should be for a trigger, and how long an event
should take to ensure that the trigger would have happened.
Both of these are configurable constants at the top of the
test, and You will probably need to adjust the numbers to your
machine/OS/VM.
import java.awt.*;
import java.awt.event.*;
import java.lang.reflect.InvocationTargetException;
import javax.swing.*;
import junit.framework.TestCase;
public class WaitCursorEventQueueTest extends TestCase {
private static final int TIMEOUT = 200;
private static final int BUFFER = 30;
private static final Cursor BASE_CURSOR = Cursor.getPredefinedCursor(
Cursor.CROSSHAIR_CURSOR);
private CursorReportingDialog dialog;
private CursorReportingDialog dialog2;
private CursorReportingFrame frame;
private TestWaitCursorEventQueue eventQueue;
public WaitCursorEventQueueTest(String name) {
super(name);
}
public void setUp() {
eventQueue = new TestWaitCursorEventQueue(TIMEOUT);
Toolkit.getDefaultToolkit().getSystemEventQueue()
.push(eventQueue);
frame = new CursorReportingFrame();
frame.pack();
frame.setBounds(-1000, -1000, 100, 100);
frame.setVisible(true);
dialog = new CursorReportingDialog(frame);
dialog.pack();
dialog.setBounds(-1000, -1000, 100, 100);
dialog2 = new CursorReportingDialog(dialog);
dialog2.pack();
dialog2.setBounds(-1000, -1000, 100, 100);
}
public void tearDown() throws InvocationTargetException,
InterruptedException {
flushQueue();
eventQueue.close();
eventQueue = null;
flushQueue();
frame.dispose();
frame = null;
dialog.dispose();
dialog = null;
}
private void flushQueue() throws InvocationTargetException,
InterruptedException {
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
}
});
}
private void postEvent(Object source, Runnable event) {
eventQueue.postEvent(new InvocationEvent(source, event));
}
private void hangOut(long timeout) {
try {
Thread.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void testNoCursor() throws InvocationTargetException,
InterruptedException {
DelayEvent event = new DelayEvent(TIMEOUT - BUFFER);
postEvent(frame, event);
postEvent(frame, event);
postEvent(frame, event);
postEvent(frame, event);
flushQueue();
assertEquals("no cursor set", 0, frame.getCursorSetCount());
assertEquals("no cursor reset", 0,
frame.getCursorResetCount());
}
public void testCursor() throws InvocationTargetException,
InterruptedException {
DelayEvent event = new DelayEvent(TIMEOUT + BUFFER);
postEvent(frame, event);
flushQueue();
assertEquals("1 cursor set", 1, frame.getCursorSetCount());
assertEquals("1 cursor reset", 1,
frame.getCursorResetCount());
}
public void testDialog() throws InvocationTargetException,
InterruptedException {
postEvent(frame, new DialogShowEvent(dialog, true, 0));
flushQueue();
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog never got cursor", 0,
dialog.getCursorSetCount());
assertEquals("dialog never reset cursor", 0,
dialog.getCursorResetCount());
assertEquals("frame never got cursor", 0,
frame.getCursorSetCount());
assertEquals("frame never reset cursor", 0,
frame.getCursorResetCount());
postEvent(dialog, new DialogShowEvent(dialog, false, 0));
flushQueue();
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog never got cursor", 0,
dialog.getCursorSetCount());
assertEquals("dialog never reset cursor", 0,
dialog.getCursorResetCount());
assertEquals("frame never got cursor", 0,
frame.getCursorSetCount());
assertEquals("frame never reset cursor", 0,
frame.getCursorResetCount());
}
public void testCursorAndDialog()
throws InvocationTargetException,
InterruptedException {
TestRunnable testAndShow = new TestRunnable() {
public void run() {
testsPassed &= (1 == frame.getCursorSetCount());
testsPassed &= (0 == frame.getCursorResetCount());
dialog.setVisible(true);
}
};
postEvent(frame,
new DelayEvent(TIMEOUT + BUFFER, testAndShow));
flushQueue();
assertTrue("Delay worked", testAndShow.getTestsPassed());
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog never got cursor", 0,
dialog.getCursorSetCount());
assertEquals("dialog never reset cursor", 0,
dialog.getCursorResetCount());
assertEquals("frame never got another cursor", 1,
frame.getCursorSetCount());
assertEquals("frame reset cursor", 1,
frame.getCursorResetCount());
postEvent(dialog, new DialogShowEvent(dialog, false, 0));
flushQueue();
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog never got cursor", 0,
dialog.getCursorSetCount());
assertEquals("dialog never reset cursor", 0,
dialog.getCursorResetCount());
assertEquals("frame never got another cursor", 1,
frame.getCursorSetCount());
assertEquals("frame never reset another cursor", 1,
frame.getCursorResetCount());
}
public void testCursorAndDialogAndCursor()
throws InvocationTargetException, InterruptedException {
TestRunnable testAndShow = new TestRunnable() {
public void run() {
testsPassed &= (1 == frame.getCursorSetCount());
testsPassed &= (0 == frame.getCursorResetCount());
dialog.setVisible(true);
hangOut(TIMEOUT + BUFFER);
}
};
postEvent(frame,
new DelayEvent(TIMEOUT + BUFFER, testAndShow));
flushQueue();
assertTrue("Delay worked", testAndShow.getTestsPassed());
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog never got cursor", 0,
dialog.getCursorSetCount());
assertEquals("dialog never reset cursor", 0,
dialog.getCursorResetCount());
assertEquals("frame never got another cursor", 1,
frame.getCursorSetCount());
assertEquals("frame reset cursor", 1,
frame.getCursorResetCount());
postEvent(dialog, new DialogShowEvent(dialog, false, 0));
flushQueue();
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog never got cursor", 0,
dialog.getCursorSetCount());
assertEquals("dialog never reset cursor", 0,
dialog.getCursorResetCount());
assertEquals("frame got another cursor", 2,
frame.getCursorSetCount());
assertEquals("frame reset another cursor", 2,
frame.getCursorResetCount());
}
/**
* This test checks the condition where the EventDispatchThread
* does not call EventQueue.getNextEvent() within TIMEOUT,
* (presumably) because of thread contention, even when there
* is not a dialog going down. Note that this case only actually
* matters if there is a dialog currently up.
*/
public void testDelayedGetNextEvent()
throws InvocationTargetException,
InterruptedException {
postEvent(frame, new DialogShowEvent(dialog, true, 0));
flushQueue();
hangOut(TIMEOUT + BUFFER);
eventQueue.setGetDelay(TIMEOUT + BUFFER);
postEvent(frame, new DelayEvent(TIMEOUT - BUFFER));
flushQueue();
hangOut(TIMEOUT + BUFFER);
assertEquals("frame got a cursor", 1,
frame.getCursorSetCount());
assertEquals("frame reset a cursor", 1,
frame.getCursorResetCount());
postEvent(dialog, new DialogShowEvent(dialog, false, 0));
flushQueue();
hangOut(TIMEOUT + BUFFER);
assertEquals("frame did not get another cursor", 1,
frame.getCursorSetCount());
assertEquals("frame did not reset another cursor", 1,
frame.getCursorResetCount());
}
public void testTwoDialogs() throws InvocationTargetException,
InterruptedException {
TestRunnable testAndShow = new TestRunnable() {
public void run() {
testsPassed &= (1 == frame.getCursorSetCount());
testsPassed &= (0 == frame.getCursorResetCount());
dialog.setVisible(true);
}
};
postEvent(frame,
new DelayEvent(TIMEOUT + BUFFER, testAndShow));
flushQueue();
assertTrue("Delay worked", testAndShow.getTestsPassed());
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog never got cursor", 0,
dialog.getCursorSetCount());
assertEquals("dialog never reset cursor", 0,
dialog.getCursorResetCount());
assertEquals("frame never got another cursor", 1,
frame.getCursorSetCount());
assertEquals("frame reset cursor", 1,
frame.getCursorResetCount());
TestRunnable testAndShow2 = new TestRunnable() {
public void run() {
testsPassed &= (1 == dialog.getCursorSetCount());
testsPassed &= (0 == dialog.getCursorResetCount());
dialog2.setVisible(true);
}
};
postEvent(dialog,
new DelayEvent(TIMEOUT + BUFFER, testAndShow2));
flushQueue();
assertTrue("Delay worked", testAndShow.getTestsPassed());
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog2 never got cursor", 0,
dialog2.getCursorSetCount());
assertEquals("dialog2 never reset cursor", 0,
dialog2.getCursorResetCount());
assertEquals("dialog never got another cursor", 1,
dialog.getCursorSetCount());
assertEquals("dialog reset cursor", 1,
dialog.getCursorResetCount());
assertEquals("frame never got another cursor", 1,
frame.getCursorSetCount());
assertEquals("frame reset cursor", 1,
frame.getCursorResetCount());
postEvent(dialog2, new DelayEvent(TIMEOUT + BUFFER));
flushQueue();
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog2 got cursor", 1,
dialog2.getCursorSetCount());
assertEquals("dialog2 reset cursor", 1,
dialog2.getCursorResetCount());
assertEquals("dialog never got another cursor", 1,
dialog.getCursorSetCount());
assertEquals("dialog reset cursor", 1,
dialog.getCursorResetCount());
assertEquals("frame never got another cursor", 1,
frame.getCursorSetCount());
assertEquals("frame reset cursor", 1,
frame.getCursorResetCount());
postEvent(dialog2, new DialogShowEvent(dialog2, false, 0));
flushQueue();
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog2 never got another cursor", 1,
dialog2.getCursorSetCount());
assertEquals("dialog2 never reset another cursor", 1,
dialog2.getCursorResetCount());
assertEquals("dialog never got another cursor", 1,
dialog.getCursorSetCount());
assertEquals("dialog never reset another cursor", 1,
dialog.getCursorResetCount());
assertEquals("frame never got another cursor", 1,
frame.getCursorSetCount());
assertEquals("frame never reset another cursor", 1,
frame.getCursorResetCount());
postEvent(dialog, new DialogShowEvent(dialog, false, 0));
flushQueue();
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog2 never got another cursor", 1,
dialog2.getCursorSetCount());
assertEquals("dialog2 never reset another cursor", 1,
dialog2.getCursorResetCount());
assertEquals("dialog never got another cursor", 1,
dialog.getCursorSetCount());
assertEquals("dialog never reset another cursor", 1,
dialog.getCursorResetCount());
assertEquals("frame never got another cursor", 1,
frame.getCursorSetCount());
assertEquals("frame never reset another cursor", 1,
frame.getCursorResetCount());
}
public void testCursorAndTwoDialogsAndCursor()
throws InvocationTargetException, InterruptedException {
postEvent(frame, new DialogShowEvent(dialog, true, 0));
flushQueue();
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog never got cursor", 0,
dialog.getCursorSetCount());
assertEquals("dialog never reset cursor", 0,
dialog.getCursorResetCount());
assertEquals("frame never got cursor", 0,
frame.getCursorSetCount());
assertEquals("frame never reset cursor", 0,
frame.getCursorResetCount());
TestRunnable testAndShow = new TestRunnable() {
public void run() {
testsPassed &= (1 == dialog.getCursorSetCount());
testsPassed &= (0 == dialog.getCursorResetCount());
dialog2.setVisible(true);
hangOut(TIMEOUT + BUFFER);
}
};
postEvent(dialog,
new DelayEvent(TIMEOUT + BUFFER, testAndShow));
flushQueue();
assertTrue("Delay worked", testAndShow.getTestsPassed());
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog2 never got cursor", 0,
dialog2.getCursorSetCount());
assertEquals("dialog2 never reset cursor", 0,
dialog2.getCursorResetCount());
assertEquals("dialog never got another cursor", 1,
dialog.getCursorSetCount());
assertEquals("dialog reset cursor", 1,
dialog.getCursorResetCount());
assertEquals("frame never got cursor", 0,
frame.getCursorSetCount());
assertEquals("frame never reset cursor", 0,
frame.getCursorResetCount());
postEvent(dialog2, new DialogShowEvent(dialog2, false, 0));
flushQueue();
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog2 never got cursor", 0,
dialog2.getCursorSetCount());
assertEquals("dialog2 never reset cursor", 0,
dialog2.getCursorResetCount());
assertEquals("dialog got another cursor", 2,
dialog.getCursorSetCount());
assertEquals("dialog reset another cursor", 2,
dialog.getCursorResetCount());
assertEquals("frame never got cursor", 0,
frame.getCursorSetCount());
assertEquals("frame never reset cursor", 0,
frame.getCursorResetCount());
postEvent(dialog, new DialogShowEvent(dialog, false, 0));
flushQueue();
hangOut(TIMEOUT + BUFFER);
assertEquals("dialog2 never got cursor", 0,
dialog2.getCursorSetCount());
assertEquals("dialog2 never reset cursor", 0,
dialog2.getCursorResetCount());
assertEquals("dialog never got another cursor", 2,
dialog.getCursorSetCount());
assertEquals("dialog never reset another cursor", 2,
dialog.getCursorResetCount());
assertEquals("frame never got cursor", 0,
frame.getCursorSetCount());
assertEquals("frame never reset cursor", 0,
frame.getCursorResetCount());
}
private class CursorReportingDialog extends Dialog {
private int cursorResetCount;
private int cursorSetCount;
public CursorReportingDialog(Frame owner) {
super(owner, true);
init();
}
public CursorReportingDialog(Dialog owner) {
super(owner, "", true);
init();
}
private void init() {
setCursor(BASE_CURSOR);
this.cursorSetCount = 0;
this.cursorResetCount = 0;
}
public int getCursorSetCount() {
return cursorSetCount;
}
public int getCursorResetCount() {
return cursorResetCount;
}
public void setCursor(Cursor cursor) {
super.setCursor(cursor);
if (BASE_CURSOR.equals(cursor)) {
cursorResetCount++;
} else {
cursorSetCount++;
}
}
}
private class CursorReportingFrame extends Frame {
private int cursorResetCount;
private int cursorSetCount;
public CursorReportingFrame() {
super();
setCursor(BASE_CURSOR);
cursorSetCount = 0;
cursorResetCount = 0;
}
public int getCursorSetCount() {
return cursorSetCount;
}
public int getCursorResetCount() {
return cursorResetCount;
}
public void setCursor(Cursor cursor) {
super.setCursor(cursor);
if (BASE_CURSOR.equals(cursor)) {
cursorResetCount++;
} else {
cursorSetCount++;
}
}
}
private abstract class TestRunnable implements Runnable {
protected boolean testsPassed = true;
public boolean getTestsPassed() {
return testsPassed;
}
}
private class DelayEvent implements Runnable {
private Runnable callback;
private int delay;
public DelayEvent(int delay) {
this(delay, null);
}
public DelayEvent(int delay, Runnable callback) {
this.delay = delay;
this.callback = callback;
}
public void run() {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (callback != null) {
callback.run();
}
}
}
private class DialogShowEvent implements Runnable {
private Dialog dialog;
private boolean visible;
private int delay;
public DialogShowEvent(Dialog dialog, boolean visible,
int delay) {
this.dialog = dialog;
this.visible = visible;
this.delay = delay;
}
public void run() {
dialog.setVisible(visible);
if (delay > 0) {
hangOut(delay);
}
}
}
private class TestWaitCursorEventQueue
extends WaitCursorEventQueue {
private int getDelay;
public TestWaitCursorEventQueue(int delay) {
super(delay);
}
public AWTEvent getNextEvent() throws InterruptedException {
if (getDelay > 0) {
hangOut(getDelay);
getDelay = 0;
}
return super.getNextEvent();
}
public void setGetDelay(int getDelay) {
this.getDelay = getDelay;
}
}
}
Two types of events are posted to the queue - DelayEvents and
DialogShowEvents. DelayEvents are simply events that take
some period of time to complete. They can also be configured
to call a Runnable callback, to trigger other evaluations or
other events. DialogShowEvents either show or hide a dialog,
and can also take some period of time after the dialog is
shown/hidden to complete. Instances of these two events are
passed to postEvent(), along with an event source, and are
wrapped in an InvocationEvent and posted to the event queue.
Different combinations of these events make up most of the
differences between the individual tests.
There are also two GUI classes - CursorReportingDialog and
CursorReportingFrame. They will both track cursor sets and
resets, and report them to tests that ask. They are usually
found in assert() statements.
Finally, you should check out setUp(), tearDown(), flushQueue(),
and hangOut(). Past those, everything else is just a test
method.
Performance
When this code was first written, I did nothing to examine its
performance, so I did not really know how efficient it was.
However, that is telling - I have never noticed (as a user)
any slowdown because of it. Nevertheless, I have received a
question about it (thanks Herman!), which made me realize that
I had not examined it. Therefore, I took some time and wrote
a unit test that shows the performance numbers for this code.
I wrote it as a unit test, just to make it easy to run and in
case I ever wanted to really codify what "good performance"
meant for this code.
import java.awt.*;
import java.awt.event.*;
import java.util.Random;
import javax.swing.*;
import junit.framework.TestCase;
public class WaitCursorEventQueuePerformanceTest
extends TestCase {
private static final long FAST = 5;
private static final long SLOW = 50;
private static final long MIXED = -1;
private static final long TIMEOUT = 15;
private Dialog dialog;
private Frame frame;
public WaitCursorEventQueuePerformanceTest(String name) {
super(name);
}
protected void setUp() throws Exception {
frame = new Frame();
frame.pack();
frame.setBounds(-1000, -1000, 100, 100);
frame.setVisible(true);
dialog = new Dialog(frame, true);
dialog.pack();
dialog.setBounds(-1000, -1000, 100, 100);
}
protected void tearDown() throws Exception {
frame.dispose();
dialog.dispose();
}
private long postEvents(long time) throws InterruptedException {
InvocationEvent repeatEvent = new InvocationEvent(
frame, new TimedEvent(time));
InvocationEvent finalEvent = new InvocationEvent(
frame, new TimedEvent(time), this, false);
EventQueue q = Toolkit.getDefaultToolkit().getSystemEventQueue();
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000; i++)
q.postEvent(repeatEvent);
synchronized (this) {
q.postEvent(finalEvent);
wait(); //we will be notified by finalEvent when it gets posted
}
long endTime = System.currentTimeMillis();
return (endTime - startTime);
}
public void testNormalPerformanceWithFastEvents()
throws InterruptedException {
System.out.println("\nnormal with fast: " + postEvents(FAST));
}
public void testNormalPerformanceWithSlowEvents()
throws InterruptedException {
System.out.println("\nnormal with slow: " + postEvents(SLOW));
}
public void testNormalPerformanceWithMixedEvents()
throws InterruptedException {
System.out.println("\nnormal with random: " + postEvents(MIXED));
}
public void testWaitQueuePerformanceWithFastEvents()
throws InterruptedException {
WaitCursorEventQueue waitQueue = new WaitCursorEventQueue(
(int) TIMEOUT);
Toolkit.getDefaultToolkit().getSystemEventQueue()
.push(waitQueue);
System.out.println("\nwait with fast: " + postEvents(FAST));
waitQueue.close();
}
public void testWaitQueuePerformanceWithSlowEvents()
throws InterruptedException {
WaitCursorEventQueue waitQueue = new WaitCursorEventQueue(
(int) TIMEOUT);
Toolkit.getDefaultToolkit().getSystemEventQueue()
.push(waitQueue);
System.out.println("\nwait with slow: " + postEvents(SLOW));
waitQueue.close();
}
public void testWaitQueuePerformanceWithMixedEvents()
throws InterruptedException {
WaitCursorEventQueue waitQueue = new WaitCursorEventQueue(
(int) TIMEOUT);
Toolkit.getDefaultToolkit().getSystemEventQueue()
.push(waitQueue);
System.out.println("\nwait with random: " + postEvents(MIXED));
waitQueue.close();
}
public void testWaitQueuePerformanceWithDialogWithFastEvents()
throws InterruptedException {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
dialog.setVisible(true);
}
});
WaitCursorEventQueue waitQueue = new WaitCursorEventQueue(
(int) TIMEOUT);
Toolkit.getDefaultToolkit().getSystemEventQueue()
.push(waitQueue);
System.out.println("\nwait with dialog with fast: " +
postEvents(FAST));
waitQueue.close();
}
public void testWaitQueuePerformanceWithDialogWithSlowEvents()
throws InterruptedException {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
dialog.setVisible(true);
}
});
WaitCursorEventQueue waitQueue = new WaitCursorEventQueue(
(int) TIMEOUT);
Toolkit.getDefaultToolkit().getSystemEventQueue()
.push(waitQueue);
System.out.println("\nwait with dialog with slow: " +
postEvents(SLOW));
waitQueue.close();
}
public void testWaitQueuePerformanceWithDialogWithMixedEvents()
throws InterruptedException {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
dialog.setVisible(true);
}
});
WaitCursorEventQueue waitQueue = new WaitCursorEventQueue(
(int) TIMEOUT);
Toolkit.getDefaultToolkit().getSystemEventQueue()
.push(waitQueue);
System.out.println("\nwait with dialog with random: " +
postEvents(MIXED));
waitQueue.close();
}
private class TimedEvent implements Runnable {
private Random random;
private long time;
public TimedEvent(long time) {
this.time = time;
if (time == MIXED) {
random = new Random(1000);
}
}
public void run() {
try {
if (time == MIXED) {
Thread.sleep(
(long) (random.nextDouble() * (SLOW - FAST) +
FAST));
} else {
Thread.sleep(time);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
I will leave the analysis of this code up to you, and I will
admit that it is not a perfect performance test, but it is
good enough for these purposes. You will notice that there
are four basic tunable parameters - how long a fast event
takes, how long a slow event takes, how long the timer on
the WaitCursorEventQueue is, and how many events are used
for each test. With the numbers shown here, on my machine,
after a reboot, with no other activity, I get:
.
normal with fast: 5118
.
normal with slow: 50772
.
normal with random: 27629
.
wait with fast: 5168
.
wait with slow: 50132
.
wait with random: 27660
.
wait with dialog with fast: 5127
.
wait with dialog with slow: 50173
.
wait with dialog with random: 27720
Time: 251.963
OK (9 tests)
There are three sets of tests - the normal EventQueue, the
WaitCursorEventQueue, and the WaitCursorEventQueue with a modal
dialog up (because of the extra processing done while modal
dialogs are up). Each set checks the time for 1000 fast events,
1000 slow events, and a series of 1000 random-length events.
(Note that because the Random object is seeded with the same
number every time, all three random sequences will be the
same.)
These results are not significant. Running the test
repeatedly gives small variations in the numbers, but
there is not any repeatable slowdown demonstratable with the
WaitCursorEventQueue. Happily, this confirms my subjective
experiences.
Conclusion
This code was hard work, and I did not do it all myself.
I did it for my company (who has given me permission to
publish it), and I had help. I would like to thank Jason Trump
for brainstorming, debugging, and sorting out threading logic.
I would also like to thank Ben Schroeder for being so instrumental
in teaching me threading so that I could understand it well,
years ago.
Well, that's it! You now have an automatic wait cursor manager,
which you can install and use in any java program you like.
You should not have to think about the wait cursor ever again!
I hope this article was clear enough to make it understandable,
and not too long. If you have questions, please feel free to
email me at truist-waitcursor@truist.com.
Enjoy!
Nathan
Well, I certainly enjoyed editing this newsletter :-) Thanks Nathan,
that was a really useful newsletter...
Heinz
GUI Articles
Related Java Course
Discuss at The Java Specialist Club
|