Improving Swing Performance

Swing must be the easiest framework ever
devised for building complicated graphical
user interfaces. Swing still requires an
understanding of certain complexities common
to all event-driven systems.

§ Responsiveness of Graphical Components

Half a dozen times now I have seen
programmers create a graphical component with
unreasonably sluggish behavior. All proved
to be variations of the same problem, with
the same solution.

A user resized or scrolled a window, fiddled
with knobs, or did something that caused a
graphical view to be redrawn over and over
again. Redraw events were generated faster
than the redraw could execute, so the redraw
lagged behind the user's activity. These
views always contained custom graphical
content, not standard Swing widgets. Redraws
were handled by the programmer, by filling
polygons, calculating rasters, and so on. A
typical first implementation redraws the
entire view from scratch each time.

Essentially this is a real-time programming
problem, where events that arrive too late
must be ignored. Swing cannot safely ignore
events as a default policy. Only the
programmer knows how events affect state.
Only the programmer can anticipate how much time
redraws might take. (X-Windows must also
handle this problem explicitly. I have seen
X-based software deliberately change redraw
policies on machines with different
performance.)

One solution is for the programmer's redraw
event listener to block further redraws until
the current one finishes. After completion,
only the latest redraw is executed and the
rest are discarded. A further refinement is
for every redraw to pause for so many
milliseconds. If another redraw event
arrives right away, the first can be ignored.
If not, the redraw continues. (See sample
code below.)

Another solution is for the programmer to
draw to a large off-screen buffer, so that
the redraw event listener just copies part of
a bitmap. Copying is fast, but not all
redraws can be done this way (say 3D
rotation). Swing does use default double
buffering to avoid flashing images as they
draw. But the default buffering cannot help
with changes in image dimensions.

When you create a new graphical component,
extend a new class from JPanel, not from
Canvas. Override
paintComponent(Graphics g) not
paint(Graphics g). The default
paint(Graphics g) implemented in
JPanel will perform double buffering and
call your paintComponent(Graphics g) when
necessary. The first line of your
paintComponent(Graphics g) should call
super.paintComponent(g) so that JPanel
can repaint the background color. You can
then cast g to a Graphic2D. More
details are available from
http://java.sun.com/products/jfc/tsc/articles/painting/index.html

Try JComponent.setDebugGraphicsOptions(int
debugOptions) to learn more about how your
component is drawn.

If you are uncertain where your events are
coming from, try
java.awt.Toolkit.getDefaultToolkit().addAWTEventListener(AWTEventListener listener, long
eventMask) or override
Component.processEvent(AWTEvent), and
delegate to super.processEvent(AWTEvent).

§ Working in the Event-Dispatching thread

Intermediate users sometimes think they have
discovered an unreasonable number of bugs.
Components freeze or do not refresh properly.
Events seem to be lost. A documented method
appears to have no effect. Workarounds only
make the problems less likely to occur. Most
likely, these programmers have created
multiple threads without proper attention to
the "Swing event-dispatching thread."

Once a Swing component has
been realized, all code that might affect or
depend on the state of that component should
be executed in the event-dispatching thread.

A component is "realized" as soon as event
listeners may be called.

The programmer usually constructs and
assembles GUI components in a single thread.
Action listeners (callbacks or event
handlers) are always called by Swing in the
separate event-dispatching thread (EDT). The
EDT is like an X-event loop where all GUI
activity maintains a well-determined order.
Only one GUI event can occur at a time,
without asynchronous overlap.

Sometimes a GUI event initiates
time-consuming work. If an event listener
does not return, then all GUI components will
freeze, block further events, and not refresh
when uncovered. Time-consuming listeners
should spawn a new worker thread and return
quickly. When done, the worker thread may
need to update the GUI again. To reenter the
EDT, the worker must instantiate a
Runnable object, then call
SwingUtilities.invokeLater() or
SwingUtilities.invokeAndWait().

Be careful after adding event-listeners. If
your initialization code changes selections
in a table or combo-box, then listeners will
be called. Add event listeners as late as
possible to avoid confusion.

You may have a method that can be called from
inside or outside the EDT. Check
SwingUtilities.isDispatchThread() before
changing the GUI from this method.

§ Sample code

Here are convenience methods to implement
strategies in the previous sections.

public class RealTimeSwing {
/** Run this Runnable in the Swing Event Dispatching Thread, and
return when done with execution.
This method can be called whether or not the current thread is
in the Swing thread.
@param runnable This is the code to be executed in the Swing thread.
*/
public static void invokeNow(Runnable runnable) {
if (runnable == null) return;
try {
if (SwingUtilities.isEventDispatchThread()) {runnable.run();}
else {SwingUtilities.invokeAndWait(runnable);}
} catch (InterruptedException ie) {
LOG.warning("Swing thread interrupted");
Thread.currentThread().interrupt();
} catch (java.lang.reflect.InvocationTargetException ite) {
ite.printStackTrace();
throw new IllegalStateException (ite.getMessage());
}
}
/**
Use this method for handling events in realtime,
when the events may be generated more quickly than the
the handler can complete. Call this method from the
appropriate Listener.
Executes Runnables inside and outside the Swing Thread.
Returns immediately.
If this method is called again with the same id after less
than the specified pause, then the first call will be
ignored. The most recent call with a given id
will not be allowed to start until previous call finishes.
When previous call finishes, only the most recent call with
same id will run. Others will be discarded.
@param id This string uniquely identifies this task.
If this method is called again with the same id within
the specified number of milliseconds, then the first call
will not be executed. If this id is already executing,
then it will wait until either the previous execution
finishes, or until a later call with the same id.
@param milliseconds Wait this long in a separate thread
before executing Runnables. You can safely set this to zero.
Set the time less than the expected time to execute the
Runnables. If you set to 0, then a series of calls will
be executed at least twice. If you set to greater than 0,
then the first call may be ignored if followed quickly by
another call.
@param worker This Runnable will be executed first outside
the Swing thread. Set to null to skip this step.
@param refresher This Runnable will be executed inside the
Swing thread after the worker has completed. Activity
that uses or changes the state of Swing widgets should be
included here. Set to null to skip this step.
*/
public static void invokeOnce(String id, final long milliseconds,
final Runnable worker, final Runnable refresher) {
synchronized (s_timestamps) {
if (!s_timestamps.containsKey(id)) { // call once for each id
s_timestamps.put(id,new Latest());
}
}
final Latest latest = s_timestamps.get(id);
final long time = System.currentTimeMillis();
latest.time = time;
(new Thread("Invoke once "+id) {public void run() {
if (milliseconds > 0) {
try {Thread.sleep(milliseconds);}
catch (InterruptedException e) {return;}
}
synchronized (latest.running) { // can't start until previous finishes
if (latest.time != time) return; // only most recent gets to run
if (worker != null) worker.run(); // outside Swing thread
if (refresher != null) invokeNow(refresher); // inside Swing thread
}
}}).start();
}
private static Map s_timestamps = new HashMap();
private static class Latest {
/** Last time for this event */
public volatile long time=0;
/** for synchronization */
public final Object running = new Object();
}
}
/*
Copyright (c) Bill Harlan, 1999
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- Neither the names of contributors, nor the names of their employers may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/