changeset 328:8dff701e098a

Clarify GC charts Part of the fix for PR960. Reviewed-by: vanaltj Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2012-May/001198.html
author Omair Majid <omajid@redhat.com>
date Wed, 23 May 2012 12:41:57 -0400
parents 65ffb89c766b
children e6b1697ab4d2
files client/core/src/main/java/com/redhat/thermostat/client/ui/SampledDataset.java client/core/src/main/java/com/redhat/thermostat/client/ui/VmGcController.java client/core/src/main/java/com/redhat/thermostat/client/ui/VmGcPanel.java client/core/src/main/java/com/redhat/thermostat/client/ui/VmGcView.java client/core/src/main/resources/com/redhat/thermostat/client/locale/strings.properties client/src/main/java/com/redhat/thermostat/client/ui/SampledDataset.java client/src/test/java/com/redhat/thermostat/client/ui/VmGcControllerTest.java common/src/main/java/com/redhat/thermostat/common/model/IntervalTimeData.java distribution/config/osgi-export.properties
diffstat 8 files changed, 517 insertions(+), 39 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/core/src/main/java/com/redhat/thermostat/client/ui/SampledDataset.java	Wed May 23 12:41:57 2012 -0400
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.ui;
+
+import org.jfree.data.xy.AbstractXYDataset;
+import org.jfree.data.xy.IntervalXYDataset;
+
+/**
+ * Represents a value sampled between two points in time.
+ */
+public class SampledDataset extends AbstractXYDataset implements IntervalXYDataset {
+
+    private static final String SERIES_KEY = "0";
+    private static final int DEFAULT_SIZE = 32;
+
+    private long[] xStart = new long[DEFAULT_SIZE];
+    private long[] xEnd = new long[DEFAULT_SIZE];
+    private double[] value = new double[DEFAULT_SIZE];
+
+    private int count = 0;
+
+    public void add(long startX, long endX, double value) {
+        if (xStart.length == count) {
+            long[] newXStart = new long[xStart.length * 2];
+            System.arraycopy(xStart, 0, newXStart, 0, xStart.length);
+            xStart = newXStart;
+            long[] newXEnd = new long[xEnd.length * 2];
+            System.arraycopy(xEnd, 0, newXEnd, 0, xEnd.length);
+            xEnd = newXEnd;
+            double[] newValue = new double[this.value.length * 2];
+            System.arraycopy(this.value, 0, newValue, 0, this.value.length);
+            this.value = newValue;
+        }
+        xStart[count] = startX;
+        xEnd[count] = endX;
+        this.value[count] = value;
+        count++;
+    }
+
+    public void clear() {
+        count = 0;
+        fireDatasetChanged();
+    }
+
+    @Override
+    public int getItemCount(int series) {
+        checkSeries(series);
+        return count;
+    }
+
+    @Override
+    public Number getX(int series, int item) {
+        checkSeries(series);
+        checkItemIndex(item);
+        return xStart[item];
+    }
+
+    @Override
+    public Number getY(int series, int item) {
+        checkSeries(series);
+        checkItemIndex(item);
+        return value[item];
+    }
+
+    @Override
+    public Number getStartX(int series, int item) {
+        checkSeries(series);
+        checkItemIndex(item);
+        return xStart[item];
+    }
+
+    @Override
+    public double getStartXValue(int series, int item) {
+        checkSeries(series);
+        checkItemIndex(item);
+        return xStart[item];
+    }
+
+    @Override
+    public Number getEndX(int series, int item) {
+        checkSeries(series);
+        checkItemIndex(item);
+        return xEnd[item];
+    }
+
+    @Override
+    public double getEndXValue(int series, int item) {
+        checkSeries(series);
+        checkItemIndex(item);
+        return xEnd[item];
+    }
+
+    @Override
+    public Number getStartY(int series, int item) {
+        checkSeries(series);
+        checkItemIndex(item);
+        return value[item];
+    }
+
+
+    @Override
+    public double getStartYValue(int series, int item) {
+        checkSeries(series);
+        checkItemIndex(item);
+        return value[item];
+    }
+
+    @Override
+    public Number getEndY(int series, int item) {
+        checkSeries(series);
+        checkItemIndex(item);
+        return value[item];
+    }
+
+    @Override
+    public double getEndYValue(int series, int item) {
+        checkSeries(series);
+        checkItemIndex(item);
+        return value[item];
+    }
+
+    @Override
+    public int getSeriesCount() {
+        return 1;
+    }
+
+    @Override
+    public Comparable<String> getSeriesKey(int series) {
+        checkSeries(series);
+        return SERIES_KEY;
+    }
+
+    @Override
+    public int indexOf(Comparable seriesKey) {
+        if (seriesKey == null) {
+            return -1;
+        }
+        if (seriesKey.compareTo(SERIES_KEY) == 0) {
+            return 0;
+        }
+        return -1;
+    }
+
+    private void checkSeries(int series) {
+        if (series != 0) {
+            throw new IllegalArgumentException(this.getClass().getName() + " supports only 1 series");
+        }
+    }
+
+    private void checkItemIndex(int index) {
+        if (index >= (count)) {
+            throw new IllegalArgumentException(this.getClass().getName() +  " supports only 1 series");
+        }
+    }
+
+    public void fireSeriesChanged() {
+        fireDatasetChanged();
+    }
+
+}
--- a/client/core/src/main/java/com/redhat/thermostat/client/ui/VmGcController.java	Wed May 23 12:03:47 2012 -0400
+++ b/client/core/src/main/java/com/redhat/thermostat/client/ui/VmGcController.java	Wed May 23 12:41:57 2012 -0400
@@ -40,10 +40,13 @@
 
 import java.awt.Component;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.concurrent.TimeUnit;
 
@@ -59,7 +62,7 @@
 import com.redhat.thermostat.common.dao.VmGcStatDAO;
 import com.redhat.thermostat.common.dao.VmMemoryStatDAO;
 import com.redhat.thermostat.common.dao.VmRef;
-import com.redhat.thermostat.common.model.DiscreteTimeData;
+import com.redhat.thermostat.common.model.IntervalTimeData;
 import com.redhat.thermostat.common.model.VmGcStat;
 import com.redhat.thermostat.common.model.VmMemoryStat;
 import com.redhat.thermostat.common.model.VmMemoryStat.Generation;
@@ -72,10 +75,19 @@
     private final VmGcStatDAO gcDao;
     private final VmMemoryStatDAO memDao;
 
-    private final Set<String> addedCollectors = new TreeSet<String>();
+    private final Set<String> addedCollectors = new TreeSet<>();
+    // the last value seen for each collector
+    private final Map<String, VmGcStat> lastValueSeen = new TreeMap<>();
 
     private final Timer timer;
 
+    private final Comparator<VmGcStat> vmGcStatComparator = new Comparator<VmGcStat>() {
+        @Override
+        public int compare(VmGcStat o1, VmGcStat o2) {
+            return Long.compare(o1.getTimeStamp(), o2.getTimeStamp());
+        }
+    };
+
     public VmGcController(VmRef ref) {
         this.ref = ref;
         this.view = ApplicationContext.getInstance().getViewFactory().getView(VmGcView.class);
@@ -104,7 +116,12 @@
         timer.setAction(new Runnable() {
             @Override
             public void run() {
-                doUpdateCollectorData();
+                try {
+                    doUpdateCollectorData();
+                } catch (Throwable t) {
+                    t.printStackTrace();
+                    throw t;
+                }
             }
         });
         timer.setSchedulingType(SchedulingType.FIXED_RATE);
@@ -128,21 +145,38 @@
     }
 
     private void doUpdateCollectorData() {
-        Map<String, List<DiscreteTimeData<? extends Number>>> dataToAdd = new HashMap<>();
-        for (VmGcStat stat : gcDao.getLatestVmGcStats(ref)) {
-            double walltime = 1.0E-6 * stat.getWallTime();
+        Map<String, List<IntervalTimeData<Double>>> dataToAdd = new HashMap<>();
+        List<VmGcStat> sortedList = gcDao.getLatestVmGcStats(ref);
+        Collections.sort(sortedList, vmGcStatComparator);
+
+        for (VmGcStat stat : sortedList) {
             String collector = stat.getCollectorName();
-            List<DiscreteTimeData<? extends Number>> data = dataToAdd.get(collector);
+            List<IntervalTimeData<Double>> data = dataToAdd.get(collector);
             if (data == null) {
-                data = new ArrayList<DiscreteTimeData<? extends Number>>();
+                data = new ArrayList<>();
                 dataToAdd.put(collector, data);
             }
-            data.add(new DiscreteTimeData<Double>(stat.getTimeStamp(), walltime));
+            if (lastValueSeen.containsKey(collector)) {
+                if (stat.getTimeStamp() <= lastValueSeen.get(collector).getTimeStamp()) {
+                    System.out.println("new gc collector value is older than previous value");
+                }
+                VmGcStat last = lastValueSeen.get(collector);
+                long diffInMicro = (stat.getWallTime() - last.getWallTime());
+                double diffInMillis = diffInMicro / 1000.0;
+                // TODO there is not much point in adding data when diff is 0,
+                // but we need to make the chart scroll automatically based on
+                // the current time when we do that
+                //  if (diff != 0) {
+                data.add(new IntervalTimeData<>(last.getTimeStamp(), stat.getTimeStamp(), diffInMillis));
+                // }
+            }
+            lastValueSeen.put(collector, stat);
+
         }
-        for (Map.Entry<String, List<DiscreteTimeData<? extends Number>>> entry : dataToAdd.entrySet()) {
+        for (Map.Entry<String, List<IntervalTimeData<Double>>> entry : dataToAdd.entrySet()) {
             String name = entry.getKey();
             if (!addedCollectors.contains(name)) {
-                view.addChart(name, chartName(name, getCollectorGeneration(name)));
+                view.addChart(name, chartName(name, getCollectorGeneration(name)), "ms");
                 addedCollectors.add(name);
             }
             view.addData(entry.getKey(), entry.getValue());
@@ -161,7 +195,7 @@
     }
 
     /**
-     * @return
+     * @return the {@link Component} representing the actual view of this controller
      */
     public Component getComponent() {
         return view.getUiComponent();
--- a/client/core/src/main/java/com/redhat/thermostat/client/ui/VmGcPanel.java	Wed May 23 12:03:47 2012 -0400
+++ b/client/core/src/main/java/com/redhat/thermostat/client/ui/VmGcPanel.java	Wed May 23 12:41:57 2012 -0400
@@ -52,14 +52,18 @@
 
 import org.jfree.chart.ChartFactory;
 import org.jfree.chart.JFreeChart;
-import org.jfree.data.time.FixedMillisecond;
-import org.jfree.data.time.TimeSeries;
-import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.chart.axis.DateAxis;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.renderer.xy.StandardXYBarPainter;
+import org.jfree.chart.renderer.xy.XYBarRenderer;
+import org.jfree.data.RangeType;
+import org.jfree.data.xy.IntervalXYDataset;
 
 import com.redhat.thermostat.client.locale.LocaleResources;
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.ActionNotifier;
-import com.redhat.thermostat.common.model.DiscreteTimeData;
+import com.redhat.thermostat.common.model.IntervalTimeData;
 
 public class VmGcPanel extends JPanel implements VmGcView {
 
@@ -67,7 +71,7 @@
 
     private final ActionNotifier<Action> notifier = new ActionNotifier<>(this);
 
-    private final Map<String, TimeSeriesCollection> dataset = new HashMap<>();
+    private final Map<String, SampledDataset> dataset = new HashMap<>();
     private final Map<String, JPanel> subPanels = new HashMap<>();
 
     private final GridBagConstraints gcPanelConstraints;
@@ -114,36 +118,46 @@
         setLayout(new GridBagLayout());
     }
 
-    private JPanel createCollectorDetailsPanel(TimeSeriesCollection timeSeriesCollection, String title) {
+    private JPanel createCollectorDetailsPanel(IntervalXYDataset timeSeriesCollection, String title, String units) {
         JPanel detailsPanel = new JPanel();
         detailsPanel.setBorder(Components.smallBorder());
         detailsPanel.setLayout(new BorderLayout());
 
         detailsPanel.add(Components.header(title), BorderLayout.NORTH);
 
-        JFreeChart chart = ChartFactory.createTimeSeriesChart(
-                null,
-                localize(LocaleResources.VM_GC_COLLECTOR_CHART_REAL_TIME_LABEL),
-                localize(LocaleResources.VM_GC_COLLECTOR_CHART_GC_TIME_LABEL),
-                timeSeriesCollection,
-                false, false, false);
+        JFreeChart chart = ChartFactory.createHistogram(
+            null,
+            localize(LocaleResources.VM_GC_COLLECTOR_CHART_REAL_TIME_LABEL),
+            localize(LocaleResources.VM_GC_COLLECTOR_CHART_GC_TIME_LABEL, units),
+            timeSeriesCollection,
+            PlotOrientation.VERTICAL,
+            false,
+            false,
+            false);
 
+        ((XYBarRenderer)(chart.getXYPlot().getRenderer())).setBarPainter(new StandardXYBarPainter());
+        chart.getXYPlot().setDomainAxis(new DateAxis());
         JPanel chartPanel = new RecentTimeSeriesChartPanel(new RecentTimeSeriesChartController(chart));
 
+        NumberAxis axis = (NumberAxis) chart.getXYPlot().getRangeAxis();
+
+        axis.setRangeType(RangeType.POSITIVE);
+        axis.setAutoRange(true);
+        axis.setAutoRangeMinimumSize(1);
+
         detailsPanel.add(chartPanel, BorderLayout.CENTER);
 
         return detailsPanel;
     }
 
     @Override
-    public void addChart(final String tag, final String title) {
+    public void addChart(final String tag, final String title, final String units) {
         SwingUtilities.invokeLater(new Runnable() {
             @Override
             public void run() {
-                TimeSeries timeSeries = new TimeSeries(tag);
-                TimeSeriesCollection timeSeriesCollection = new TimeSeriesCollection(timeSeries);
-                dataset.put(tag, timeSeriesCollection);
-                JPanel subPanel = createCollectorDetailsPanel(timeSeriesCollection, title);
+                SampledDataset newData = new SampledDataset();
+                dataset.put(tag, newData);
+                JPanel subPanel = createCollectorDetailsPanel(newData, title, units);
                 subPanels.put(tag, subPanel);
                 add(subPanel, gcPanelConstraints);
                 gcPanelConstraints.gridy++;
@@ -167,14 +181,14 @@
     }
 
     @Override
-    public void addData(final String tag, List<DiscreteTimeData<? extends Number>> data) {
-        final List<DiscreteTimeData<? extends Number>> copy = new ArrayList<>(data);
+    public void addData(final String tag, List<IntervalTimeData<Double>> data) {
+        final List<IntervalTimeData<Double>> copy = new ArrayList<>(data);
         SwingUtilities.invokeLater(new Runnable() {
             @Override
             public void run() {
-                TimeSeries series = dataset.get(tag).getSeries(tag);
-                for (DiscreteTimeData<? extends Number> timeData: copy) {
-                    series.add(new FixedMillisecond(timeData.getTimeInMillis()), timeData.getData(), false);
+                SampledDataset series = dataset.get(tag);
+                for (IntervalTimeData<Double> timeData: copy) {
+                    series.add(timeData.getStartTimeInMillis(), timeData.getEndTimeInMillis(), timeData.getData());
                 }
                 series.fireSeriesChanged();
             }
@@ -186,7 +200,7 @@
         SwingUtilities.invokeLater(new Runnable() {
             @Override
             public void run() {
-                TimeSeries series = dataset.get(tag).getSeries(tag);
+                SampledDataset series = dataset.get(tag);
                 series.clear();
             }
         });
--- a/client/core/src/main/java/com/redhat/thermostat/client/ui/VmGcView.java	Wed May 23 12:03:47 2012 -0400
+++ b/client/core/src/main/java/com/redhat/thermostat/client/ui/VmGcView.java	Wed May 23 12:41:57 2012 -0400
@@ -41,7 +41,7 @@
 
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.View;
-import com.redhat.thermostat.common.model.DiscreteTimeData;
+import com.redhat.thermostat.common.model.IntervalTimeData;
 
 public interface VmGcView extends View {
 
@@ -54,11 +54,11 @@
 
     void removeActionListener(ActionListener<Action> listener);
 
-    void addChart(String tag, String title);
+    void addChart(String tag, String title, String valueUnit);
 
     void removeChart(String tag);
 
-    void addData(String tag, List<DiscreteTimeData<? extends Number>> data);
+    void addData(String tag, List<IntervalTimeData<Double>> data);
 
     void clearData(String tag);
 
--- a/client/core/src/main/resources/com/redhat/thermostat/client/locale/strings.properties	Wed May 23 12:03:47 2012 -0400
+++ b/client/core/src/main/resources/com/redhat/thermostat/client/locale/strings.properties	Wed May 23 12:41:57 2012 -0400
@@ -132,7 +132,7 @@
 
 VM_GC_COLLECTOR_OVER_GENERATION = Collector {0} running on {1}
 VM_GC_COLLECTOR_CHART_REAL_TIME_LABEL = Time
-VM_GC_COLLECTOR_CHART_GC_TIME_LABEL = Total Time Spent on GC (s)
+VM_GC_COLLECTOR_CHART_GC_TIME_LABEL = GC Time ({0})
 
 VM_LOADED_CLASSES = Loaded Classes
 VM_CLASSES_CHART_REAL_TIME_LABEL = Time
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/src/test/java/com/redhat/thermostat/client/ui/VmGcControllerTest.java	Wed May 23 12:41:57 2012 -0400
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.client.ui;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Matchers.isNotNull;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import com.redhat.thermostat.common.ActionEvent;
+import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.common.Timer;
+import com.redhat.thermostat.common.Timer.SchedulingType;
+import com.redhat.thermostat.common.TimerFactory;
+import com.redhat.thermostat.common.ViewFactory;
+import com.redhat.thermostat.common.appctx.ApplicationContext;
+import com.redhat.thermostat.common.appctx.ApplicationContextUtil;
+import com.redhat.thermostat.common.dao.DAOFactory;
+import com.redhat.thermostat.common.dao.VmGcStatDAO;
+import com.redhat.thermostat.common.dao.VmMemoryStatDAO;
+import com.redhat.thermostat.common.dao.VmRef;
+import com.redhat.thermostat.common.model.VmGcStat;
+import com.redhat.thermostat.common.model.VmMemoryStat;
+import com.redhat.thermostat.common.model.VmMemoryStat.Generation;
+
+public class VmGcControllerTest {
+
+    private Timer timer;
+    private Runnable timerAction;
+    private VmGcView view;
+    private ActionListener<VmGcView.Action> viewListener;
+
+    @Before
+    public void setUp() {
+        ApplicationContextUtil.resetApplicationContext();
+
+        // Setup Timer
+        timer = mock(Timer.class);
+        ArgumentCaptor<Runnable> timerActionCaptor = ArgumentCaptor.forClass(Runnable.class);
+        doNothing().when(timer).setAction(timerActionCaptor.capture());
+
+        TimerFactory timerFactory = mock(TimerFactory.class);
+        when(timerFactory.createTimer()).thenReturn(timer);
+        ApplicationContext.getInstance().setTimerFactory(timerFactory);
+
+        // Set up fake data
+        List<VmGcStat> stats = new ArrayList<>();
+        VmGcStat stat1 = new VmGcStat(42, 1, "collector1", 1, 10);
+        VmGcStat stat2 = new VmGcStat(42, 2, "collector1", 5, 20);
+        stats.add(stat1);
+        stats.add(stat2);
+
+        Generation gen;
+        List<Generation> gens = new ArrayList<>();
+        gen = new Generation();
+        gen.name = "generation 1";
+        gen.collector = "collector1";
+        gens.add(gen);
+        VmMemoryStat memoryStat = new VmMemoryStat(1, 42, gens);
+
+        // Setup DAO
+        VmGcStatDAO vmGcStatDAO = mock(VmGcStatDAO.class);
+        when(vmGcStatDAO.getLatestVmGcStats(isA(VmRef.class))).thenReturn(stats);
+        VmMemoryStatDAO vmMemoryStatDAO = mock(VmMemoryStatDAO.class);
+        when(vmMemoryStatDAO.getLatestMemoryStat(isA(VmRef.class))).thenReturn(memoryStat);
+
+        DAOFactory daoFactory = mock(DAOFactory.class);
+        when(daoFactory.getVmGcStatDAO()).thenReturn(vmGcStatDAO);
+        when(daoFactory.getVmMemoryStatDAO()).thenReturn(vmMemoryStatDAO);
+        ApplicationContext.getInstance().setDAOFactory(daoFactory);
+
+        // Setup View
+        view = mock(VmGcView.class);
+        ArgumentCaptor<ActionListener> viewArgumentCaptor = ArgumentCaptor.forClass(ActionListener.class);
+        doNothing().when(view).addActionListener(viewArgumentCaptor.capture());
+
+        ViewFactory viewFactory = mock(ViewFactory.class);
+        when(viewFactory.getView(eq(VmGcView.class))).thenReturn(view);
+
+        ApplicationContext.getInstance().setViewFactory(viewFactory);
+
+        // Now start the controller
+        VmRef ref = mock(VmRef.class);
+
+        new VmGcController(ref);
+
+        // Extract relevant objects
+        viewListener = viewArgumentCaptor.getValue();
+        timerAction = timerActionCaptor.getValue();
+    }
+
+    @After
+    public void tearDown() {
+        ApplicationContextUtil.resetApplicationContext();
+    }
+
+    @Test
+    public void verifyTimer() {
+        verify(timer).setAction(isNotNull(Runnable.class));
+        verify(timer).setAction(isA(Runnable.class));
+        verify(timer).setDelay(5);
+        verify(timer).setInitialDelay(0);
+        verify(timer).setTimeUnit(TimeUnit.SECONDS);
+        verify(timer).setSchedulingType(SchedulingType.FIXED_RATE);
+
+    }
+
+    @Test
+    public void verifyStartAndStop() {
+        viewListener.actionPerformed(new ActionEvent<>(view, VmGcView.Action.VISIBLE));
+
+        verify(timer).start();
+
+        viewListener.actionPerformed(new ActionEvent<>(view, VmGcView.Action.HIDDEN));
+
+        verify(timer).stop();
+    }
+
+    @Test
+    public void verifyAction() {
+        timerAction.run();
+
+        verify(view).addData(isA(String.class), isA(List.class));
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/common/src/main/java/com/redhat/thermostat/common/model/IntervalTimeData.java	Wed May 23 12:41:57 2012 -0400
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2012 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.common.model;
+
+public class IntervalTimeData<T> {
+
+    private long start;
+    private long end;
+    private T data;
+
+    public IntervalTimeData(long start, long end, T data) {
+        this.start = start;
+        this.end = end;
+        this.data = data;
+    }
+
+    public long getStartTimeInMillis() {
+        return start;
+    }
+
+    public long getEndTimeInMillis() {
+        return end;
+    }
+
+    public T getData() {
+        return data;
+    }
+
+}
--- a/distribution/config/osgi-export.properties	Wed May 23 12:03:47 2012 -0400
+++ b/distribution/config/osgi-export.properties	Wed May 23 12:41:57 2012 -0400
@@ -41,6 +41,7 @@
 org.jfree.chart.labels
 org.jfree.chart.plot
 org.jfree.chart.renderer.xy
+org.jfree.data
 org.jfree.data.time
 org.jfree.data.xy