
Optimizing Custom Java GUI Elements
This article discusses optimizing custom Java graphical user interface (GUI) components
including technical tips to increase performance and a tool to profile Java applications. This
article is intended for beginner and intermediate Java programmers.
Java is an excellent platform to develop complex graphical user interfaces. However, many developers creating
Java GUIs fail to use the correct Java components and painting techniques. Failure to
do so results in an application that is sluggish and frustrating to use. This
article will discuss how to optimize the performance of Java GUI components by
developing a sample component and performing basic benchmarking to find bottlenecks. After
we find the bottlenecks subtle changes will be made to make huge leaps in performance.
Our sample component will display the contents of a microarray. Briefly a DNA microarray, or DNA chips are fabricated by
high-speed robotics, generally on glass, for which probes with a known identity are used to determine complementary binding thereby
allowing massively parallel gene expression and gene discovery studies. Each probe is a tiny hole on the
glass slide, typically less then 200 microns in diameter, and are large enough to host the
material needed to conduct the experiment. A slide can have thousands of probes so an experiment with a
single DNA chip can provide researchers information on thousands of genes simultaneously.
In order for researchers to interpret the results of an experiment the microarray slide needs to be analyzed on a per spot basis.
This specialized analysis produces an
expression level for each spot represented by two float values. Our Java component will
visualize these levels in a grid using colors to represent the degree of expression level. Maximum
positive values are displayed as solid red rectangles, maximum negative values are
solid green rectangles and value in between feature various intensities of green or red.
A lightweight custom UI component should extend
javax.swing.JComponent or a more specialized Swing component class and implement
the paintComponent method. If a component is going to draw
its content on top of a blank or transparent background then javax.swing.JPanel
subclass is recommended. The initial implementation of the
paintComponent method for our component evaluates all the values to draw microarray spots.
The code uses two methods from java.awt.Graphics,
fillRect and drawRect to draw spots. The color of each spot is
normalized as a ratio of a current value to min or max value of the microarray because
the java.awt.Color class applies only floats from
0.0f to 1.0f.
Listing 1. The initial implementation of the paintComponent method.
public class ArrayViewer extends JPanel {
private float[][] microarray;
public ArrayViewer(float[][] microarray) {
this.microarray = microarray;
...
}
...
protected void paintComponent(Graphics g) {
super.paintComponent(g);
float max = getMaxValue();
float min = getMinValue();
final int rows = this.microarray.length;
final int cols = this.microarray[0].length;
for (int row = 0; row < rows; row++) {
for (int column=0; column < cols; column++) {
float value = this.microarray[row][column];
Color color = value > 0 ? new Color(value/max, 0, 0) : new Color(0, value/min, 0);
g.setColor(color);
g.fillRect(column*SPOT_WIDTH, row*SPOT_HEIGHT, SPOT_WIDTH, SPOT_HEIGHT);
g.setColor(Color.black);
g.drawRect(column*SPOT_WIDTH, row*SPOT_HEIGHT, SPOT_WIDTH-1, SPOT_HEIGHT-1);
}
}
}
}
|
The result of this implementation is very poor performance taking about 20 seconds to display a 32768x100 array of data.
This is not acceptable so we will look to optimize the code to increase drawing performance. The
first optimization step is searching for performance bottlenecks using
a code profiler. As a side note, we highly recommend using a profiler on your source. It helps find out leaks and bugs in addition
to finding performance bottlenecks. Borland OptimizeIt profiler was used perform the analysis on our
microarray component with results illustrated below:
Figure 2. Profiling information (the initial version).
The profile reports that most of the time is wasted invoking graphics primitives used to drawing and fill rectangles. In fact
over 600,000 combined invocations of drawRect and
fillRect occur for every paint method invocation.
The goal of this optimization step is to reduce the number of painting
operations using information about spots layout. In other words, the component should paint only those spots that
need painting. Repaints are trigger by moving windows, scrolling or other user actions and can occur frequently.
The java.awt.Graphics getClipBounds
method returns the bounding rectangle of the current clipping area, the region that must be repainted. The coordinates of this
area and information about spots layout makes it easy to determine which spots need to be painted.
In listing 2, this method is used to detect which exactly spots should be painted, for example, if
clipping area has 300x300 pixels size then repaint only 15x60 spots (spot size is 20x5).
Listing 2. The paintComponent method using the clipping area.
public class ArrayViewer extends JPanel {
private float[][] microarray;
public ArrayViewer(float[][] microarray) {
this.microarray = microarray;
...
}
...
protected void paintComponent(Graphics g) {
super.paintComponent(g);
float max = getMaxValue();
float min = getMinValue();
final int rows = this.microarray.length;
final int cols = this.microarray[0].length;
int topRow = 0;
int bottomRow = 0;
int leftColumn = rows;
int rightColumn = cols;
Rectangle clip = g.getClipBounds();
if (clip != null) {
leftColumn = clip.x/SPOT_WIDTH;
topRow = clip.y/SPOT_HEIGHT;
rightColumn = Math.min((clip.x+clip.width)/SPOT_WIDTH+1, cols);
bottomRow = Math.min((clip.y+clip.height)/SPOT_HEIGHT+1, rows);
}
for (int row = topRow; row < bottomRow; row++) {
for (int column = leftColumn; column < rightColumn; column++) {
float value = this.microarray[row][column];
Color color = value > 0 ? new Color(value/max, 0, 0) : new Color(0, value/min, 0);
g.setColor(color);
g.fillRect(column*SPOT_WIDTH, row*SPOT_HEIGHT, SPOT_WIDTH, SPOT_HEIGHT);
g.setColor(Color.black);
g.drawRect(column*SPOT_WIDTH, row*SPOT_HEIGHT, SPOT_WIDTH-1, SPOT_HEIGHT-1);
}
}
}
}
|
It is a common mistake for developers to use the
java.awt.Graphics class without advantages of clipping areas. With this adjustment our component can
display the same amount of data in 0.2 seconds (Figure 3). This is about 100 times faster then the initial version and
shows how a developer can use a clipping area to improve application performance.
Now the new version of the component is much faster but we will now look at another problem. When scrolling
our component it is necessary to paint approximately 20-30 frames per second. Our component currently does not handle this
well and is the next optimization target.
Figure 3. Profiling information (the intermediate version).
The analysis indicates that most of the time is now spent to calculate max and
min values of the array. The reason to calculate these values in paint method
is to make sure that values of the array are displayed correctly. For example,
if values of the array were changed anywhere outside of the component they will be correctly displayed
just after invocation of the repaint method.
To remedy this problem the component needs to be modified to move CPU intensive code outside of
the paint method.
The next implementation of the component uses new two member variables to store calculated maximum and minimum
values and the code used to perform this calculation is moved out of the paint method.
Listing 3. The paintComponent method with removed calculation code.
public class ArrayViewer extends JPanel {
private float[][] microarray;
private float max;
private float min;
public ArrayViewer(float[][] microarray) {
this.microarray = microarray;
this.max = getMaxValue();
this.min = getMinValue();
...
}
...
protected void paintComponent(Graphics g) {
super.paintComponent(g);
final int rows = this.microarray.length;
final int cols = this.microarray[0].length;
int topRow = 0;
int bottomRow = 0;
int leftColumn = rows;
int rightColumn = cols;
Rectangle clip = g.getClipBounds();
if (clip != null) {
topRow = getTopIndex(clip.y);
bottomRow = getBottomIndex(clip.y+clip.height, rows);
leftColumn = getLeftIndex(clip.x);
rightColumn = getRightIndex(clip.x+clip.width, cols);
}
for (int row = topRow; row < bottomRow; row++) {
for (int column = leftColumn; column < rightColumn; column++) {
float value = this.microarray[row][column];
Color color = value > 0 ? new Color(value/this.max, 0, 0) : new Color(0, value/this.min, 0);
g.setColor(color);
g.fillRect(column*SPOT_WIDTH, row*SPOT_HEIGHT, SPOT_WIDTH, SPOT_HEIGHT);
g.setColor(Color.black);
g.drawRect(column*SPOT_WIDTH, row*SPOT_HEIGHT, SPOT_WIDTH-1, SPOT_HEIGHT-1);
}
}
}
}
|
Table 1. Comparing the component drawing performance
|
Version |
Time to paint, ms |
| initial |
19200 |
| intermediate |
190 |
| release |
10 |
Conclusion
After performing minor modifications our release version of the component is 2000 times faster then the
initial version taking 10 ms to display the microrray data. The largest performance gain was using the clipping area and
this technique can be applied for all the components that have a priori knowledge of sub-component layout.
Furthermore, each sub-component can be painted individually where a rectangle element could draw only the visible part
or a line would only draw only between points of intersection with the clipping
region.
Using the following guidelines will help you make powerful complex, scaled controls:
- Use a code profiler on your application to find bottlenecks and other surprises
- Use the clip bounds to draw only the necessary part of a component.
- Minimize logic in paint method
- Swing supports built-in double-buffering via the
JComponent doubleBuffered property, and it defaults to true for all Swing components.
- Extensions of Swing components that have UI delegates (including JPanel),
should typically invoke super.paintComponent() within their own paintComponent() implementation.
- Swing introduces two properties to that can be used to maximize painting efficiency: opaque and
optimizedDrawingEnabled. Work with these properties in your application to maximum Swing performance.
Resources
|