changeset 1526:1c6b65679463

The vm-cpu tab should fetch less data Reviewed-by: jerboaa, jkang Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2014-October/011142.html PR2007
author Omair Majid <omajid@redhat.com>
date Mon, 20 Oct 2014 14:16:54 -0400
parents 84fbf4af48e3
children de5f72a3381a
files vm-cpu/client-core/src/main/java/com/redhat/thermostat/vm/cpu/client/core/VmCpuView.java vm-cpu/client-core/src/main/java/com/redhat/thermostat/vm/cpu/client/core/internal/TimeRangeComputer.java vm-cpu/client-core/src/main/java/com/redhat/thermostat/vm/cpu/client/core/internal/VmCpuController.java vm-cpu/client-core/src/test/java/com/redhat/thermostat/vm/cpu/client/core/internal/TimeRangeComputerTest.java vm-cpu/client-core/src/test/java/com/redhat/thermostat/vm/cpu/client/core/internal/VmCpuControllerTest.java vm-cpu/client-swing/src/main/java/com/redhat/thermostat/vm/cpu/client/swing/internal/VmCpuChartPanel.java vm-cpu/client-swing/src/main/java/com/redhat/thermostat/vm/cpu/client/swing/internal/VmCpuPanel.java vm-cpu/common/src/main/java/com/redhat/thermostat/vm/cpu/common/VmCpuStatDAO.java vm-cpu/common/src/main/java/com/redhat/thermostat/vm/cpu/common/internal/VmCpuStatDAOImpl.java vm-cpu/common/src/main/java/com/redhat/thermostat/vm/cpu/common/internal/VmCpuStatDAOImplStatementDescriptorRegistration.java vm-cpu/common/src/test/java/com/redhat/thermostat/vm/cpu/common/internal/VmCpuStatDAOImplStatementDescriptorRegistrationTest.java
diffstat 11 files changed, 712 insertions(+), 33 deletions(-) [+]
line wrap: on
line diff
--- a/vm-cpu/client-core/src/main/java/com/redhat/thermostat/vm/cpu/client/core/VmCpuView.java	Mon Oct 20 15:50:43 2014 -0400
+++ b/vm-cpu/client-core/src/main/java/com/redhat/thermostat/vm/cpu/client/core/VmCpuView.java	Mon Oct 20 14:16:54 2014 -0400
@@ -37,13 +37,46 @@
 package com.redhat.thermostat.vm.cpu.client.core;
 
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import com.redhat.thermostat.client.core.views.BasicView;
 import com.redhat.thermostat.client.core.views.UIComponent;
+import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.common.model.Range;
 import com.redhat.thermostat.storage.model.DiscreteTimeData;
+import com.redhat.thermostat.vm.cpu.client.core.VmCpuView.Duration;
 
 public abstract class VmCpuView extends BasicView implements UIComponent {
 
+    public static class Duration {
+        public final int value;
+        public final TimeUnit unit;
+
+        public Duration(int value, TimeUnit unit) {
+            this.value = value;
+            this.unit = unit;
+        }
+
+        @Override
+        public String toString() {
+            return value + " " + unit;
+        }
+    }
+
+    public enum UserAction {
+        USER_CHANGED_TIME_RANGE,
+    }
+
+    public abstract void addUserActionListener(ActionListener<UserAction> listener);
+
+    public abstract void removeUserActionListener(ActionListener<UserAction> listener);
+
+    public abstract Duration getUserDesiredDuration();
+
+    public abstract void setVisibleDataRange(int time, TimeUnit unit);
+
+    public abstract void setAvailableDataRange(Range<Long> availableInterval);
+
     public abstract void addData(List<DiscreteTimeData<? extends Number>> data);
 
     public abstract void clearData();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-cpu/client-core/src/main/java/com/redhat/thermostat/vm/cpu/client/core/internal/TimeRangeComputer.java	Mon Oct 20 14:16:54 2014 -0400
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2012-2014 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.vm.cpu.client.core.internal;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import com.redhat.thermostat.common.model.Range;
+
+/**
+ * Compute additional data ranges to fetch when trying to update a currently
+ * displayed time-data with additional time-data.
+ */
+public class TimeRangeComputer {
+
+    public static List<Range<Long>> computeAdditionalRangesToFetch(Range<Long> availableRange, Range<Long> desiredRange, Range<Long> displayedRange) {
+
+        // TODO find out how to show 'sampled' values across a
+        // very, very large desiredRange
+
+        // Do we need to show additional data?
+        if (displayedRange.equals(desiredRange)) {
+            return Collections.emptyList();
+        }
+
+        // Do we already have all the data?
+        if (contains(displayedRange, desiredRange)) {
+            // TODO 'narrow' view to the desired range
+            return Collections.emptyList();
+        }
+
+        // What's the best that we can do with the desired range?
+        Range<Long> closestInterval = closestInterval(availableRange, desiredRange);
+
+        List<Range<Long>> results = new ArrayList<>();
+        if (closestInterval.getMin() < displayedRange.getMin()) {
+            if (closestInterval.getMax() < displayedRange.getMin()) {
+                results.add(closestInterval);
+            } else if (closestInterval.getMax() < displayedRange.getMax()) {
+                results.add(new Range<>(closestInterval.getMin(), displayedRange.getMin()));
+            } else {
+                results.add(new Range<>(closestInterval.getMin(), displayedRange.getMin()));
+                results.add(new Range<>(displayedRange.getMax(), closestInterval.getMax()));
+            }
+        } else if (closestInterval.getMin() < displayedRange.getMax()) {
+            if (closestInterval.getMax() < displayedRange.getMax()) {
+                // nothing to do here
+            } else if (closestInterval.getMax() > displayedRange.getMax()) {
+                results.add(new Range<>(displayedRange.getMax(), closestInterval.getMax()));
+            }
+        } else {
+            results.add(closestInterval);
+        }
+        return results;
+    }
+
+    private static boolean contains(Range<Long> larger, Range<Long> smaller) {
+        return (larger.getMin() <= smaller.getMin()) && (larger.getMax() >= smaller.getMax());
+    }
+
+    private static Range<Long> closestInterval(Range<Long> avilable, Range<Long> desired) {
+        long min = Math.max(avilable.getMin(), desired.getMin());
+        long max = Math.min(avilable.getMax(), desired.getMax());
+        return new Range<>(min, max);
+    }
+
+}
--- a/vm-cpu/client-core/src/main/java/com/redhat/thermostat/vm/cpu/client/core/internal/VmCpuController.java	Mon Oct 20 15:50:43 2014 -0400
+++ b/vm-cpu/client-core/src/main/java/com/redhat/thermostat/vm/cpu/client/core/internal/VmCpuController.java	Mon Oct 20 14:16:54 2014 -0400
@@ -37,6 +37,7 @@
 package com.redhat.thermostat.vm.cpu.client.core.internal;
 
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
@@ -49,17 +50,21 @@
 import com.redhat.thermostat.common.NotImplementedException;
 import com.redhat.thermostat.common.Timer;
 import com.redhat.thermostat.common.Timer.SchedulingType;
+import com.redhat.thermostat.common.model.Range;
 import com.redhat.thermostat.shared.locale.LocalizedString;
 import com.redhat.thermostat.shared.locale.Translate;
 import com.redhat.thermostat.storage.core.VmRef;
 import com.redhat.thermostat.storage.model.DiscreteTimeData;
 import com.redhat.thermostat.vm.cpu.client.core.VmCpuView;
+import com.redhat.thermostat.vm.cpu.client.core.VmCpuView.Duration;
 import com.redhat.thermostat.vm.cpu.client.core.VmCpuViewProvider;
+import com.redhat.thermostat.vm.cpu.client.core.VmCpuView.UserAction;
 import com.redhat.thermostat.vm.cpu.client.locale.LocaleResources;
 import com.redhat.thermostat.vm.cpu.common.VmCpuStatDAO;
 import com.redhat.thermostat.vm.cpu.common.model.VmCpuStat;
 
 public class VmCpuController implements InformationServiceController<VmRef> {
+
     private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
 
     private final VmRef ref;
@@ -68,7 +73,10 @@
 
     private final Timer timer;
 
-    private long lastSeenTimeStamp = Long.MIN_VALUE;
+    private Range<Long> availableRange = new Range<Long>(Long.MAX_VALUE, Long.MIN_VALUE);
+    private Range<Long> displayedRange = new Range<Long>(Long.MAX_VALUE, Long.MIN_VALUE);
+
+    private Duration userDesiredDuration;
 
     public VmCpuController(ApplicationService appSvc, VmCpuStatDAO vmCpuStatDao, VmRef ref, VmCpuViewProvider provider) {
         this.ref = ref;
@@ -78,7 +86,7 @@
         timer.setAction(new Runnable() {
             @Override
             public void run() {
-                doUpdateVmCpuCharts();
+                updateData();
             }
         });
         timer.setTimeUnit(TimeUnit.SECONDS);
@@ -103,20 +111,76 @@
                 }
             }
         });
+
+        view.addUserActionListener(new ActionListener<VmCpuView.UserAction>() {
+
+            @Override
+            public void actionPerformed(ActionEvent<UserAction> actionEvent) {
+                switch (actionEvent.getActionId()) {
+                case USER_CHANGED_TIME_RANGE:
+                    Duration duration = (Duration) view.getUserDesiredDuration();
+                    userDesiredDuration = duration;
+                    view.setVisibleDataRange(duration.value, duration.unit);
+                    break;
+                default:
+                    throw new AssertionError("Unhandled action type");
+                }
+            }
+        });
+
+        userDesiredDuration = view.getUserDesiredDuration();
     }
 
     private void start() {
         timer.start();
     }
 
-    private void doUpdateVmCpuCharts() {
-        List<VmCpuStat> stats = dao.getLatestVmCpuStats(ref, lastSeenTimeStamp);
+    private void updateData() {
+        long now = System.currentTimeMillis();
+        long userVisibleTimeDelta = (userDesiredDuration.unit.toMillis(userDesiredDuration.value));
+        Range<Long> desiredRange = new Range<Long>(now - userVisibleTimeDelta, now);
+
+        VmCpuStat oldest = dao.getOldest(ref);
+        VmCpuStat latest = dao.getLatest(ref);
+
+        Range<Long> newAvailableRange = new Range<>(oldest.getTimeStamp(), latest.getTimeStamp());
+
+        if (availableRange.equals(newAvailableRange)) {
+            return;
+        }
+
+        availableRange = newAvailableRange;
+
+        view.setAvailableDataRange(availableRange);
+
+        List<Range<Long>> additionalIntervals = TimeRangeComputer.computeAdditionalRangesToFetch(availableRange, desiredRange, displayedRange);
+
+        long displayedMin = Math.min(displayedRange.getMin(), Long.MAX_VALUE);
+        long displayedMax = Math.max(displayedRange.getMax(), Long.MIN_VALUE);
+
+        for (Range<Long> interval : additionalIntervals) {
+            List<VmCpuStat> stats = dao.getVmCpuStats(ref, interval.getMin(), interval.getMax());
+            appendToVmCpuCharts(stats);
+
+            displayedMin = Math.min(displayedMin, interval.getMin());
+            displayedMax = Math.max(displayedMax, interval.getMax());
+        }
+
+        // the view does not show data older than the delta
+        displayedMin = Math.max(displayedMin, displayedMax - userVisibleTimeDelta);
+
+        // System.out.println("Wanted to display " + new Date(desiredRange.getMin()) + " to " + new Date(desiredRange.getMax()));
+        displayedRange = new Range<>(displayedMin, displayedMax);
+        // System.out.println("Displayed from " + new Date(displayedRange.getMin()) + " to " + new Date(displayedRange.getMax()));
+
+    }
+
+    private void appendToVmCpuCharts(List<VmCpuStat> stats) {
         List<DiscreteTimeData<? extends Number>> toDisplay = new ArrayList<>(stats.size());
         for (VmCpuStat stat: stats) {
             DiscreteTimeData<? extends Number> data =
                     new DiscreteTimeData<Number>(stat.getTimeStamp(), stat.getCpuLoad());
             toDisplay.add(data);
-            lastSeenTimeStamp = Math.max(lastSeenTimeStamp, stat.getTimeStamp());
         }
 
         view.addData(toDisplay);
@@ -126,6 +190,7 @@
         timer.stop();
     }
 
+    @Override
     public UIComponent getView() {
         return (UIComponent) view;
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-cpu/client-core/src/test/java/com/redhat/thermostat/vm/cpu/client/core/internal/TimeRangeComputerTest.java	Mon Oct 20 14:16:54 2014 -0400
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2012-2014 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.vm.cpu.client.core.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.common.model.Range;
+
+public class TimeRangeComputerTest {
+
+    @Test
+    public void verifyComputesEmptyResultIfAllDataIsAvaialable() {
+        Range<Long> available = new Range<>(1l, 10l);
+        Range<Long> desired = new Range<>(2l, 3l);
+        Range<Long> displayed = new Range<>(1l, 10l);
+
+        List<Range<Long>> result = TimeRangeComputer.computeAdditionalRangesToFetch(available, desired, displayed);
+
+        assertTrue(result.isEmpty());
+    }
+
+    @Test
+    public void verifyComputesRangeBeforeCorrectly() {
+        Range<Long> available = new Range<>(1l, 10l);
+        Range<Long> desired = new Range<>(1l, 7l);
+        Range<Long> displayed = new Range<>(5l, 10l);
+
+        List<Range<Long>> result = TimeRangeComputer.computeAdditionalRangesToFetch(available, desired, displayed);
+
+        assertEquals(1, result.size());
+        assertEquals(new Range<>(1l,5l), result.get(0));
+    }
+
+    @Test
+    public void verifyComputesRangeAfterCorrectly() {
+        Range<Long> available = new Range<>(1l, 10l);
+        Range<Long> desired = new Range<>(2l, 6l);
+        Range<Long> displayed = new Range<>(1l, 5l);
+
+        List<Range<Long>> result = TimeRangeComputer.computeAdditionalRangesToFetch(available, desired, displayed);
+
+        assertEquals(1, result.size());
+        assertEquals(new Range<>(5l,6l), result.get(0));
+    }
+
+    @Test
+    public void verifyComputesOverlappingRangeCorrectly() {
+        Range<Long> available = new Range<>(1l, 10l);
+        Range<Long> desired = new Range<>(2l, 8l);
+        Range<Long> displayed = new Range<>(4l, 6l);
+
+        List<Range<Long>> result = TimeRangeComputer.computeAdditionalRangesToFetch(available, desired, displayed);
+
+        assertEquals(2, result.size());
+        assertEquals(new Range<>(2l,4l), result.get(0));
+        assertEquals(new Range<>(6l, 8l), result.get(1));
+    }
+
+}
--- a/vm-cpu/client-core/src/test/java/com/redhat/thermostat/vm/cpu/client/core/internal/VmCpuControllerTest.java	Mon Oct 20 15:50:43 2014 -0400
+++ b/vm-cpu/client-core/src/test/java/com/redhat/thermostat/vm/cpu/client/core/internal/VmCpuControllerTest.java	Mon Oct 20 14:16:54 2014 -0400
@@ -37,7 +37,6 @@
 package com.redhat.thermostat.vm.cpu.client.core.internal;
 
 import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -45,6 +44,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
@@ -56,26 +56,26 @@
 import com.redhat.thermostat.common.TimerFactory;
 import com.redhat.thermostat.storage.core.VmRef;
 import com.redhat.thermostat.vm.cpu.client.core.VmCpuView;
+import com.redhat.thermostat.vm.cpu.client.core.VmCpuView.Duration;
 import com.redhat.thermostat.vm.cpu.client.core.VmCpuViewProvider;
-import com.redhat.thermostat.vm.cpu.client.core.internal.VmCpuController;
 import com.redhat.thermostat.vm.cpu.common.VmCpuStatDAO;
 import com.redhat.thermostat.vm.cpu.common.model.VmCpuStat;
 
-
 public class VmCpuControllerTest {
 
     @SuppressWarnings({ "unchecked", "rawtypes" }) // any(List.class)
     @Test
     public void testChartUpdate() {
+        VmRef ref = mock(VmRef.class);
 
         VmCpuStat stat1 = new VmCpuStat("foo-agent", 123, "vmId", 50.5);
         List<VmCpuStat> stats = new ArrayList<VmCpuStat>();
         stats.add(stat1);
 
         VmCpuStatDAO vmCpuStatDAO = mock(VmCpuStatDAO.class);
-        when(vmCpuStatDAO.getLatestVmCpuStats(any(VmRef.class), eq(Long.MIN_VALUE))).thenReturn(stats).thenReturn(new ArrayList<VmCpuStat>());
-
-        VmRef ref = mock(VmRef.class);
+        when(vmCpuStatDAO.getLatestVmCpuStats(any(VmRef.class), any(Long.class))).thenThrow(new AssertionError("Unbounded queries are bad!"));
+        when(vmCpuStatDAO.getOldest(ref)).thenReturn(stat1);
+        when(vmCpuStatDAO.getLatest(ref)).thenReturn(stat1);
 
         Timer timer = mock(Timer.class);
         ArgumentCaptor<Runnable> timerActionCaptor = ArgumentCaptor.forClass(Runnable.class);
@@ -89,6 +89,8 @@
         final VmCpuView view = mock(VmCpuView.class);
         ArgumentCaptor<ActionListener> viewArgumentCaptor = ArgumentCaptor.forClass(ActionListener.class);
         doNothing().when(view).addActionListener(viewArgumentCaptor.capture());
+
+        when(view.getUserDesiredDuration()).thenReturn(new Duration(1, TimeUnit.MINUTES));
         
         VmCpuViewProvider viewProvider = mock(VmCpuViewProvider.class);
         when(viewProvider.createView()).thenReturn(view);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-cpu/client-swing/src/main/java/com/redhat/thermostat/vm/cpu/client/swing/internal/VmCpuChartPanel.java	Mon Oct 20 14:16:54 2014 -0400
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2012-2014 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.vm.cpu.client.swing.internal;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.concurrent.TimeUnit;
+
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.SwingUtilities;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Document;
+import javax.swing.text.JTextComponent;
+
+import org.jfree.chart.ChartPanel;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.plot.XYPlot;
+
+import com.redhat.thermostat.client.locale.LocaleResources;
+import com.redhat.thermostat.client.swing.components.ValueField;
+import com.redhat.thermostat.shared.locale.Translate;
+import com.redhat.thermostat.vm.cpu.client.core.VmCpuView;
+import com.redhat.thermostat.vm.cpu.client.core.VmCpuView.Duration;
+
+public class VmCpuChartPanel extends JPanel {
+
+    static final TimeUnit[] DEFAULT_TIMEUNITS = new TimeUnit[] { TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MINUTES };
+
+    public static final String PROPERTY_VISIBLE_TIME_RANGE = "visibleTimeRange";
+
+    private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
+
+    private static final long serialVersionUID = -1733906800911900456L;
+    private static final int MINIMUM_DRAW_SIZE = 100;
+
+    private ChartPanel chartPanel;
+
+    private JPanel labelContainer;
+    private JTextComponent label;
+
+    public VmCpuChartPanel(JFreeChart chart, Duration duration) {
+
+        this.setLayout(new BorderLayout());
+
+        // instead of just disabling display of tooltips, disable their generation too
+        if (chart.getPlot() instanceof XYPlot) {
+            chart.getXYPlot().getRenderer().setBaseToolTipGenerator(null);
+        }
+
+        chart.getXYPlot().getRangeAxis().setAutoRange(true);
+
+        chart.getXYPlot().getDomainAxis().setAutoRange(true);
+        chart.getXYPlot().getDomainAxis().setFixedAutoRange(duration.unit.toMillis(duration.value));
+
+        chartPanel = new ChartPanel(chart);
+
+        chartPanel.setDisplayToolTips(false);
+        chartPanel.setDoubleBuffered(true);
+        chartPanel.setMouseZoomable(false);
+        chartPanel.setPopupMenu(null);
+
+        /*
+         * By default, ChartPanel scales itself instead of redrawing things when
+         * it's resized. To have it resize automatically, we need to set minimum
+         * and maximum sizes. Lets constrain the minimum, but not the maximum
+         * size.
+         */
+        chartPanel.setMinimumDrawHeight(MINIMUM_DRAW_SIZE);
+        chartPanel.setMaximumDrawHeight(Integer.MAX_VALUE);
+        chartPanel.setMinimumDrawWidth(MINIMUM_DRAW_SIZE);
+        chartPanel.setMaximumDrawWidth(Integer.MAX_VALUE);
+
+        add(chartPanel, BorderLayout.CENTER);
+        add(getControlsAndAdditionalDisplay(duration), BorderLayout.SOUTH);
+
+    }
+
+    private Component getControlsAndAdditionalDisplay(Duration duration) {
+        JPanel container = new JPanel();
+        container.setOpaque(false);
+
+        container.setLayout(new BorderLayout());
+
+        container.add(getChartControls(duration), BorderLayout.LINE_START);
+        container.add(getAdditionalDataDisplay(), BorderLayout.LINE_END);
+
+        return container;
+    }
+
+    private Component getChartControls(Duration duration) {
+        JPanel container = new JPanel();
+        container.setOpaque(false);
+
+        final JTextField durationSelector = new JTextField(5);
+        final JComboBox<TimeUnit> unitSelector = new JComboBox<>();
+        unitSelector.setModel(new DefaultComboBoxModel<>(DEFAULT_TIMEUNITS));
+
+        TimeUnitChangeListener timeUnitChangeListener = new TimeUnitChangeListener(duration.value, duration.unit);
+
+        durationSelector.getDocument().addDocumentListener(timeUnitChangeListener);
+        unitSelector.addActionListener(timeUnitChangeListener);
+
+        durationSelector.setText(String.valueOf(duration.value));
+        unitSelector.setSelectedItem(duration.unit);
+
+        container.add(new JLabel(translator.localize(LocaleResources.CHART_DURATION_SELECTOR_LABEL).getContents()));
+        container.add(durationSelector);
+        container.add(unitSelector);
+
+        return container;
+    }
+
+    private Component getAdditionalDataDisplay() {
+        JPanel panel = new JPanel(new GridBagLayout());
+        panel.setOpaque(false);
+        labelContainer = new JPanel();
+        labelContainer.setOpaque(false);
+        GridBagConstraints constraints = new GridBagConstraints();
+        constraints.fill = GridBagConstraints.BOTH;
+        constraints.anchor = GridBagConstraints.CENTER;
+        panel.add(labelContainer, constraints);
+        return panel;
+    }
+
+    public void setTimeRangeToShow(int timeValue, TimeUnit timeUnit) {
+        XYPlot plot = chartPanel.getChart().getXYPlot();
+
+        // Don't drop old data; just dont' show it.
+        plot.getDomainAxis().setAutoRange(true);
+        plot.getDomainAxis().setFixedAutoRange(timeUnit.toMillis(timeValue));
+    }
+
+    public void setDataInformationLabel(final String text) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                if (label == null) {
+                    label = new ValueField(text);
+                    labelContainer.add(label);
+                }
+
+                label.setText(text);
+            }
+        });
+    }
+
+    private class TimeUnitChangeListener implements DocumentListener, ActionListener {
+
+        private int value;
+        private TimeUnit unit;
+
+        public TimeUnitChangeListener(int defaultValue, TimeUnit defaultUnit) {
+            this.value = defaultValue;
+            this.unit = defaultUnit;
+        }
+
+        @Override
+        public void removeUpdate(DocumentEvent event) {
+            changed(event.getDocument());
+        }
+
+        @Override
+        public void insertUpdate(DocumentEvent event) {
+            changed(event.getDocument());
+        }
+
+        @Override
+        public void changedUpdate(DocumentEvent event) {
+            changed(event.getDocument());
+        }
+
+        private void changed(Document doc) {
+            try {
+                this.value = Integer.valueOf(doc.getText(0, doc.getLength()));
+            } catch (NumberFormatException nfe) {
+                // ignore
+            } catch (BadLocationException ble) {
+                // ignore
+            }
+            chartChanged();
+        }
+
+        private void chartChanged() {
+            VmCpuChartPanel.this.firePropertyChange(PROPERTY_VISIBLE_TIME_RANGE, null, new VmCpuView.Duration(value, unit));
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            @SuppressWarnings("unchecked") // We are a TimeUnitChangeListener, specifically.
+            JComboBox<TimeUnit> comboBox = (JComboBox<TimeUnit>) e.getSource();
+            TimeUnit time = (TimeUnit) comboBox.getSelectedItem();
+            this.unit = time;
+            chartChanged();
+        }
+    }
+
+
+}
+
--- a/vm-cpu/client-swing/src/main/java/com/redhat/thermostat/vm/cpu/client/swing/internal/VmCpuPanel.java	Mon Oct 20 15:50:43 2014 -0400
+++ b/vm-cpu/client-swing/src/main/java/com/redhat/thermostat/vm/cpu/client/swing/internal/VmCpuPanel.java	Mon Oct 20 14:16:54 2014 -0400
@@ -37,10 +37,12 @@
 package com.redhat.thermostat.vm.cpu.client.swing.internal;
 
 import java.awt.Component;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
-import javax.swing.JPanel;
 import javax.swing.SwingUtilities;
 
 import org.jfree.chart.ChartFactory;
@@ -53,9 +55,9 @@
 import com.redhat.thermostat.client.swing.ComponentVisibleListener;
 import com.redhat.thermostat.client.swing.SwingComponent;
 import com.redhat.thermostat.client.swing.components.HeaderPanel;
-import com.redhat.thermostat.client.swing.components.RecentTimeSeriesChartPanel;
-import com.redhat.thermostat.client.ui.RecentTimeSeriesChartController;
 import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.common.ActionNotifier;
+import com.redhat.thermostat.common.model.Range;
 import com.redhat.thermostat.shared.locale.Translate;
 import com.redhat.thermostat.storage.model.DiscreteTimeData;
 import com.redhat.thermostat.vm.cpu.client.core.VmCpuView;
@@ -65,15 +67,26 @@
 
     private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
 
+    private static final int DEFAULT_VALUE = 10;
+    private static final TimeUnit DEFAULT_UNIT = TimeUnit.MINUTES;
+
+    private Duration duration;
+
     private HeaderPanel visiblePanel;
     
     private final TimeSeriesCollection data = new TimeSeriesCollection();
     private final TimeSeries cpuTimeSeries = new TimeSeries("cpu-stats");
 
+    private VmCpuChartPanel chartPanel;
+
+    private ActionNotifier<UserAction> userActionNotifier = new ActionNotifier<VmCpuView.UserAction>(this);
+
     public VmCpuPanel() {
         super();
         data.addSeries(cpuTimeSeries);
 
+        duration = new Duration(DEFAULT_VALUE, DEFAULT_UNIT);
+
         initializePanel();
 
         visiblePanel.addHierarchyListener(new ComponentVisibleListener() {
@@ -90,16 +103,6 @@
     }
 
     @Override
-    public void addActionListener(ActionListener<Action> listener) {
-        notifier.addActionListener(listener);
-    }
-
-    @Override
-    public void removeActionListener(ActionListener<Action> listener) {
-        notifier.removeActionListener(listener);
-    }
-
-    @Override
     public Component getUiComponent() {
         return visiblePanel;
     }
@@ -117,9 +120,42 @@
 
         chart.getXYPlot().getRangeAxis().setLowerBound(0.0);
 
-        JPanel chartPanel = new RecentTimeSeriesChartPanel(new RecentTimeSeriesChartController(chart));
+        chartPanel = new VmCpuChartPanel(chart, duration);
 
         visiblePanel.setContent(chartPanel);
+
+        chartPanel.addPropertyChangeListener(VmCpuChartPanel.PROPERTY_VISIBLE_TIME_RANGE, new PropertyChangeListener() {
+            @Override
+            public void propertyChange(PropertyChangeEvent evt) {
+                duration = (Duration) evt.getNewValue();
+                userActionNotifier.fireAction(UserAction.USER_CHANGED_TIME_RANGE);
+            }
+        });
+    }
+
+    @Override
+    public void addUserActionListener(ActionListener<UserAction> listener) {
+        userActionNotifier.addActionListener(listener);
+    }
+
+    @Override
+    public void removeUserActionListener(ActionListener<UserAction> listener) {
+        userActionNotifier.removeActionListener(listener);
+    }
+
+    @Override
+    public void setAvailableDataRange(Range<Long> availableInterval) {
+        // FIXME indicate the total data range to the user somehow
+    }
+
+    @Override
+    public void setVisibleDataRange(int time, TimeUnit unit) {
+        chartPanel.setTimeRangeToShow(time, unit);
+    }
+
+    @Override
+    public Duration getUserDesiredDuration() {
+        return duration;
     }
 
     @Override
--- a/vm-cpu/common/src/main/java/com/redhat/thermostat/vm/cpu/common/VmCpuStatDAO.java	Mon Oct 20 15:50:43 2014 -0400
+++ b/vm-cpu/common/src/main/java/com/redhat/thermostat/vm/cpu/common/VmCpuStatDAO.java	Mon Oct 20 14:16:54 2014 -0400
@@ -53,8 +53,14 @@
     static final Category<VmCpuStat> vmCpuStatCategory = new Category<>("vm-cpu-stats", VmCpuStat.class,
             Arrays.<Key<?>>asList(Key.AGENT_ID, Key.VM_ID, Key.TIMESTAMP, vmCpuLoadKey), Arrays.<Key<?>>asList(Key.TIMESTAMP));
 
+    public abstract VmCpuStat getOldest(VmRef ref);
+
+    public abstract VmCpuStat getLatest(VmRef ref);
+
     public abstract List<VmCpuStat> getLatestVmCpuStats(VmRef ref, long since);
 
+    public abstract List<VmCpuStat> getVmCpuStats(VmRef ref, long since, long to);
+
     public abstract void putVmCpuStat(VmCpuStat stat);
 
 }
--- a/vm-cpu/common/src/main/java/com/redhat/thermostat/vm/cpu/common/internal/VmCpuStatDAOImpl.java	Mon Oct 20 15:50:43 2014 -0400
+++ b/vm-cpu/common/src/main/java/com/redhat/thermostat/vm/cpu/common/internal/VmCpuStatDAOImpl.java	Mon Oct 20 14:16:54 2014 -0400
@@ -36,11 +36,13 @@
 
 package com.redhat.thermostat.vm.cpu.common.internal;
 
+import java.util.Date;
 import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import com.redhat.thermostat.common.utils.LoggingUtils;
+import com.redhat.thermostat.storage.core.Cursor;
 import com.redhat.thermostat.storage.core.DescriptorParsingException;
 import com.redhat.thermostat.storage.core.Key;
 import com.redhat.thermostat.storage.core.PreparedStatement;
@@ -49,6 +51,7 @@
 import com.redhat.thermostat.storage.core.Storage;
 import com.redhat.thermostat.storage.core.VmLatestPojoListGetter;
 import com.redhat.thermostat.storage.core.VmRef;
+import com.redhat.thermostat.storage.core.VmTimeIntervalPojoListGetter;
 import com.redhat.thermostat.vm.cpu.common.VmCpuStatDAO;
 import com.redhat.thermostat.vm.cpu.common.model.VmCpuStat;
 
@@ -64,15 +67,36 @@
                  "'" + Key.VM_ID.getName() + "' = ?s , " +
                  "'" + Key.TIMESTAMP.getName() + "' = ?l , " +
                  "'" + vmCpuLoadKey.getName() + "' = ?d";
-    
-    
+
+    // QUERY vm-cpu-stats WHERE 'agentId' = ?s AND \
+    //                        'vmId' = ?s \
+    //                        SORT 'timeStamp' ASC  \
+    //                        LIMIT 1
+    static final String DESC_OLDEST_VM_CPU_STAT = "QUERY " + vmCpuStatCategory.getName() +
+            " WHERE '" + Key.AGENT_ID.getName() + "' = ?s " +
+            "   AND '" + Key.VM_ID.getName() + "' = ?s " +
+            "  SORT '" + Key.TIMESTAMP.getName() + "' ASC " +
+            "  LIMIT 1";
+
+    // QUERY vm-cpu-stats WHERE 'agentId' = ?s AND \
+    //                        'vmId' = ?s \
+    //                        SORT 'timeStamp' DSC  \
+    //                        LIMIT 1
+    static final String DESC_LATEST_VM_CPU_STAT = "QUERY " + vmCpuStatCategory.getName() +
+            " WHERE '" + Key.AGENT_ID.getName() + "' = ?s " +
+            "   AND '" + Key.VM_ID.getName() + "' = ?s " +
+            "  SORT '" + Key.TIMESTAMP.getName() + "' DSC " +
+            "  LIMIT 1";
+
     private final Storage storage;
     private final VmLatestPojoListGetter<VmCpuStat> getter;
+    private final VmTimeIntervalPojoListGetter<VmCpuStat> otherGetter;
 
     VmCpuStatDAOImpl(Storage storage) {
         this.storage = storage;
         storage.registerCategory(vmCpuStatCategory);
         this.getter = new VmLatestPojoListGetter<>(storage, vmCpuStatCategory);
+        this.otherGetter = new VmTimeIntervalPojoListGetter<>(storage, vmCpuStatCategory);
     }
 
     @Override
@@ -81,6 +105,40 @@
     }
 
     @Override
+    public List<VmCpuStat> getVmCpuStats(VmRef ref, long since, long to) {
+        return otherGetter.getLatest(ref, since, to);
+    }
+
+    @Override
+    public VmCpuStat getLatest(VmRef ref) {
+        return runAgentAndVmIdQuery(ref, DESC_LATEST_VM_CPU_STAT);
+    }
+
+    @Override
+    public VmCpuStat getOldest(VmRef ref) {
+        return runAgentAndVmIdQuery(ref, DESC_OLDEST_VM_CPU_STAT);
+    }
+
+    private VmCpuStat runAgentAndVmIdQuery(VmRef ref, String descriptor) {
+        StatementDescriptor<VmCpuStat> desc = new StatementDescriptor<>(vmCpuStatCategory, descriptor);
+        PreparedStatement<VmCpuStat> prepared;
+        try {
+            prepared = storage.prepareStatement(desc);
+            prepared.setString(0, ref.getHostRef().getAgentId());
+            prepared.setString(1, ref.getVmId());
+            Cursor<VmCpuStat> cursor = prepared.executeQuery();
+            if (cursor.hasNext()) {
+                return cursor.next();
+            }
+        } catch (DescriptorParsingException e) {
+            logger.log(Level.SEVERE, "Preparing stmt '" + desc + "' failed!", e);
+        } catch (StatementExecutionException e) {
+            logger.log(Level.SEVERE, "Executing stmt '" + desc + "' failed!", e);
+        }
+        return null;
+    }
+
+    @Override
     public void putVmCpuStat(VmCpuStat stat) {
         StatementDescriptor<VmCpuStat> desc = new StatementDescriptor<>(vmCpuStatCategory, DESC_ADD_VM_CPU_STAT);
         PreparedStatement<VmCpuStat> prepared;
--- a/vm-cpu/common/src/main/java/com/redhat/thermostat/vm/cpu/common/internal/VmCpuStatDAOImplStatementDescriptorRegistration.java	Mon Oct 20 15:50:43 2014 -0400
+++ b/vm-cpu/common/src/main/java/com/redhat/thermostat/vm/cpu/common/internal/VmCpuStatDAOImplStatementDescriptorRegistration.java	Mon Oct 20 14:16:54 2014 -0400
@@ -41,6 +41,7 @@
 
 import com.redhat.thermostat.storage.core.PreparedParameter;
 import com.redhat.thermostat.storage.core.VmLatestPojoListGetter;
+import com.redhat.thermostat.storage.core.VmTimeIntervalPojoListGetter;
 import com.redhat.thermostat.storage.core.auth.DescriptorMetadata;
 import com.redhat.thermostat.storage.core.auth.StatementDescriptorRegistration;
 import com.redhat.thermostat.vm.cpu.common.VmCpuStatDAO;
@@ -53,21 +54,29 @@
 public class VmCpuStatDAOImplStatementDescriptorRegistration implements
         StatementDescriptorRegistration {
     
-    static final String descriptor = String.format(VmLatestPojoListGetter.VM_LATEST_QUERY_FORMAT,
+    static final String latestDescriptor = String.format(VmLatestPojoListGetter.VM_LATEST_QUERY_FORMAT,
+            VmCpuStatDAO.vmCpuStatCategory.getName());
+    static final String rangeDescriptor = String.format(VmTimeIntervalPojoListGetter.VM_INTERVAL_QUERY_FORMAT,
             VmCpuStatDAO.vmCpuStatCategory.getName());
 
     @Override
     public Set<String> getStatementDescriptors() {
         Set<String> descs = new HashSet<>();
         descs.add(VmCpuStatDAOImpl.DESC_ADD_VM_CPU_STAT);
-        descs.add(descriptor);
+        descs.add(VmCpuStatDAOImpl.DESC_LATEST_VM_CPU_STAT);
+        descs.add(VmCpuStatDAOImpl.DESC_OLDEST_VM_CPU_STAT);
+        descs.add(latestDescriptor);
+        descs.add(rangeDescriptor);
         return descs;
     }
     
     @Override
     public DescriptorMetadata getDescriptorMetadata(String descriptor,
             PreparedParameter[] params) {
-        if (descriptor.equals(VmCpuStatDAOImplStatementDescriptorRegistration.descriptor)) {
+        if (descriptor.equals(VmCpuStatDAOImplStatementDescriptorRegistration.latestDescriptor)
+                || descriptor.equals(VmCpuStatDAOImplStatementDescriptorRegistration.rangeDescriptor)
+                || descriptor.equals(VmCpuStatDAOImpl.DESC_LATEST_VM_CPU_STAT)
+                || descriptor.equals(VmCpuStatDAOImpl.DESC_OLDEST_VM_CPU_STAT)) {
             String agentId = (String)params[0].getValue();
             String vmId = (String)params[1].getValue();
             DescriptorMetadata metadata = new DescriptorMetadata(agentId, vmId);
--- a/vm-cpu/common/src/test/java/com/redhat/thermostat/vm/cpu/common/internal/VmCpuStatDAOImplStatementDescriptorRegistrationTest.java	Mon Oct 20 15:50:43 2014 -0400
+++ b/vm-cpu/common/src/test/java/com/redhat/thermostat/vm/cpu/common/internal/VmCpuStatDAOImplStatementDescriptorRegistrationTest.java	Mon Oct 20 14:16:54 2014 -0400
@@ -64,7 +64,7 @@
     public void registersAllDescriptors() {
         VmCpuStatDAOImplStatementDescriptorRegistration reg = new VmCpuStatDAOImplStatementDescriptorRegistration();
         Set<String> descriptors = reg.getStatementDescriptors();
-        assertEquals(2, descriptors.size());
+        assertEquals(5, descriptors.size());
         assertFalse("null descriptor not allowed", descriptors.contains(null));
     }
     
@@ -92,11 +92,38 @@
         // storage-core + this module
         assertEquals(2, registrations.size());
         assertNotNull(vmCpuStatReg);
-        assertEquals(2, vmCpuStatReg.getStatementDescriptors().size());
+        assertEquals(5, vmCpuStatReg.getStatementDescriptors().size());
     }
     
     @Test
     public void canGetMetadataForLatestCpuStatQuery() {
+        String descriptor = VmCpuStatDAOImplStatementDescriptorRegistration.latestDescriptor;
+
+        assertValidDescriptor(descriptor);
+    }
+
+    @Test
+    public void canGetMetadataForRangeCpuStatQuery() {
+        String descriptor = VmCpuStatDAOImplStatementDescriptorRegistration.rangeDescriptor;
+
+        assertValidDescriptor(descriptor);
+    }
+
+    @Test
+    public void canGetMetadataForSingleOldestCpuStatQuery() {
+        String descriptor = VmCpuStatDAOImpl.DESC_OLDEST_VM_CPU_STAT;
+
+        assertValidDescriptor(descriptor);
+    }
+
+    @Test
+    public void canGetMetadataForSingleLatestCpuStatQuery() {
+        String descriptor = VmCpuStatDAOImpl.DESC_LATEST_VM_CPU_STAT;
+
+        assertValidDescriptor(descriptor);
+    }
+
+    private void assertValidDescriptor(String descriptor) {
         PreparedParameter agentIdParam = mock(PreparedParameter.class);
         PreparedParameter vmIdParam = mock(PreparedParameter.class);
         String agentId = "agentId";
@@ -107,7 +134,7 @@
                 vmIdParam };
         
         StatementDescriptorMetadataFactory factory = new VmCpuStatDAOImplStatementDescriptorRegistration();
-        DescriptorMetadata data = factory.getDescriptorMetadata(VmCpuStatDAOImplStatementDescriptorRegistration.descriptor, params);
+        DescriptorMetadata data = factory.getDescriptorMetadata(descriptor, params);
         assertNotNull(data);
         assertEquals(agentId, data.getAgentId());
         assertEquals(vmId, data.getVmId());