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."

I first recommend Appendix B "Swing Components and Multi-threading" of Kim Topley's "Core Java Foundation Classes" (Prentice Hall). The Swing development team also explains this issue clearly at http://java.sun.com/products/jfc/tsc/articles/threads/threads1.html :

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.

See more relevant articles at http://java.sun.com/products/jfc/tsc/articles/index.html .

§    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.
*/

Bill Harlan, 1999


Return to parent directory.