ORIGINAL DRAFT

Monitoring software to intercept failures or anticipate maintenance requirements is tricky business, especially when dealing with large-scale, distributed systems. Whether you’re dealing with web servers or custom applications, it helps to visualize the state of the system using a suitable metaphor. One of those metaphors is a graph that shows a given value between a range of possibilities, charted over a a number of events. This month, we’ll implement a component that resembles the Windows Performance Monitor, allowing you to display a normalized range of values.

Figure 1: JMonitor showing values generated by a 
simulator class. Both the LED-style status bar and the scrolling graph show data points received through by
the MonitorModel.

Figure 1: JMonitor showing values generated by a simulator class. Both the LED-style status bar and the scrolling graph show data points received through by the MonitorModel.

One of our objectives is to make it possible to receive monitorable events at any suitable interval, so we won’t impose any timing requirements on monitorable events. If you need to control event timing more precisely, you can create a proxy class that polls values from a system, triggering monitor events at more predictable intervals. In most cases this won’t be necessary, but real-time systems take these issues into account and our component leaves that to the event generator in order to keep the interface more flexible.

As usual, we want to keep the model and view separate, so we’ll use a MonitorModel interface, which looks like this:

public interface MonitorModel
{
  public int getDataPointCount();
  public void setMaximumCount(int count);
  public void addDataPoint(double value);
  public double getDataPoint(int index);
  public void addChangeListener(ChangeListener listener);
  public void removeChangeListener(ChangeListener listener);
}

Any object that you want to monitor can generate periodic calls to the addDataPoint method and the view will be updated automatically. The DefaultMonitorModel manages data points in a ring buffer to maximize performance and sets a default buffer size of 500. If you plan to display a JMonitor with a broader view, you can set the model explicitly and use a larger value for the DefaultMonitorModel.

Figure 2 shows the classes we’ll need to make this work. The JMonitor class provides references to its model to two subviews that display the vertical graph and the horizontal chart, respectively. The JMonitorTest class creates a couple of JMonitor objects and associated them with a MonitorSimulator that periodically updates a data point that randomly moves either up or down by a small increment. All values are normalized between 0 and 1 and are stored as doubles.

Figure 2: JMonitor classes, including the
MonitorSimulator and JMonitorTest classes.

Figure 2: JMonitor classes, including the MonitorSimulator and JMonitorTest classes.

Because space is limited, we can’t look at every class directly, though you can download them all from www.javapro.com. We’ll take a closer look at MonitorChart, MonitorGraph and the DefaultMonitorModel classes, because they’re responsible for the more interesting part of this work.

The MonitorGraph class, in Listing 1, is responsible for painting the green bars you seen on the west side of the display. There are only two methods in this class. The paintComponent methods does most of the work, triggered by the stateChanged method call in JMonitor. JMonitor listens for ChangeListener events and calls repaint to update the view, forcing both the MonitorGraph and MonitorChart to call paintComponent.

The paintComponent method draws two lines for every three pixels vertically, either in light or dark green. The instance variables lite and dark are set when the object is constructed and could easily be exposed. I chose not to do so to keep the code smaller but there’s no reason not to extend this behavior. The lines are drawn from bottom to top, based on the current value (a double between 0 and 1) multiplied by the height. After the lines are drawn, we draw a vertical black line down the middle to create the two bar effect.

The setModel method simply updates a local variable whenever the JMonitor setModel is called. The constructor sets a preferred size to reflect 50 by 100 pixel size dimensions. The only instance variables are the two colors and a reference to the model.

Listing 2 shows the MonitorChart class, which graphs the values in a scrolling view. Like MonitorGraph, most of the work here is in the paintComponent method. We use a BufferedImage to store the background grid and reallocate the image if the component changes size or if the image is null (the initial condition). This helps minimize the cost of drawing to some extent. With the grid drawn, we then draw lines between each data point pair from left to right, starting at the appropriate offset.

There are only two instance variables, one for the grid image and the other to store a reference to the model. Like the MonitorChart, JMonitor uses the setModel method to set this reference whenever its own setModel method is called.

DefaultMonitorModel implements the MonitorModel interface we mentioned earlier. We manage instance variables to accommodate a listener list, an array of doubles that acts as a ring buffer and values for the maximum number of values, the current offset and the length or number of entries in the array. Our default constructor initializes the maximum value to 500, while the other constructor allows you to set this value explicitly.

The actual allocation is done by the setMaximumCount method, which stores the maximum value and allocations the array, setting the offset and length values to zero and firing a change event. The addDataPoiunt method adjusts some of these values, incrementing the offset after assigning the new value to the current offset. If the offset goes past the maximum value, we loop around to the first entry in the ring buffer (array). If the length is equal to or greater than the maximum number of entries, the buffer is full and each new value replaces the old one at the current (incrementing) position. Otherwise, we increment the offset counter by one and fire a chance event.

The listeners are managed by the ArrayList and a standard add and remove method is provided to register and unregister listeners. The actual event is fired by the fireChangeListener method, which first clones the list to avoid contention and then walks the listener list sending an event to each in turn through the stateChanged method.

The only other method we need to implement are getDataPoint and getDataPointCount. The first does a quick calculation to determine the proper offset at which the data resides in the ring buffer. If the length is at capacity, we add the requested index value to the current offset and normalize the value back down if it exceeds the maximum. otherwise we use the offset directly. The getDataPointCount method merely gets the length value which is a measure of how many entries are in the array.

JMonitor is a powerful visual component, primarily because of it’s usefulness in monitoring a wide variety of values in a real-time environments. By normalizing values (between 0.0 and 1.0), any range can be charted and the history of charted values remains persistent until the values scrolls off the left part of the display. This provides enough context to ensure administrators can recognize short-term trends in the data, potentially avoiding problematic or sometimes fatal conditions.

Listing 1

import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;

public class MonitorGraph extends JPanel
{
  protected MonitorModel model;
  protected Color dark = new Color(0, 96, 0);
  protected Color lite = Color.green;
  
  public MonitorGraph()
  {
    setPreferredSize(new Dimension(50, 100));
  }
  
  public void setModel(MonitorModel model)
  {
    this.model = model;
  }

  public void paintComponent(Graphics g)
  {
    int w = getSize().width;
    int h = getSize().height;
    g.setColor(Color.black);
    g.fillRect(0, 0, w, h);
    
    int count = model.getDataPointCount();
    double value = model.getDataPoint(count - 1);
    int height = (int)(h - (h * value));
    for (int i = h; i > 0; i -= 3)
    {
      g.setColor(i > height ? lite : dark);
      g.drawLine(0, i + 1, w, i + 1);
      g.drawLine(0, i + 2, w, i + 2);
    }
    
    g.setColor(Color.black);
    g.fillRect(w / 2 - 1, 0, 3, h);
  }
}

Listing 2

import java.awt.*;
import java.awt.image.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;

public class MonitorChart extends JPanel
{
  protected MonitorModel model;
  protected BufferedImage grid;
  
  public MonitorChart()
  {
    setPreferredSize(new Dimension(350, 100));
  }
  
  public void setModel(MonitorModel model)
  {
    this.model = model;
  }

  public void paintComponent(Graphics g)
  {
    int w = getSize().width;
    int h = getSize().height;
    double vert = ((double)h / 10.0);

    if (grid == null ||
      grid.getWidth() != w ||
      grid.getHeight() != h)
    {
      grid = new BufferedImage(w, h,
        BufferedImage.TYPE_INT_RGB);
      Graphics gg = grid.getGraphics();
      gg.setColor(new Color(0, 96, 0));
      for (double i = 0; i < h; i += vert)
      {
        gg.drawLine(0, (int)i, w, (int)i);
      }
      for (double i = 0; i < w; i += vert)
      {
        gg.drawLine((int)i, 0, (int)i, h);
      }
    }
    g.drawImage(grid, 0, 0, this);
    
    g.setColor(Color.green);
    int count = model.getDataPointCount();
    for (int i = 0; i < count - 1; i++)
    {
      int x = w - count + i;
      double one = model.getDataPoint(i);
      double two = model.getDataPoint(i + 1);
      int y1 = (int)(h - (h * one));
      int y2 = (int)(h - (h * two));
      g.drawLine(x, y1, x + 1, y2);
    }
  }
}

Listing 3

import java.util.*;
import javax.swing.event.*;

public class DefaultMonitorModel
  implements MonitorModel
{
  protected ArrayList listeners = new ArrayList();
  
  protected double[] ringBuffer;
  protected int maximum, offset, length;
  
  public DefaultMonitorModel()
  {
    this(500);
  }
  
  public DefaultMonitorModel(int maximum)
  {
    setMaximumCount(maximum);
  }
  
  public int getDataPointCount()
  {
    return length;
  }
  
  public void setMaximumCount(int maximum)
  {
    this.maximum = maximum;
    ringBuffer = new double[maximum];
    offset = 0;
    length = 0;
    fireChangeEvent();
  }
  
  public void addDataPoint(double value)
  {
    ringBuffer[offset] = value;
    offset++;
    if (offset >= maximum) offset = 0;
    if (length < maximum) length++;
    fireChangeEvent();
  }
  
  public double getDataPoint(int index)
  {
    if (length == maximum)
    {
      index = (offset + index) % maximum;
    }
    return ringBuffer[index];
  }
  
  public void addChangeListener(ChangeListener listener)
  {
    listeners.add(listener);
  }

  public void removeChangeListener(ChangeListener listener)
  {
    listeners.remove(listener);
  }
  
  protected void fireChangeEvent()
  {
    ArrayList copy = (ArrayList)listeners.clone();
    ChangeEvent event = new ChangeEvent(this);
    for (int i = 0; i < copy.size(); i++)
    {
      ChangeListener listener = (ChangeListener)copy.get(i);
      listener.stateChanged(event);
    }
  }
}