|
|
By Scott Violet & Kathy Walrath
One common type of application features a JTable with frequently updated data. This style of application is often found in the financial industry, but it can crop up in other industries, as well. This article explores techniques for improving the performance of this style of application, affectionately called Christmas tree applications because the rapid updating of their GUIs resembles blinking lights on holiday trees.
Because Swing is a general purpose toolkit, many of its design decisions were made in the name of flexibility and extensibility. When you know what your application is doing, however, and know that you do not require the level of flexibility offered by Swing, you can make many optimizations to speed things up. Some of these performance improvements come at a price: loss of flexibility. This isn't an issue in the sample application that goes along with this article.
Before you begin incorporating any of the suggestions from this article into your application, you need to thoroughly understand your application. If it takes your application 15 seconds to figure out what needs to be updated, speeding up the painting code by a factor of 10, or even 100, likely won't make much of a difference to the user. To this end we highly recommend that you profile your application without the painting code to make sure the data side of your application performs reasonably well.
Once you're confident in your non-painting code, you can profile the application with painting code. We used Optimizeit to gather information about where our sample application's time was spent. We found many unnecessary repaint requests and discovered we could eliminate some unnecessary code and object creation from commonly executed methods. We also found that we could streamline and speed up the publishing and processing of table model events, which inform the JTable that the table's data has changed.
Our goal should be obvious, but it is worth stating because it will affect some of the decisions that we make later on: the application should be responsive to the user.
This article comes with a sample Christmas tree application, which looks like this:
Source code for our sample application is available under BSD+ license. All the source and build files are in XmasTreeSource.zip . The source code consists of the following classes:
GeneratorThread - Subclass of Thread that is responsible for accumulating changes. The thread sleeps for a defined period and then generates a set of changes to be published later. This behavior simulates a real application where a thread is talking to some continually updating data feed.UpdateThread - Subclass of Thread that is responsible for taking a set of changes generated by GeneratorThread and publishing the changes to the model.CTTableModel - Custom TableModel that models a Set of Datas. It is implemented as a List of Lists. Additionally, this model uses a Map to facilitate finding the row of individual data items.Data - Represents the data of the application. In our implementation, Data objects contain no real data.DataChange - Consists of a Data and a column number. GeneratorThread generates a Map of DataChanges that map from DataChange to a new value.CTTable - JTable subclass that implements the techniques outlined in this article.CTTableCellRenderer - Subclass of DefaultTableCellRenderer that implements the techniques outlined in this article.VisibleTableModelEvent - Subclass of TableModelEvent used to avoid repainting regions that aren't visible.Main - Ties everything together and builds the GUI.The sample application allows you to configure the following options, either by modifying Main.java or through text fields in the GUI:
JTable Painting
JTable, likeJTreeandJList, uses renderers to paint individual cells. Each renderer wraps aComponent(which we'll call the rendering component) and can be thought of as a rubber stamp: It is moved to the region of the affected cell and painted. Painting aJTableconsists of two parts -- determining the cells to be painted, based on the clip region of theGraphicsobject passed topaint, and then painting each cell. For each cell that needs to be painted, the following steps occur:
- Getting the renderer by invoking
getCellRenderer.- Getting the value from the model.
- Getting the rendering component.
- Adding this component to the
CellRendererPaneand painting theCellRendererPane. (CellRendererPaneis a convenience class used to paint the rendering component. We'll discuss it in more detail later.)Let's look for potential performance improvements in painting individual cells.
Step 1: Getting the renderer
The
JTableimplementation of thegetCellRenderermethod retrieves table cell renderers in the following way:
- The method checks whether a renderer is associated with the table column. If so, the method returns that renderer.
- If not, the method returns the renderer most appropriate for the
Classof theObjectreturned from the model.Custom renderers such as ours are often registered using the latter,
Class-based approach because of its flexibility. Unfortunately, to obtain the renderer theJTablemust do a lookup in aMap. Using aMapis typically a cheap operation, but because the lookup is performed for every cell that is painted we'll avoid this approach.The alternate approach (associating a renderer with a table column) is relatively cheap and provides quite a bit of flexibility. But we can do better!
Because this application uses the same renderer for all cells,
CTTableoverridesgetCellRendererto simply return the renderer. This is the fastest approach possible as no additional lookup is done to obtain the renderer.Step 2: Getting the value from the model
Because
CTTableModelis implemented as aListofLists, getting the value is a pretty cheap operation. If you decide to implement a customTableModel, be sure to make getting the value a cheap operation, and avoid synchronization if possible.Step 3: Getting the rendering component
Our custom table cell renderer,
CTTableCellRenderer, is a subclass ofDefaultTableCellRenderer. Like its parent, our renderer responds to thegetTableCellRendererComponentmethod by updating its own state (to reflect the desired appearance for the cell being rendered) and then returning itself.To improve performance,
DefaultTableCellRendereroverrides a handful of methods to be more efficient -- often, to do nothing. If you create customTableCellRenderers not derived fromDefaultTableCellRenderer, you should override these methods as well. Refer to the API documentation forDefaultTableCellRendererfor a list of these methods. Note that the override ofisOpaque, added in 1.4, is appropriate for table cell renderers written for earlier versions, as well.Our custom renderer overrides and simplifies two of the methods already overridden by
DefaultTableCellRenderer. We override thefirePropertyChangemethod to do nothing at all, since this application does not show HTML text in the table. (DefaultTableCellRendereroverridesfirePropertyChangeto only notify listeners when the "text" property changes, which is required for HTML support.) Our renderer also overrides theisOpaquemethod to be simpler than theDefaultTableCellRendererimplementation. Details of its implementation are discussed later in this article.Two additional methods are overridden by
CTTableCellRendererto do nothing: the no-argumentrepaintmethod andinvalidate. The first,repaint, can be overridden because the renderer is only transient; any repaints do not need to processed later. Additionally we can overrideinvalidatesafely because our rendering component does not contain childComponents and does not rely oninvalidateto reset any internal state. We expectDefaultTableCellRendererto override the no-argumentrepaintmethod in a future release.One key thing to remember is that your
TableCellRenderershould do as little as possible in preparing the component to be used for rendering: set the text and colors, and that should be it. Because the rendering component is fetched for every cell, any additional operations tend to balloon in cost. For example, it is often necessary to support tooltip text in the cells of aJTable, but setting the tooltip text on each cell is an expensive operation. Instead, overridegetToolTipTextin aJTablesubclass.Step 4: Painting the CellRendererPane
Most of the painting for any
JComponentis handled by the component's UI object. In most look and feels, the UI object for aJTabledescends from theBasicTableUIclass.CellRendererPaneis a convenience class used byBasicTableUIto paint the rendering component.In step 4, the rendering component is added to the
CellRendererPaneand the UI asks theCellRendererPaneto paint. Having theCellRendererPanemight seem unnecessary, but it allows a rendering-savvy component to always be parented to theJTable, rather than having to add it on every paint. (TheCellRendererPaneis not visible.) Having theCellRendererPanealso gives us a single place to override as no-ops a number of methods that typically walk the containment hierarchy (such asinvalidate).Our application uses a custom
CellRendererPane(CTTable.CellRendererPane) to avoid duplicating theGraphicsobject passed into its painting method. Normally, Swing components clone theGraphicsobject before passing it to children, so that any changes made by the child can't clobber theGraphicsobject used by the parent. Clobbering theGraphicsobject means setting a property on it, such asXORMode, that doesn't later get reset by either the child's code or Swing's code. Because we know that our painting code doesn't clobber theGraphicsobject, we can skip the costly step of cloning it.The trick with having a custom
CellRendererPanesubclass is installing it. Because theCellRendererPaneis managed by the UI (BasicTableUI), we need to create a subclass ofBasicTableUIthat replaces the default renderer with our customCellRendererPane.Warning: Subclassing UIs, while possible, locks you into a particular look and feel. In our example this isn't an issue, but it might be in your application.
To force a subclass of
JTableto always have a particular UI, override theupdateUImethod:public void updateUI() { super.updateUI(); setUI(new CTTableUI()); }Our subclass of
BasicTableUIreplaces theCellRendererPaneusing the following code:
Option Description Columns Number of columns the table is to contain. Rows Number of rows the table is to contain. Update Sleep Amount of time, in milliseconds, to sleep between runs of UpdateThread.EQ Sleep Amount of time, in milliseconds, UpdateThreadshould sleep (if the event queue is busy) before trying to update the model.Update All Threshold Number of cells that need to change before a table model event is fired indicating the complete table has changed. Generator Sleep Amount of time GeneratorThreadsleeps between updates.Batch Size Number of cells GeneratorThreadis to update.class CTTableUI extends BasicTableUI { public void installUI(JComponent c) { super.installUI(c); c.remove(rendererPane); rendererPane = new CTCellRendererPane(); c.add(rendererPane); } }
Optimizing Painting in the Rendering Component
Having a custom renderer (
CTTableCellRenderer) lets us improve the code that's executed every time a table cell is painted. Our improvements center around avoiding unnecessary object creation and method calls, in general, and streamlining how we deal with the background color, in particular.One small improvement we made was having
CTTableCellRendereroverridesetBackgroundandgetBackgroundto cache the background color. (DefaultTableCellRendererchanges the background color on every invocation ofgetTableCellRendererComponent.) Although getting and setting the background are typically cheap operations, removing unnecessary method calls can't hurt.We also avoid unnecessary painting of the background.
CTTableCellRendererdoes this by overriding theisOpaquemethod to returntrueonly when the background color of the cell differs from that of theJTable. This works because the defaultJComponentpainting code (inComponentUI.update) fills in the component's background only if the component is opaque. When the cell's background color matches that of the table, we can rely on the table's painting code to fill in the background.To understand how we simplified the painting code, in general, you need to understand what happens when a
JComponentgets a paint request. When aJComponentis asked to paint itself, itspaintmethod invokes itspaintComponent,paintBorder, andpaintChildrenmethods. Thepaintmethod also checks the clipping rectangle and performs other operations to attempt to improve painting performance.For our rendering component, only the code in
paintComponentis necessary. The rest is irrelevant because our component is small, paints quickly, doesn't use aBorder, and has no child components to paint. Thus, we can save a method call by moving the painting code frompaintComponentinto thepaintmethod.The default
paintComponentimplementation clones theGraphicspassed into it and invokesupdateon the UI to handle the actual painting. As we mentioned before, our application will not clobber theGraphicsduring painting. We can therefore safely skip cloning theGraphicsobject, which lets us avoid the creation of an additionalGraphicsobject per cell during painting. We can thus override thepaintmethod so that it simply invokes the UI'supdatemethod. TheCTTableCellRendererversion ofpaintends up looking like this:public void paint(Graphics g) { ui.update(g, this); }Notifying the Table of Data Changes
Up to this point, we've discussed various options for reducing the overhead of painting, but we haven't delved into notifying the table of updates. We'll briefly talk about this now.
First, you need to understand how the table model and table are notified of data changes. When the data represented by the
TableModelchanges -- for example, you change a cell's value (DefaultTableModel.setValueAt) or add a row (DefaultTableModel.addRow) -- the table model sends aTableModelEventto allTableModelListenersregistered on the model, notifying them of the change. EachJTableinstalls itself as a listener on its table model so that the table can update its display in response to model changes.Most Christmas tree applications have a thread that is responsible for reading the data to be displayed in the table. Having this separate thread is a good thing as it does not lock down the event-dispatching thread in any way. The
UpdateThreadin our application takes the following approach in notifying the table model of changes:
- Accumulate some changes to apply.
- When the table is not scrolling and the event-dispatching thread has no pending events, inform the table model of all the changes.
We notify the table model by executing a loop on the event-dispatching thread that calls the table model's
set(row, column, value, notifyFlag)method (a non-standard method we added to our table model) once for each cell whose value has changed. We'll discuss thenotifyFlagparameter later.
- To allow the user time to interact with the application, sleep for some amount of time before returning to 1.
Publishing Changes
UpdateThreadusesinvokeAndWaitto schedule aRunnableto update the table model. To avoid scheduling moreRunnables than the system can keep up with, thereby causing the application to be dead in the water, the calling thread needs to block until it knows theRunnablehas been processed;invokeAndWaitdoes just this. We can't useinvokeLaterbecause it doesn't block the caller, with the result that moreRunnables might be scheduled than could be processed.After the model changes an event is published that ultimately makes its way to the
JTableand results in a repaint request for the affected cell or cells. Because such a large portion of the table can be updated at once, notifying listeners of the model change and making the subsequent repaint request need to have as little overhead as possible.
JTableresponds to table model events by invokingrepaintfor the region identified by theTableModelEvent. As only a small portion of theJTableis visible at a time, the majority of the updated regions are not visible, meaning much of the work done byJTableto process theTableModelListenermethod notification is unnecessary, as well as costly.To avoid unnecessary work but continue to notify listeners, we've created a custom subclass of
TableModelEventthatCTTableModeluses when handling changes fromUpdateThread. The new subclass, calledVisibleTableModelEvent, adds the methodisVisible(JTable), which calculates whether the rows and columns specified by the event are visible in the passed-in table.CTTableoverridestableChanged(its method that processesTableModelEvents) to ignoreVisibleTableModelEvents for non-visible cells.VisibleTableModelEventis mutable so thatCTTableModelcan reuse a single instance ofVisibleTableModelEvent. This allows for minimal garbage creation when the table is modified.Warning: Having mutable event objects could cause problems if consumers cache the
TableModelEvents. This typically is not an issue, however -- especially internally in Swing.
One additional point worth mentioning is that sometimes the number of changes is close to that of the underlying model -- most or all cells have changed. In this case, it is far cheaper to fire a single event indicating all the cells have changed (using
TableModel.fireTableDataChanged) than for the model to send an update for each cell. In our application,UpdateThreadchecks the number of changes before it enters the loop that invokesset(row, column, value, notifyFlag)on the table model. If the number of changes reaches some threshold,UpdateThreadprevents the table model from firing the individual events by setting thenotifyFlagparameter tofalse. Once the loop has finished,UpdateThreadfires a single "everything changed" event.Delaying Publishing Changes
To keep the application responsive, changes are not published when the user is scrolling. You can check for scrolling by attaching a
ChangeListenerto the horizontal and verticalJScrollBars contained in theJScrollPane. For example:
ChangeListener changeListener = new ChangeListener() { public void stateChanged(ChangeEvent e) { BoundedRangeModel m = (BoundedRangeModel)(e.getSource()); setUpdatesEnabled(!(m.getValueIsAdjusting())); } }; sp.getVerticalScrollBar().getModel().addChangeListener(changeListener); sp.getHorizontalScrollBar().getModel().addChangeListener(changeListener);
In the preceding code,
setUpdatesEnabled(false)is invoked when the user starts scrolling. When the scrolling stops,setUpdatesEnabled(true)is invoked.Attaching the
ChangeListenerallows determining when the user is scrolling, but there are often other times where the application may be busy responding to user requests and updates should be avoided. A convenient, simple way to check for a busy application is to check whether any pending events are on theEventQueue:EventQueue queue = Toolkit.getDefaultToolkit().getSystemEventQueue(); while (queue.peekEvent() != null) { sleep(10); }This code loops until no events are on the
EventQueue. It isn't foolproof, but it's a good start that doesn't require additional changes to the rest of the application.Publishing changes now looks like this:
public void publishChanges() { // Wait until the user isn't scrolling synchronized(this) { while (!updatesEnabled) { wait(); } } // And wait until there are no pending events. EventQueue queue = Toolkit.getDefaultToolkit().getSystemEventQueue(); while (queue.peekEvent() != null) { sleep(10); } Runnable publishRunnable = new Runnable() { public void run() { publishChangesOnEventDispatchingThread(); } }; // publish the changes on the event-dispatching thread SwingUtilities.invokeAndWait(publishRunnable); // Wait until the system has completed processing of any // events we triggered as part of publishing changes. SwingUtilities.invokeAndWait(emptyRunnable); }
An important thing to note is that although we delay publishing of changes, the delay only occurs when the user is busy with the application and not likely to notice that we have delayed notification. The
GeneratorThreadcontinues to generate changes, and as soon as the system is ready we publish them to the user. Nothing is lost.Results
The following table shows the time to paint the visible contents of the table with various optimizations. The optimizations have been cumulatively applied, so that the last line is with all the optimizations outlined in this article. The timings come from an Ultra 60 workstation and version 1.4.
Optimizations Painting Time (in ms) Baseline 140 + override getTableCellRendererComponent134 + override firePropertyChange131 + override TableCellRenderer.paint128 + create custom CellRendererPane104 + miscellaneous CellRendereroverrides99 The following table shows the before and after times of publishing a set of 15,000 changes to the model.
Optimization Publishing Time (in ms) Baseline 331 + VisibleTableModelEvent48 Warning
This article advocates short circuiting the cloning of the Graphics passed to the Renderer. While this provides a dramatic performance boost it has the potential to cause rendering artifacts in future versions of Swing that offer different painting behavior. This is very unlikely to happen, but developers wishing to support multiple JREs should be aware of this.
Conclusion
As we've just shown, a handful of targeted tweaks can improve painting performance by roughly 30% and notification by a factor of 7. These improvements are possible because we understand the nature of the application and how it uses
JTable, and we can short-circuit unused features.Can we do more? You bet! A future article will discuss creating a custom
RepaintManager, as well as how to deal withJTableif you cannot batch updates.
