changeset 1539:0963235b25fe

Added 'since' user filter to vm-memory tab Addresses gui side of PR2006 Reviewed-by: jerboaa, omajid Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2014-November/011475.html
author Jie Kang <jkang@redhat.com>
date Mon, 10 Nov 2014 10:14:30 -0500
parents fb69a13dd11c
children 874eb4ddfe8f
files vm-cpu/client-core/src/main/java/com/redhat/thermostat/vm/cpu/client/core/VmCpuView.java vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpController.java vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/core/MemoryMeter.java vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/core/MemoryStatsView.java vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/core/StatsModel.java vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/core/internal/MemoryStatsController.java vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/locale/LocaleResources.java vm-memory/client-core/src/main/resources/com/redhat/thermostat/vm/memory/client/locale/strings.properties vm-memory/client-core/src/test/java/com/redhat/thermostat/vm/memory/client/core/PayloadTest.java vm-memory/client-core/src/test/java/com/redhat/thermostat/vm/memory/client/core/StatsModelTest.java vm-memory/client-core/src/test/java/com/redhat/thermostat/vm/memory/client/core/internal/MemoryStatsControllerTest.java vm-memory/client-swing/src/main/java/com/redhat/thermostat/vm/memory/client/swing/internal/MemoryStatsViewImpl.java vm-memory/client-swing/src/main/java/com/redhat/thermostat/vm/memory/client/swing/internal/SwingMemoryStatsViewProvider.java vm-memory/client-swing/src/test/java/com/redhat/thermostat/vm/memory/client/swing/internal/MemoryStatsViewImplTest.java vm-memory/common/src/main/java/com/redhat/thermostat/vm/memory/common/internal/VmMemoryStatDAOImpl.java
diffstat 15 files changed, 343 insertions(+), 100 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 27 18:46:26 2014 +0100
+++ b/vm-cpu/client-core/src/main/java/com/redhat/thermostat/vm/cpu/client/core/VmCpuView.java	Mon Nov 10 10:14:30 2014 -0500
@@ -44,7 +44,6 @@
 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 {
 
--- a/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpController.java	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpController.java	Mon Nov 10 10:14:30 2014 -0500
@@ -375,10 +375,9 @@
         }
 
         private void updateMemoryChartAndDisplay() {
-            List<VmMemoryStat> vmInfo = null;
-            vmInfo = vmDao.getLatestVmMemoryStats(ref, desiredUpdateTimeStamp);
+            List<VmMemoryStat> vmInfo = vmDao.getLatestVmMemoryStats(ref, desiredUpdateTimeStamp);
 
-            for (VmMemoryStat memoryStats: vmInfo) {
+            for (VmMemoryStat memoryStats : vmInfo) {
                 long used = 0l;
                 long capacity = 0l;
                 Generation[] generations = memoryStats.getGenerations();
--- a/vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/core/MemoryMeter.java	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/core/MemoryMeter.java	Mon Nov 10 10:14:30 2014 -0500
@@ -91,7 +91,7 @@
     private String primaryUnit;
     private String secondaryUnit;
 
-        public void setPrimaryScaleUnit(String primaryUnit) {
+    public void setPrimaryScaleUnit(String primaryUnit) {
         this.primaryUnit = primaryUnit;
     }
     
--- a/vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/core/MemoryStatsView.java	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/core/MemoryStatsView.java	Mon Nov 10 10:14:30 2014 -0500
@@ -36,6 +36,8 @@
 
 package com.redhat.thermostat.vm.memory.client.core;
 
+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;
@@ -43,13 +45,40 @@
 import com.redhat.thermostat.shared.locale.LocalizedString;
 
 public abstract class MemoryStatsView extends BasicView implements UIComponent {
+
+    //FIXME: Duplicate class : See VmCpuView
+    public static class Duration {
+        public final int value;
+        public final TimeUnit unit;
+
+        public Duration(int value, TimeUnit unit) {
+            this.value = value;
+            this.unit = unit;
+        }
+
+        public long getMilliseconds() {
+            return this.unit.toMillis(this.value);
+        }
+
+        @Override
+        public String toString() {
+            return value + " " + unit;
+        }
+    }
+
+    //FIXME: Duplicate enum : See VmCpuView
+    public enum UserAction {
+        USER_CHANGED_TIME_RANGE,
+    }
     
     public abstract void addRegion(Payload region);
     public abstract void updateRegion(Payload region);
     
     public abstract void setEnableGCAction(boolean enable);
     public abstract void addGCActionListener(ActionListener<GCAction> listener);
-    
+
+    public abstract void addUserActionListener(ActionListener<UserAction> actionListener);
+
     public abstract void requestRepaint();
 
     public abstract void displayWarning(LocalizedString string);
--- a/vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/core/StatsModel.java	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/core/StatsModel.java	Mon Nov 10 10:14:30 2014 -0500
@@ -40,6 +40,7 @@
 import java.awt.geom.Rectangle2D;
 import java.awt.image.BufferedImage;
 import java.util.Date;
+import java.util.concurrent.TimeUnit;
 
 import javax.swing.plaf.ColorUIResource;
 
@@ -61,14 +62,16 @@
     private String name;
     
     private TimeSeries dataSet;
-    
-    public StatsModel() {
+
+    private long range;
+
+    public StatsModel(long range) {
+        this.range = range;
         dataSet = new TimeSeries("");
     }
     
     BufferedImage getChart(int width, int height, ColorUIResource bgColor, ColorUIResource fgColor) {
         JFreeChart chart = createChart(bgColor, fgColor);
-        
         BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
         chart.draw((Graphics2D) image.getGraphics(), new Rectangle2D.Double(0, 0, width, height), null);
         
@@ -97,14 +100,19 @@
             }
         }
     }
-    
+
+    public void setTimeRangeToShow(int timeValue, TimeUnit timeUnit) {
+        synchronized (lock) {
+            range = timeUnit.toMillis(timeValue);
+        }
+    }
+
     @Override
     protected StatsModel clone() {
         
-        StatsModel model = new StatsModel();
+        StatsModel model = new StatsModel(this.range);
         model.setName(name);
-        model.setRange(dataSet.getMaximumItemCount());
-        
+
         try {
             model.dataSet = dataSet.createCopy(0, dataSet.getItemCount() - 1);
         } catch (CloneNotSupportedException e) {
@@ -138,16 +146,17 @@
         plot.setDomainCrosshairVisible(false);
         plot.setRangeGridlinesVisible(false);
         plot.setRangeCrosshairVisible(false);
-                
+
         DateAxis dateAxis = new DateAxis();
-        
+        dateAxis.setAutoRange(true);
+        dateAxis.setFixedAutoRange(range);
         dateAxis.setTickLabelsVisible(false);
         dateAxis.setTickMarksVisible(false);
         dateAxis.setAxisLineVisible(false);
         dateAxis.setNegativeArrowVisible(false);
         dateAxis.setPositiveArrowVisible(false);
         dateAxis.setVisible(false);
-        
+
         NumberAxis numberAxis = new NumberAxis();
         numberAxis.setTickLabelsVisible(false);
         numberAxis.setTickMarksVisible(false);
@@ -156,11 +165,11 @@
         numberAxis.setPositiveArrowVisible(false);
         numberAxis.setVisible(false);
         numberAxis.setAutoRangeIncludesZero(false);
-        
+
         plot.setDomainAxis(dateAxis);
         plot.setRangeAxis(numberAxis);
         plot.setDataset(priceData);
-        
+
         plot.setInsets(new RectangleInsets(-1, -1, 0, 0));
         
         plot.setRenderer(new StandardXYItemRenderer(StandardXYItemRenderer.LINES));
--- a/vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/core/internal/MemoryStatsController.java	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/core/internal/MemoryStatsController.java	Mon Nov 10 10:14:30 2014 -0500
@@ -49,7 +49,6 @@
 import com.redhat.thermostat.common.ApplicationService;
 import com.redhat.thermostat.common.NotImplementedException;
 import com.redhat.thermostat.common.Size;
-import com.redhat.thermostat.common.Size.Unit;
 import com.redhat.thermostat.common.Timer;
 import com.redhat.thermostat.common.Timer.SchedulingType;
 import com.redhat.thermostat.common.command.Request;
@@ -76,81 +75,111 @@
 public class MemoryStatsController implements InformationServiceController<VmRef> {
 
     private static final Translate<LocaleResources> translate = LocaleResources.createLocalizer();
+    private static final MemoryStatsView.Duration defaultDuration = new MemoryStatsView.Duration(10, TimeUnit.MINUTES);
 
     private final MemoryStatsView view;
     private final VmMemoryStatDAO vmDao;
    
     private final VmRef ref;
     private final Timer timer;
-    
+
     private final Map<String, Payload> regions;
     
     private VMCollector collector;
     
     class VMCollector implements Runnable {
 
-        private long desiredUpdateTimeStamp = System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1);
+        private long desiredUpdateTimeStamp = System.currentTimeMillis() - defaultDuration.getMilliseconds();
 
         @Override
         public void run() {
             List<VmMemoryStat> vmInfo = vmDao.getLatestVmMemoryStats(ref, desiredUpdateTimeStamp);
             for (VmMemoryStat memoryStats: vmInfo) {
-                Generation[] generations = memoryStats.getGenerations();
-                
-                for (Generation generation : generations) {
-                    Space[] spaces = generation.getSpaces();
-                    for (Space space: spaces) {
-                        Payload payload = regions.get(space.getName());
-                        if (payload == null) {
-                            payload = new Payload();
-                            payload.setName(space.getName());
-                        }
+                update(memoryStats);
+            }
+        }
+
+        private void update(VmMemoryStat memoryStats) {
+            Generation[] generations = memoryStats.getGenerations();
+            for (Generation generation : generations) {
+                updateGeneration(generation, memoryStats.getTimeStamp());
+            }
+        }
+
+        private void updateGeneration(Generation generation, long timeStamp) {
+            Space[] spaces = generation.getSpaces();
+            for (Space space: spaces) {
+                updateSpace(space, timeStamp);
+            }
+        }
+
+        private void updateSpace(Space space, long timeStamp) {
+            Payload payload = getPayload(space.getName());
+
+            updatePayloadModel(payload, timeStamp, space.getUsed());
+
+            setPayloadFields(payload, space.getUsed(), space.getCapacity(), space.getMaxCapacity());
+
+            updateViewAndRegion(payload);
+
+            desiredUpdateTimeStamp = Math.max(desiredUpdateTimeStamp, timeStamp);
+        }
+
+        private void updateViewAndRegion(Payload payload) {
+            if (regions.containsKey(payload.getName())) {
+                view.updateRegion(payload.clone());
+            } else {
+                view.addRegion(payload.clone());
+                regions.put(payload.getName(), payload);
+            }
+
+            view.requestRepaint();
+        }
 
-                        Size.Unit usedScale = bestUnitForRange(space.getUsed(), space.getCapacity());
-                        double used = Size.bytes(space.getUsed()).convertTo(usedScale).getValue();
-                        double maxUsed = Size.bytes(space.getCapacity()).convertTo(usedScale).getValue();
-                        
-                        payload.setUsed(used);
-                        payload.setMaxUsed(maxUsed);
-                        payload.setUsedUnit(usedScale);
-                        
-                        Size.Unit maxScale = bestUnitForRange(space.getCapacity(), space.getMaxCapacity());
-                        double capacity = Size.bytes(space.getCapacity()).convertTo(maxScale).getValue();
-                        double maxCapacity = Size.bytes(space.getMaxCapacity()).convertTo(maxScale).getValue();
-                        
-                        payload.setCapacity(capacity);
-                        payload.setMaxCapacity(maxCapacity);
-                        payload.setCapacityUnit(maxScale);
-                        
-                        String tooltip = space.getName() + ": used: " + String.format("%.2f", used) + " " + usedScale +
-                                ", capacity: " + String.format("%.2f", capacity) + " " + maxScale +
-                                ", max capacity: " + String.format("%.2f", maxCapacity) + " " + maxScale;
-                        
-                        payload.setTooltip(tooltip);
-                        
-                        StatsModel model = payload.getModel();
-                        if (model == null) {
-                            model = new StatsModel();
-                            model.setName(space.getName());
-                            model.setRange(3600);
-                        }
-                        
-                        // normalize this always in the same unit
-                        model.addData(memoryStats.getTimeStamp(), Size.bytes(space.getUsed()).convertTo(Unit.MiB).getValue());
-                        
-                        payload.setModel(model);
-                        if (regions.containsKey(space.getName())) {
-                            view.updateRegion(payload.clone());
-                        } else {
-                            view.addRegion(payload.clone());
-                            regions.put(space.getName(), payload);
-                        }
-                        
-                        view.requestRepaint();
-                        desiredUpdateTimeStamp = Math.max(desiredUpdateTimeStamp, memoryStats.getTimeStamp());
-                    }
-                }
+        private void updatePayloadModel(Payload payload, long timeStamp, long used) {
+            StatsModel model = payload.getModel();
+            if (model == null) {
+                model = new StatsModel(defaultDuration.getMilliseconds());
+                model.setName(payload.getName());
+            }
+            // normalize this always in the same unit
+            model.addData(timeStamp, Size.bytes(used).convertTo(Size.Unit.MiB).getValue());
+
+            payload.setModel(model);
+        }
+
+        private Payload getPayload(String payloadName) {
+            Payload payload = regions.get(payloadName);
+            if (payload == null) {
+                payload = new Payload();
+                payload.setName(payloadName);
             }
+            return payload;
+        }
+
+
+        public void setPayloadFields(Payload payload, long used, long capacity, long maxCapacity) {
+            Size.Unit usedScale = bestUnitForRange(used, capacity);
+            double usedSpace = Size.bytes(used).convertTo(usedScale).getValue();
+            double maxUsed = Size.bytes(capacity).convertTo(usedScale).getValue();
+
+            payload.setUsed(usedSpace);
+            payload.setMaxUsed(maxUsed);
+            payload.setUsedUnit(usedScale);
+
+            Size.Unit maxScale = bestUnitForRange(capacity, maxCapacity);
+            double capacitySpace = Size.bytes(capacity).convertTo(maxScale).getValue();
+            double maxCapacitySpace = Size.bytes(maxCapacity).convertTo(maxScale).getValue();
+
+            payload.setCapacity(capacitySpace);
+            payload.setMaxCapacity(maxCapacitySpace);
+            payload.setCapacityUnit(maxScale);
+
+            String tooltip = payload.getName() + ": used: " + String.format("%.2f", usedSpace) + " " + usedScale +
+                    ", capacity: " + String.format("%.2f", capacitySpace) + " " + maxScale +
+                    ", max capacity: " + String.format("%.2f", maxCapacitySpace) + " " + maxScale;
+
+            payload.setTooltip(tooltip);
         }
     }
     
@@ -163,7 +192,7 @@
         this.ref = ref;
         vmDao = vmMemoryStatDao;
         view = viewProvider.createView();
-        
+
         timer = appSvc.getTimerFactory().createTimer();
         
         collector = new VMCollector();
@@ -191,6 +220,26 @@
                 }
             }
         });
+
+        view.addUserActionListener(new ActionListener<MemoryStatsView.UserAction>() {
+            @Override
+            public void actionPerformed(ActionEvent<MemoryStatsView.UserAction> e) {
+                switch (e.getActionId()) {
+                    case USER_CHANGED_TIME_RANGE:
+                        MemoryStatsView.Duration duration = (MemoryStatsView.Duration) e.getPayload();
+                        for (Map.Entry<String, Payload> entry : regions.entrySet()) {
+                            Payload p = entry.getValue();
+                            StatsModel model = p.getModel();
+                            if (model != null) {
+                                model.setTimeRangeToShow(duration.value, duration.unit);
+                            }
+                        }
+                        break;
+                    default:
+                        throw new AssertionError("Unhandled action type: " + e.getActionId());
+                }
+            }
+        });
         
         view.addGCActionListener(new ActionListener<GCAction>() {
             @Override
--- a/vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/locale/LocaleResources.java	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-memory/client-core/src/main/java/com/redhat/thermostat/vm/memory/client/locale/LocaleResources.java	Mon Nov 10 10:14:30 2014 -0500
@@ -44,7 +44,8 @@
     ERROR_PERFORMING_GC,
     MEMORY_REGIONS_HEADER,
 
-    RESOURCE_MISSING;
+    RESOURCE_MISSING,
+    CHART_DURATION_SELECTOR_LABEL;
     
     public static final String RESOURCE_BUNDLE =
             "com.redhat.thermostat.vm.memory.client.locale.strings";
--- a/vm-memory/client-core/src/main/resources/com/redhat/thermostat/vm/memory/client/locale/strings.properties	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-memory/client-core/src/main/resources/com/redhat/thermostat/vm/memory/client/locale/strings.properties	Mon Nov 10 10:14:30 2014 -0500
@@ -2,4 +2,5 @@
 VM_INFO_TAB_MEMORY = Memory
 MEMORY_REGIONS_HEADER = Memory Regions
 
-ERROR_PERFORMING_GC = Error performing Garbage Collection (agent: {0}, vm: {1})
\ No newline at end of file
+ERROR_PERFORMING_GC = Error performing Garbage Collection (agent: {0}, vm: {1})
+CHART_DURATION_SELECTOR_LABEL = Display the most recent
\ No newline at end of file
--- a/vm-memory/client-core/src/test/java/com/redhat/thermostat/vm/memory/client/core/PayloadTest.java	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-memory/client-core/src/test/java/com/redhat/thermostat/vm/memory/client/core/PayloadTest.java	Mon Nov 10 10:14:30 2014 -0500
@@ -47,8 +47,9 @@
 
     @Test
     public void testClone() {
-        
-        StatsModel model = new StatsModel();
+        long fluff = 10;
+        StatsModel model = new StatsModel(fluff);
+
         model.setName("fluffModel");
         model.setRange(100);
         model.addData(500, 2.0);
--- a/vm-memory/client-core/src/test/java/com/redhat/thermostat/vm/memory/client/core/StatsModelTest.java	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-memory/client-core/src/test/java/com/redhat/thermostat/vm/memory/client/core/StatsModelTest.java	Mon Nov 10 10:14:30 2014 -0500
@@ -46,8 +46,8 @@
 
     @Test
     public void testClone() {
-        
-        StatsModel source = new StatsModel();
+        long fluff = 10;
+        StatsModel source = new StatsModel(fluff);
         source.setName("fluffModel");
         source.setRange(100);
         source.addData(500, 2.0);
--- a/vm-memory/client-core/src/test/java/com/redhat/thermostat/vm/memory/client/core/internal/MemoryStatsControllerTest.java	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-memory/client-core/src/test/java/com/redhat/thermostat/vm/memory/client/core/internal/MemoryStatsControllerTest.java	Mon Nov 10 10:14:30 2014 -0500
@@ -280,7 +280,7 @@
         verify(memoryStatDao, times(2)).getLatestVmMemoryStats(isA(VmRef.class), timeStampCaptor.capture());
 
         long timeStamp1 = timeStampCaptor.getAllValues().get(0);
-        assertTimeStampIsAround(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1), timeStamp1);
+        assertTimeStampIsAround(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10), timeStamp1);
 
         long timeStamp2 = timeStampCaptor.getAllValues().get(1);
         assertTimeStampIsAround(DATA_TIMESTAMP, timeStamp2);
@@ -298,10 +298,10 @@
         verify(memoryStatDao, times(2)).getLatestVmMemoryStats(isA(VmRef.class), timeStampCaptor.capture());
 
         long timeStamp1 = timeStampCaptor.getAllValues().get(0);
-        assertTimeStampIsAround(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1), timeStamp1);
+        assertTimeStampIsAround(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10), timeStamp1);
 
         long timeStamp2 = timeStampCaptor.getAllValues().get(1);
-        assertTimeStampIsAround(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1), timeStamp2);
+        assertTimeStampIsAround(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(10), timeStamp2);
     }
 
     private void assertTimeStampIsAround(long expected, long actual) {
--- a/vm-memory/client-swing/src/main/java/com/redhat/thermostat/vm/memory/client/swing/internal/MemoryStatsViewImpl.java	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-memory/client-swing/src/main/java/com/redhat/thermostat/vm/memory/client/swing/internal/MemoryStatsViewImpl.java	Mon Nov 10 10:14:30 2014 -0500
@@ -36,23 +36,37 @@
 
 package com.redhat.thermostat.vm.memory.client.swing.internal;
 
+import java.awt.BorderLayout;
 import java.awt.Component;
 import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
 import java.beans.Transient;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 import javax.swing.Box;
 import javax.swing.BoxLayout;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
 import javax.swing.JOptionPane;
 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 com.redhat.thermostat.client.core.views.BasicView;
 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.common.ActionListener;
+import com.redhat.thermostat.common.ActionNotifier;
 import com.redhat.thermostat.gc.remote.client.common.RequestGCAction;
 import com.redhat.thermostat.gc.remote.client.swing.ToolbarGCButton;
 import com.redhat.thermostat.gc.remote.common.command.GCAction;
@@ -66,11 +80,18 @@
 
     private static final Translate<LocaleResources> t = LocaleResources.createLocalizer();
 
+    //FIXME: Duplicate default timeunits : See VmCpuChartPanel.java
+    private static final TimeUnit[] DEFAULT_TIMEUNITS = new TimeUnit[] { TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MINUTES };
+
     private static final long REPAINT_DELAY = 500;
+
     private long lastRepaint;
     
     private HeaderPanel visiblePanel;
-    private JPanel realPanel;
+    private JPanel graphPanel;
+    private JPanel contentPanel;
+
+    private JPanel labelContainer;
     
     private final Map<String, MemoryGraphPanel> regions;
     
@@ -78,8 +99,10 @@
     private RequestGCAction toolbarButtonAction;
     
     private Dimension preferredSize;
-    
-    public MemoryStatsViewImpl() {
+
+    private ActionNotifier<UserAction> userActionNotifier = new ActionNotifier<>(this);
+
+    public MemoryStatsViewImpl(Duration duration) {
         super();
         visiblePanel = new HeaderPanel();
         regions = new HashMap<>();
@@ -100,9 +123,16 @@
             }
         });
 
-        realPanel = new JPanel();
-        realPanel.setLayout(new BoxLayout(realPanel, BoxLayout.Y_AXIS));
-        visiblePanel.setContent(realPanel);
+        graphPanel = new JPanel();
+        graphPanel.setLayout(new BoxLayout(graphPanel, BoxLayout.Y_AXIS));
+
+        contentPanel = new JPanel();
+        contentPanel.setLayout(new BorderLayout());
+
+        contentPanel.add(graphPanel, BorderLayout.CENTER);
+        contentPanel.add(getControlsAndAdditionalDisplay(duration), BorderLayout.SOUTH);
+
+        visiblePanel.setContent(contentPanel);
         
         toolbarButtonAction = new RequestGCAction();
         toolbarButton = new ToolbarGCButton(toolbarButtonAction);
@@ -141,7 +171,12 @@
     public void addGCActionListener(ActionListener<GCAction> listener) {
         toolbarButtonAction.addActionListener(listener);
     }
-    
+
+    @Override
+    public void addUserActionListener(ActionListener<UserAction> listener) {
+        userActionNotifier.addActionListener(listener);
+    }
+
     @Override
     public void addRegion(final Payload region) {
 
@@ -150,8 +185,8 @@
             public void run() {
                 MemoryGraphPanel memoryGraphPanel = new MemoryGraphPanel();
                 
-                realPanel.add(memoryGraphPanel);
-                realPanel.add(Box.createRigidArea(new Dimension(5,5)));
+                graphPanel.add(memoryGraphPanel);
+                graphPanel.add(Box.createRigidArea(new Dimension(5, 5)));
                 regions.put(region.getName(), memoryGraphPanel);
                 
                 // components are stacked up vertically in this panel
@@ -162,7 +197,7 @@
                 }
 
                 updateRegion(region);
-                realPanel.revalidate();
+                graphPanel.revalidate();
             }
         });
     }
@@ -190,5 +225,109 @@
     public BasicView getView() {
         return this;
     }
+
+    //FIXME : Duplicate method : see RecentTimeSeriesChartPanel.java or VmCpuChartPanel.java
+    public 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);
+
+        container.setName("since-panel");
+
+        return container;
+    }
+
+    //FIXME : Duplicate method : see RecentTimeSeriesChartPanel.java or VmCpuChartPanel.java
+    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(t.localize(LocaleResources.CHART_DURATION_SELECTOR_LABEL).getContents()));
+        container.add(durationSelector);
+        container.add(unitSelector);
+
+        return container;
+    }
+
+    //FIXME : Duplicate method : see RecentTimeSeriesChartPanel.java or VmCpuChartPanel.java
+    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;
+    }
+
+    //FIXME : Duplicate class : see RecentTimeSeriesChartPanel.java or VmCpuChartPanel.java
+    private class TimeUnitChangeListener implements DocumentListener, java.awt.event.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() {
+            MemoryStatsViewImpl.this.userActionNotifier.fireAction(UserAction.USER_CHANGED_TIME_RANGE, new 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-memory/client-swing/src/main/java/com/redhat/thermostat/vm/memory/client/swing/internal/SwingMemoryStatsViewProvider.java	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-memory/client-swing/src/main/java/com/redhat/thermostat/vm/memory/client/swing/internal/SwingMemoryStatsViewProvider.java	Mon Nov 10 10:14:30 2014 -0500
@@ -36,6 +36,8 @@
 
 package com.redhat.thermostat.vm.memory.client.swing.internal;
 
+import java.util.concurrent.TimeUnit;
+
 import com.redhat.thermostat.vm.memory.client.core.MemoryStatsView;
 import com.redhat.thermostat.vm.memory.client.core.MemoryStatsViewProvider;
 
@@ -43,7 +45,7 @@
 
     @Override
     public MemoryStatsView createView() {
-        return new MemoryStatsViewImpl();
+        return new MemoryStatsViewImpl(new MemoryStatsView.Duration(10, TimeUnit.MINUTES));
     }
 
 }
--- a/vm-memory/client-swing/src/test/java/com/redhat/thermostat/vm/memory/client/swing/internal/MemoryStatsViewImplTest.java	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-memory/client-swing/src/test/java/com/redhat/thermostat/vm/memory/client/swing/internal/MemoryStatsViewImplTest.java	Mon Nov 10 10:14:30 2014 -0500
@@ -36,9 +36,9 @@
 
 package com.redhat.thermostat.vm.memory.client.swing.internal;
 
-import javax.swing.JFrame;
+import java.util.concurrent.TimeUnit;
 
-import net.java.openjdk.cacio.ctc.junit.CacioFESTRunner;
+import javax.swing.JFrame;
 
 import org.fest.swing.annotation.GUITest;
 import org.fest.swing.edt.FailOnThreadViolationRepaintManager;
@@ -46,6 +46,7 @@
 import org.fest.swing.edt.GuiTask;
 import org.fest.swing.fixture.FrameFixture;
 import org.fest.swing.fixture.JButtonFixture;
+import org.fest.swing.fixture.JPanelFixture;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.BeforeClass;
@@ -53,6 +54,10 @@
 import org.junit.experimental.categories.Category;
 import org.junit.runner.RunWith;
 
+import com.redhat.thermostat.vm.memory.client.core.MemoryStatsView;
+
+import net.java.openjdk.cacio.ctc.junit.CacioFESTRunner;
+
 @RunWith(CacioFESTRunner.class)
 public class MemoryStatsViewImplTest {
 
@@ -70,7 +75,7 @@
         GuiActionRunner.execute(new GuiTask() {
             @Override
             protected void executeInEDT() throws Throwable {
-                view = new MemoryStatsViewImpl();
+                view = new MemoryStatsViewImpl(new MemoryStatsView.Duration(10, TimeUnit.MINUTES));
                 frame = new JFrame();
                 frame.add(view.getUiComponent());
 
@@ -96,5 +101,15 @@
         JButtonFixture togglefixture = frameFixture.button("gcButton");
         togglefixture.requireDisabled();
     }
+
+    @Category(GUITest.class)
+    @GUITest
+    @Test
+    public void testSincePanelIsVisible() {
+        frameFixture.show();
+
+        JPanelFixture sincePanel = frameFixture.panel("since-panel");
+        sincePanel.requireVisible();
+    }
 }
 
--- a/vm-memory/common/src/main/java/com/redhat/thermostat/vm/memory/common/internal/VmMemoryStatDAOImpl.java	Mon Oct 27 18:46:26 2014 +0100
+++ b/vm-memory/common/src/main/java/com/redhat/thermostat/vm/memory/common/internal/VmMemoryStatDAOImpl.java	Mon Nov 10 10:14:30 2014 -0500
@@ -129,6 +129,5 @@
     public List<VmMemoryStat> getLatestVmMemoryStats(VmRef ref, long since) {
         return getter.getLatest(ref, since);
     }
-
 }