Christmas Tree Applications
|
| 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, UpdateThread should 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 GeneratorThread
sleeps
between updates.
|
| Batch Size | Number of cells GeneratorThread
is to update.
|
JTable, like JTree and JList,
uses renderers to paint individual cells.
Each renderer wraps a Component
(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 a JTable
consists of two parts --
determining the cells to be painted,
based on the clip region of the Graphics
object passed to paint,
and then painting each cell.
For each cell that needs to be painted,
the following steps occur:
getCellRenderer.
CellRendererPane
and painting the CellRendererPane.
(CellRendererPane is 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.
The JTable
implementation of the getCellRenderer method
retrieves table cell renderers in the following way:
Class
of the Object returned 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
the JTable must
do a lookup in a Map.
Using a Map is 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, CTTable overrides
getCellRenderer to simply return the renderer. This is the
fastest approach possible as no additional lookup is done to
obtain the renderer.
Because CTTableModel is implemented as a List of
Lists, getting the value is a pretty cheap
operation. If
you decide to implement a custom TableModel, be sure
to make getting the value a cheap operation, and avoid
synchronization if possible.
Our custom table cell renderer,
CTTableCellRenderer, is a subclass of
DefaultTableCellRenderer.
Like its parent,
our renderer responds
to the getTableCellRendererComponent method
by updating its own state (to reflect
the desired appearance for the cell being rendered)
and then returning itself.
To improve performance,
DefaultTableCellRenderer overrides a handful of
methods to be more efficient --
often, to do nothing.
If you create custom TableCellRenderers
not derived from DefaultTableCellRenderer,
you should override these methods as well. Refer to the
API documentation for
DefaultTableCellRenderer
for a list of these methods.
Note that the override of isOpaque,
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 the firePropertyChange method
to do nothing at all,
since this application does not show HTML text in the table.
(DefaultTableCellRenderer overrides
firePropertyChange
to only notify listeners when the "text" property changes,
which is required for HTML support.)
Our renderer also overrides the isOpaque
method to be simpler than the
DefaultTableCellRenderer implementation.
Details of its implementation are discussed later in this article.
Two additional methods are
overridden by CTTableCellRenderer
to do nothing:
the no-argument repaint method and
invalidate.
The first,
repaint, can be overridden because
the renderer is only transient;
any repaints do not need to processed later. Additionally
we can override invalidate safely
because our rendering component
does not contain child Components
and does not rely on invalidate
to reset any internal state.
We expect
DefaultTableCellRenderer
to override the no-argument repaint method
in a future release.
One key thing to remember is that your TableCellRenderer
should 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 a JTable,
but setting the
tooltip text on each cell is an expensive operation.
Instead,
override getToolTipText in a
JTable
subclass.
Most of the painting for any JComponent
is handled by the component's UI object.
In most look and feels,
the UI object for a JTable
descends from the BasicTableUI class.
CellRendererPane is a convenience class used by
BasicTableUI
to paint the rendering component.
In step 4, the rendering component is added to the
CellRendererPane and
the UI
asks the CellRendererPane to paint.
Having the CellRendererPane
might seem unnecessary,
but it allows a rendering-savvy component
to always be parented to the
JTable,
rather than having to add it on
every paint. (The CellRendererPane is not visible.)
Having the CellRendererPane
also gives us a single place
to override as no-ops a number of methods that typically
walk the containment hierarchy (such as invalidate).
Our application uses a custom CellRendererPane
(CTTable.CellRendererPane)
to avoid duplicating the Graphics
object passed into its painting method.
Normally, Swing components
clone the Graphics object
before passing it to children,
so that any changes made by the child can't
clobber the Graphics object used by the parent.
Clobbering the Graphics object
means setting a property on it,
such as XORMode,
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 the Graphics object,
we can skip the costly step of cloning it.
The trick with having a custom CellRendererPane subclass
is installing it. Because the
CellRendererPane is managed by the UI (BasicTableUI),
we need to create a
subclass of BasicTableUI that replaces the
default renderer with our custom CellRendererPane.
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
JTable
to
always have a particular UI, override the
updateUI method:
public void updateUI() {
super.updateUI();
setUI(new CTTableUI());
}
Our subclass of BasicTableUI replaces the
CellRendererPane
using the following code:
class CTTableUI extends BasicTableUI {
public void installUI(JComponent c) {
super.installUI(c);
c.remove(rendererPane);
rendererPane = new CTCellRendererPane();
c.add(rendererPane);
}
}
|
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 CTTableCellRenderer override
setBackground and getBackground
to cache the background color.
(DefaultTableCellRenderer changes the
background color on every
invocation of getTableCellRendererComponent.)
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.
CTTableCellRenderer
does this by overriding the isOpaque method
to return true only
when the background color of the cell
differs from that of the JTable.
This works because the default JComponent painting code
(in ComponentUI.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 JComponent gets a paint request.
When a JComponent is asked to paint itself,
its paint method
invokes its paintComponent,
paintBorder,
and paintChildren methods.
The paint method also checks the clipping rectangle
and performs other operations to attempt to improve painting performance.
For our rendering component,
only the code in paintComponent
is necessary.
The rest is irrelevant because our component is small,
paints quickly,
doesn't use a Border,
and has no child components to paint.
Thus, we can save a method call
by moving the painting code from paintComponent
into the paint method.
The default paintComponent implementation clones
the Graphics passed into
it and invokes update on the UI to handle the actual
painting.
As we mentioned before,
our application will not clobber the Graphics
during painting. We can therefore safely
skip cloning the Graphics object,
which lets us avoid the creation
of an additional Graphics object per cell during
painting.
We can thus override the
paint method
so that it simply invokes the UI's update method.
The CTTableCellRenderer version
of paint ends up looking like this:
public void paint(Graphics g) {
ui.update(g, this);
}
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
TableModel changes --
for example,
you change a cell's value
(DefaultTableModel.setValueAt)
or add a row
(DefaultTableModel.addRow) --
the table model sends a TableModelEvent
to all TableModelListeners registered on the model,
notifying them of the change.
Each JTable installs 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 UpdateThread
in our application takes the following
approach in notifying the table model of 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 the notifyFlag parameter later.
UpdateThread uses invokeAndWait
to schedule a Runnable to update the table model.
To avoid scheduling more Runnables 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 the
Runnable has been processed;
invokeAndWait does just this.
We can't use invokeLater
because it doesn't block the caller,
with the result that more
Runnables might be scheduled than could be processed.
After the model changes an event is published that ultimately
makes its way to the
JTable
and 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.
JTable responds to table model events
by invoking repaint for the
region identified by the TableModelEvent.
As only a small portion of the
JTable
is visible at
a time, the majority of the updated regions are not visible, meaning
much of the work done by
JTable
to process the TableModelListener
method
notification is unnecessary, as well as costly.
To avoid unnecessary work
but continue to notify listeners,
we've created a custom subclass of
TableModelEvent
that CTTableModel uses
when handling changes
from UpdateThread.
The new subclass, called VisibleTableModelEvent,
adds the method isVisible(JTable),
which calculates whether the
rows and columns
specified by the event are visible
in the passed-in table.
CTTable
overrides tableChanged
(its method that processes TableModelEvents)
to ignore VisibleTableModelEvents
for non-visible cells.
VisibleTableModelEvent is mutable
so that CTTableModel can reuse a single
instance of VisibleTableModelEvent.
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,
UpdateThread
checks the number of changes before it enters
the loop that invokes
set(row, column, value, notifyFlag)
on the table model.
If the number of
changes reaches some threshold,
UpdateThread
prevents the table model
from firing the individual events
by setting the notifyFlag parameter
to false.
Once the loop has finished,
UpdateThread fires a single "everything changed" event.
To keep the application responsive, changes are not published when
the user is scrolling. You can check for scrolling by attaching a
ChangeListener to the horizontal and vertical JScrollBars contained
in the JScrollPane.
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 ChangeListener allows 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 the
EventQueue:
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 GeneratorThread
continues to
generate changes, and as soon as
the system is ready we publish them to the user. Nothing is
lost.
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
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.
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 with JTable
if you cannot batch updates.
Oracle is reviewing the Sun product roadmap and will provide guidance to customers in accordance with Oracle's standard product communication policies. Any resulting features and timing of release of such features as determined by Oracle's review of roadmaps, are at the sole discretion of Oracle. All product roadmap information, whether communicated by Sun Microsystems or by Oracle, does not represent a commitment to deliver any material, code, or functionality, and should not be relied upon in making purchasing decisions. It is intended for information purposes only, and may not be incorporated into any contract.
|
| ||||||||||||