Mercurial > hg > thermostat-ng > agent
changeset 2455:998669414ab4
Align Byteman chart view with look and feel.
Reviewed-by: neugens
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-September/020887.html
author | Severin Gehwolf <sgehwolf@redhat.com> |
---|---|
date | Wed, 14 Sep 2016 15:05:13 +0200 |
parents | 69dee5f97497 |
children | 8c3134195c82 |
files | vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/GraphDataset.java vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/LocaleResources.java vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanView.java vm-byteman/client-swing/src/main/resources/com/redhat/thermostat/vm/byteman/client/swing/internal/strings.properties vm-byteman/client-swing/src/test/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanViewTest.java |
diffstat | 5 files changed, 586 insertions(+), 468 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/GraphDataset.java Wed Sep 14 15:05:13 2016 +0200 @@ -0,0 +1,391 @@ +/* + * Copyright 2012-2016 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.byteman.client.swing.internal; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jfree.data.DefaultKeyedValues; +import org.jfree.data.category.CategoryDataset; +import org.jfree.data.category.DefaultCategoryDataset; +import org.jfree.data.general.DefaultPieDataset; +import org.jfree.data.general.PieDataset; +import org.jfree.data.xy.XYDataset; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; + +import com.redhat.thermostat.common.Pair; +import com.redhat.thermostat.vm.byteman.common.BytemanMetric; + +class GraphDataset { + + enum CoordinateType { + INTEGRAL, + REAL, + TIME, + CATEGORY + }; + + /** + * A special coordinate name used to identify the timestamp associated + * with any given Byteman metric. If it is entered as the x or y coordinate + * name in the graph dialogue then it will select the tiemstamp as the value + * to be graphed against the other chosen coordinate. Timestamp values + * are stored as longs but are displayed as time values. + * + * TODO: it really only makes sense to use timestamp as the X axis. maybe + * we should reject any attempt to use it for the y axis? + */ + static final String TIMESTAMP_KEY = "timestamp"; + + /** + * A special coordinate name used to identify the frequency count + * of any given Byteman metric. If it is entered as the x or y coordinate + * name in the graph dialogue then it will count 1 for each occurence of + * other value. Frequency values are stored as longs. + */ + static final String FREQUENCY_KEY = "frequency"; + + /** + * A special coordinate name used to identify the marker string + * of any given Byteman metric. If it is entered as the x or y coordinate + * name in the graph dialogue then it will select the marker as the value + * to be graphed against the other chosen coordinate. + */ + static final String MARKER_KEY = "marker"; + + private final List<Pair<Object, Object>> data; + String xkey; + String ykey; + CoordinateType xtype; + CoordinateType ytype; + private static CategoryDataset emptyCategoryDataset = new DefaultCategoryDataset(); + private static PieDataset emptyPieDataset = new DefaultPieDataset(); + private static XYDataset emptyXYDataset = new XYSeriesCollection(); + private static Number frequencyUnit = Long.valueOf(1); + + public GraphDataset(List<BytemanMetric> metrics, String xkey, String ykey, String filter, String value) + { + this.xkey = xkey; + this.ykey = ykey; + xtype = CoordinateType.INTEGRAL; + ytype = CoordinateType.INTEGRAL; + data = new ArrayList<Pair<Object,Object>>(); + if (TIMESTAMP_KEY.equals(xkey)) { + xtype = CoordinateType.TIME; + } else if (FREQUENCY_KEY.equals(xkey)) { + xtype = CoordinateType.INTEGRAL; + } else if (MARKER_KEY.equals(xkey)) { + xtype = CoordinateType.CATEGORY; + } + if (TIMESTAMP_KEY.equals(ykey)) { + ytype = CoordinateType.TIME; + } else if (FREQUENCY_KEY.equals(ykey)) { + ytype = CoordinateType.INTEGRAL; + } else if (MARKER_KEY.equals(ykey)) { + ytype = CoordinateType.CATEGORY; + } + // if we have a filter value then convert it to a number if it is numeric + Object filterValue = value; + if (filter != null && value != null) { + // may need to convert String to Numeric + filterValue = maybeNumeric(value); + } + if (metrics != null) { + for (BytemanMetric m : metrics) { + Map<String, Object> map = m.getDataAsMap(); + // ensure that lookups for the timestamp key always retrieve + // the Long timestamp value associated with the metric and + // that lookups for the frequency key always retrieve + // the Long value 1. + map.put(TIMESTAMP_KEY, m.getTimeStamp()); + map.put(FREQUENCY_KEY, frequencyUnit); + map.put(MARKER_KEY, m.getMarker()); + // if we have a filter then check for presence of filter key + if (filter != null && filter.length() > 0) { + Object v = map.get(filter); + if (v == null) { + // skip this metric + continue; + } + if (filterValue != null) { + // may need to process String value as Numeric + if (v instanceof String) { + v = maybeNumeric((String)v); + } + if (!filterValue.equals(v)) { + // skip this metric + continue; + } + } + } + Object xval = map.get(xkey); + Object yval = map.get(ykey); + // only include records which contain values for both coordinates + if(xval != null && yval != null) { + // maybe re-present retrieved values as Numeric + // and/or downgrade coordinate type from INTEGRAL + // to REAL or even CATEGORY + xval = newCoordinate(xkey, xval, true); + yval = newCoordinate(ykey, yval, false); + data.add(new Pair<Object, Object>(xval, yval)); + } + } + } + } + + public int size() { + return data.size(); + } + + public XYDataset getXYDataset() + { + if (xtype == CoordinateType.CATEGORY || + ytype == CoordinateType.CATEGORY) { + return emptyXYDataset; + } + + XYSeries xyseries = new XYSeries(ykey + " against " + xkey); + + for (Pair<Object,Object> p : data) { + Number x = (Number)p.getFirst(); + Number y = (Number)p.getSecond(); + int idx = xyseries.indexOf(x); + if (idx >= 0) { + Number y1 = xyseries.getY(idx); + switch (ytype) { + case REAL: + y = y.doubleValue() + y1.doubleValue(); + default: + y = y.longValue() + y1.longValue(); + } + } + xyseries.add(x, y); + } + XYSeriesCollection xycollection = new XYSeriesCollection(); + xycollection.addSeries(xyseries); + return xycollection; + } + + public CategoryDataset getCategoryDataset() + { + if (xtype == CoordinateType.TIME) { + return emptyCategoryDataset; + } + DefaultCategoryDataset dataset = new DefaultCategoryDataset(); + // treat x values as category values by calling toString + // where they are numeric we ought to support binning them into ranges + switch (ytype) { + case CATEGORY: + // graph category against category by frequency + for (Pair<Object, Object> p : data) { + String first = p.getFirst().toString(); + String second = p.getSecond().toString(); + if(dataset.getRowKeys().contains(first) && dataset.getColumnKeys().contains(second)) { + dataset.incrementValue(1.0, first, second); + } else { + dataset.addValue(1.0, first, second); + } + } + break; + case TIME: + // bin time values into ranges and label range with start time + // for now just drop through to treat time value as numeric + default: + // graph category against numeric by summing numeric values + for (Pair<Object, Object> p : data) { + String first = p.getFirst().toString(); + String second = ""; + double increment = ((Number) p.getSecond()).doubleValue(); + if(dataset.getRowKeys().contains(first)) { + dataset.incrementValue(increment, first, second); + } else { + dataset.addValue(increment, first, second); + } + } + break; + } + return dataset; + } + + // alternative option for presenting category xkey with numeric ykey + public PieDataset getPieDataset() + { + if (xtype != CoordinateType.CATEGORY || ytype == CoordinateType.CATEGORY) { + return emptyPieDataset; + } + + DefaultKeyedValues keyedValues = new DefaultKeyedValues(); + + for (Pair<Object,Object> p : data) { + String first = p.getFirst().toString(); + double second = ((Number)p.getSecond()).doubleValue(); + int index = keyedValues.getIndex(first); + if (index >= 0) { + Number existing = keyedValues.getValue(first); + keyedValues.addValue(first, existing.doubleValue() + second); + } else { + keyedValues.addValue(first, second); + } + } + PieDataset pieDataset = new DefaultPieDataset(keyedValues); + return pieDataset; + } + + public XYDataset getCategoryTimePlot(String[][] symbolsReturn) + { + if (xtype != CoordinateType.TIME || ytype != CoordinateType.CATEGORY) { + return emptyXYDataset; + } + + // we need to display changing category state over time + // + // we can create an XYDataSet substituting numeric Y values + // to encode the category data. then we provide the data + // set with a range axis which displays the numeric + // values symbolically. + + XYSeries xyseries = new XYSeries(ykey + " against " + xkey); + int count = 0; + HashMap<String, Number> tickmap = new HashMap<String, Number>(); + + for (Pair<Object,Object> p : data) { + Number x = (Number)p.getFirst(); + String ysym = (String)p.getSecond(); + Number y = tickmap.get(ysym); + if (y == null) { + y = Long.valueOf(count++); + tickmap.put(ysym, y); + } + xyseries.add(x, y); + } + // populate key array + String[] symbols = new String[count]; + for (String key: tickmap.keySet()) { + int value = tickmap.get(key).intValue(); + symbols[value] = key; + } + + symbolsReturn[0] = symbols; + + XYSeriesCollection xycollection = new XYSeriesCollection(); + xycollection.addSeries(xyseries); + + return xycollection; + } + + public String getXLabel() { + return xkey; + } + + public String getYLabel() { + return ykey; + } + + public CoordinateType getXType() { + return xtype; + } + + public CoordinateType getYType() { + return ytype; + } + + private Object maybeNumeric(String value) { + if (value == null || value.length() == 0) { + return null; + } + try { + if(value.contains(".")) { + return Double.valueOf(value); + } else { + return Long.valueOf(value); + } + } catch (NumberFormatException nfe) { + return value; + } + } + + /** + * process a newly read x or y coordinate value, which is either a Long timestanp or an unparsed + * numeric or category value String, returning a Long, parsed Numeric or String value. As a side + * effect of attempting to parse an input String the coordinate type for the relevant coordinate + * axis may be downgraded from INTEGRAL (assumed default) to DOUBLE or CATEGORY. + * @param key the label for the coordinate axis which may be the special value timestamp + * @param value the new found coordinate value which may be a Long timestamp or a String yet to be parsed + * @param isX true if this is an x coordinate value false if it is a y coordinate value + * @return an Object repreenting + */ + private Object newCoordinate(String key, Object value, boolean isX) { + + CoordinateType ctype = (isX ? xtype : ytype); + if (ctype == CoordinateType.TIME) { + // guaranteed already to be a Long + return value; + } + + boolean updateCType = false; + + if (value instanceof String && ctype != CoordinateType.CATEGORY) { + String str = (String)value; + // see if we can parse this as a number + try { + if (str.contains(".")) { + value = Double.valueOf(str); + if (ctype != CoordinateType.REAL) { + ctype = CoordinateType.REAL; + updateCType = true; + } + } else { + value = Long.valueOf(str); + } + } catch (NumberFormatException nfe) { + ctype = CoordinateType.CATEGORY; + updateCType = true; + } + } + if (updateCType) { + if (isX) { + xtype = ctype; + } else { + ytype = ctype; + } + } + return value; + } +} \ No newline at end of file
--- a/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/LocaleResources.java Fri Sep 16 10:25:53 2016 -0400 +++ b/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/LocaleResources.java Wed Sep 14 15:05:13 2016 +0200 @@ -55,6 +55,10 @@ LABEL_LOCAL_RULE, LABEL_INJECTED_RULE, IMPORT_RULE, + FILTER, + FILTER_VALUE_LABEL, + X_COORD, + Y_COORD ; static final String RESOURCE_BUNDLE = LocaleResources.class.getPackage().getName() + ".strings";
--- a/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanView.java Fri Sep 16 10:25:53 2016 -0400 +++ b/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanView.java Wed Sep 14 15:05:13 2016 +0200 @@ -54,13 +54,9 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.text.DateFormat; -import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -94,29 +90,26 @@ import com.redhat.thermostat.client.swing.components.ThermostatTabbedPane; import com.redhat.thermostat.client.swing.components.ThermostatTextArea; import com.redhat.thermostat.client.swing.components.experimental.RecentTimeControlPanel; +import com.redhat.thermostat.client.swing.components.experimental.RecentTimeControlPanel.UnitRange; +import com.redhat.thermostat.client.swing.components.experimental.ThermostatChartPanel; +import com.redhat.thermostat.client.swing.components.experimental.ThermostatChartPanelBuilder; import com.redhat.thermostat.client.swing.experimental.ComponentVisibilityNotifier; import com.redhat.thermostat.common.ActionEvent; import com.redhat.thermostat.common.ActionListener; import com.redhat.thermostat.common.Duration; -import com.redhat.thermostat.common.Pair; import com.redhat.thermostat.common.utils.LoggingUtils; import com.redhat.thermostat.shared.locale.LocalizedString; import com.redhat.thermostat.shared.locale.Translate; +import com.redhat.thermostat.vm.byteman.client.swing.internal.GraphDataset.CoordinateType; import com.redhat.thermostat.vm.byteman.common.BytemanMetric; import org.jfree.chart.ChartFactory; -import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.SymbolAxis; import org.jfree.chart.plot.PlotOrientation; -import org.jfree.data.DefaultKeyedValues; import org.jfree.data.category.CategoryDataset; import org.jfree.data.category.DefaultCategoryDataset; -import org.jfree.data.general.DefaultPieDataset; -import org.jfree.data.general.PieDataset; import org.jfree.data.xy.XYDataset; -import org.jfree.data.xy.XYSeries; -import org.jfree.data.xy.XYSeriesCollection; public class SwingVmBytemanView extends VmBytemanView implements SwingComponent { @@ -127,6 +120,7 @@ private static final Icon ARROW_LEFT = IconResource.ARROW_LEFT.getIcon(); private static final Icon ARROW_RIGHT = IconResource.ARROW_RIGHT.getIcon(); private static final String EMPTY_STR = ""; + private static final String BYTEMAN_CHART_LABEL = EMPTY_STR; static final String NO_METRICS_AVAILABLE = t.localize(LocaleResources.NO_METRICS_AVAILABLE).getContents(); @@ -138,15 +132,16 @@ private String injectedRuleContent; private String unloadedRuleContent; + private ThermostatChartPanel graphPanel; + private RecentTimeControlPanel graphTimeControlPanel; private boolean generateRuleToggle; private final JTextArea metricsText; private final JTextArea unloadedRulesText; private final JTextArea injectedRulesText; private final JButton injectRuleButton; private final JButton unloadRuleButton; - private JPanel graphMainPanel; - private ChartPanel graphPanel; - private JFreeChart graph; + private final JPanel graphMainPanel; + private final JPanel metricsPanel; private final JTabbedPane tabbedPane; private final HeaderPanel mainContainer; private final ActionToggleButton toggleButton; @@ -158,15 +153,14 @@ // ideally these ought to be stored as // fields of a separate model instance - String xkey = null; - String ykey = null; - String filter = null; - String value = null; - String graphtype = null; + private String xkey = null; + private String ykey = null; + private String filter = null; + private String value = null; + private String graphtype = null; // duration over which to search for metrics - - Duration duration = new Duration(5, TimeUnit.MINUTES); + private Duration duration = ThermostatChartPanel.DEFAULT_DATA_DISPLAY; // Mutable state private boolean viewControlsEnabled; @@ -264,6 +258,7 @@ splitPane.setRightComponent(injectedPane); splitPane.setResizeWeight(halfWeight); BasicSplitPaneUI ui = new BasicSplitPaneUI() { + @SuppressWarnings("serial") public BasicSplitPaneDivider createDefaultDivider() { return new BasicSplitPaneDivider(this) { @Override @@ -357,7 +352,7 @@ rulesPanel.add(buttonHolder, cRules); // Metrics tab - final JPanel metricsPanel = new JPanel(); + metricsPanel = new JPanel(); metricsPanel.setLayout(new GridBagLayout()); metricsText = new ThermostatTextArea(EMPTY_STR); metricsText.setName(METRICS_TEXT_NAME); @@ -373,40 +368,18 @@ c.insets = paddingInsets; JScrollPane metricsScroll = new ThermostatScrollPane(metricsText); metricsPanel.add(metricsScroll, c); - c.fill = GridBagConstraints.BOTH; - c.gridx = 0; - c.gridy = 1; - c.weighty = yWeightRow2; - c.weightx = xWeightFullWidth; // add a panel to control selection of metrics time interval - RecentTimeControlPanel graphTimeControlPanel = new RecentTimeControlPanel(duration, RecentTimeControlPanel.UnitRange.MEDIUM); - graphTimeControlPanel.addPropertyChangeListener(RecentTimeControlPanel.PROPERTY_VISIBLE_TIME_RANGE, new PropertyChangeListener() { - @Override - public void propertyChange(final PropertyChangeEvent evt) { - duration = (Duration) evt.getNewValue(); - fireTabSelectedEvent(TabbedPaneAction.METRICS_TAB_SELECTED); - } - }); - - graphTimeControlPanel.setPreferredSize(buttonHolder.getPreferredSize()); - metricsPanel.add(graphTimeControlPanel, c); + updateGraphControlPanel(xWeightFullWidth, yWeightRow2); // graph tab + Insets spacerLeftInsets = new Insets(0, 5, 0, 0); graphMainPanel = new JPanel(); // add a panel to control display of the graph JPanel graphControlHolder = new JPanel(); - JPanel subHolder1 = new JPanel(); - JPanel subHolder2 = new JPanel(); - layout = new FlowLayout(); - layout.setAlignment(FlowLayout.RIGHT); - layout.setHgap(5); - layout.setVgap(0); - subHolder1.setLayout(layout); - subHolder1.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); // insert two labelled text fields to allow axis selection - JLabel xlabel = new JLabel("x:"); - JLabel ylabel = new JLabel("y:"); + JLabel xlabel = new JLabel(t.localize(LocaleResources.X_COORD).getContents()); + JLabel ylabel = new JLabel(t.localize(LocaleResources.Y_COORD).getContents()); final JTextField xtext = new JTextField(30); final JTextField ytext = new JTextField(30); @@ -454,15 +427,9 @@ } }); - subHolder1.add(generateGraphButton); - subHolder1.add(ytext); - subHolder1.add(ylabel); - subHolder1.add(xtext); - subHolder1.add(xlabel); - subHolder1.setAlignmentX(Component.RIGHT_ALIGNMENT); - JLabel filterlabel = new JLabel("filter:"); - JLabel valuelabel = new JLabel("=="); + JLabel filterlabel = new JLabel(t.localize(LocaleResources.FILTER).getContents()); + JLabel valuelabel = new JLabel(t.localize(LocaleResources.FILTER_VALUE_LABEL).getContents()); final JTextField filterText = new JTextField(30); final JTextField valueText = new JTextField(30); @@ -499,32 +466,74 @@ } }); - layout = new FlowLayout(); - layout.setAlignment(FlowLayout.RIGHT); - layout.setHgap(5); - layout.setVgap(0); - subHolder2.setLayout(layout); - subHolder2.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); - - subHolder2.add(valueText); - subHolder2.add(valuelabel); - subHolder2.add(filterText); - subHolder2.add(filterlabel); - subHolder2.setAlignmentX(Component.RIGHT_ALIGNMENT); - graphControlHolder.setLayout(new GridBagLayout()); - c.fill = GridBagConstraints.BOTH; - c.gridx = 0; - c.gridy = 0; - c.weighty = 0.5; - c.weightx = xWeightFullWidth; - graphControlHolder.add(subHolder1, c); - - c.gridx = 0; - c.gridy = 1; - c.weighty = 0.5; - c.weightx = xWeightFullWidth; - graphControlHolder.add(subHolder2, c); + + double weightShortLabel = 0.05; + double weightTextBox = 0.3; + GridBagConstraints graphConstraints = new GridBagConstraints(); + graphConstraints.fill = GridBagConstraints.BOTH; + graphConstraints.gridx = 0; + graphConstraints.gridy = 0; + graphConstraints.weighty = 0.5; + graphConstraints.weightx = weightShortLabel; + graphConstraints.insets = spacerLeftInsets; + graphControlHolder.add(xlabel, graphConstraints); + graphConstraints.fill = GridBagConstraints.HORIZONTAL; + graphConstraints.gridx = 1; + graphConstraints.gridy = 0; + graphConstraints.weighty = 0.5; + graphConstraints.weightx = weightTextBox; + graphConstraints.insets = spacerLeftInsets; + graphControlHolder.add(xtext, graphConstraints); + graphConstraints.fill = GridBagConstraints.HORIZONTAL; + graphConstraints.gridx = 2; + graphConstraints.gridy = 0; + graphConstraints.weighty = 0.5; + graphConstraints.weightx = weightShortLabel; + graphConstraints.insets = spacerLeftInsets; + graphControlHolder.add(ylabel, graphConstraints); + graphConstraints.fill = GridBagConstraints.HORIZONTAL; + graphConstraints.gridx = 3; + graphConstraints.gridy = 0; + graphConstraints.weighty = 0.5; + graphConstraints.weightx = weightTextBox; + graphConstraints.insets = spacerLeftInsets; + graphControlHolder.add(ytext, graphConstraints); + graphConstraints.fill = GridBagConstraints.HORIZONTAL; + graphConstraints.gridx = 0; + graphConstraints.gridy = 1; + graphConstraints.weighty = 0.5; + graphConstraints.weightx = weightShortLabel; + graphConstraints.insets = spacerLeftInsets; + graphControlHolder.add(filterlabel, graphConstraints); + graphConstraints.fill = GridBagConstraints.HORIZONTAL; + graphConstraints.gridx = 1; + graphConstraints.gridy = 1; + graphConstraints.weighty = 0.5; + graphConstraints.weightx = weightTextBox; + graphConstraints.insets = spacerLeftInsets; + graphControlHolder.add(filterText, graphConstraints); + graphConstraints.fill = GridBagConstraints.HORIZONTAL; + graphConstraints.gridx = 2; + graphConstraints.gridy = 1; + graphConstraints.weighty = 0.5; + graphConstraints.weightx = weightShortLabel; + graphConstraints.insets = spacerLeftInsets; + graphControlHolder.add(valuelabel, graphConstraints); + graphConstraints.fill = GridBagConstraints.HORIZONTAL; + graphConstraints.gridx = 3; + graphConstraints.gridy = 1; + graphConstraints.weighty = 0.5; + graphConstraints.weightx = weightTextBox; + graphConstraints.insets = spacerLeftInsets; + graphControlHolder.add(valueText, graphConstraints); + graphConstraints.fill = GridBagConstraints.HORIZONTAL; + graphConstraints.gridx = 4; + graphConstraints.gridy = 1; + graphConstraints.weighty = 0.5; + graphConstraints.weightx = weightTextBox; + graphConstraints.insets = spacerLeftInsets; + graphControlHolder.add(generateGraphButton, graphConstraints); // add controls and empty graph to main panel but don't add graph panel yet graphMainPanel.setLayout(new GridBagLayout()); @@ -536,18 +545,11 @@ graphMainPanel.add(graphControlHolder, c); CategoryDataset categoryDataset = new DefaultCategoryDataset(); - graph = ChartFactory.createBarChart("empty", "", "", + JFreeChart graph = ChartFactory.createBarChart(BYTEMAN_CHART_LABEL, EMPTY_STR, EMPTY_STR, categoryDataset, PlotOrientation.VERTICAL, true, true, false); - graphPanel = new ChartPanel(graph, true); - c.fill = GridBagConstraints.BOTH; - c.gridx = 0; - c.gridy = 1; - c.weighty = yWeightRow1 + yWeightRow2; - c.weightx = xWeightFullWidth; - graphMainPanel.add(graphPanel, c); - - graphMainPanel.revalidate(); + double weighty = yWeightRow1 + yWeightRow2; + updateGraphPanel(weighty, xWeightFullWidth, graph); tabbedPane = new ThermostatTabbedPane(); tabbedPane.addTab(t.localize(LocaleResources.TAB_RULES).getContents(), rulesPanel); @@ -576,6 +578,54 @@ mainContainer.setContent(tabbedPane); mainContainer.addToolBarButton(toggleButton); } + + private void updateGraphControlPanel(double weightx, double weighty) { + GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.BOTH; + c.gridx = 0; + c.gridy = 1; + c.weighty = weighty; + c.weightx = weightx; + + graphTimeControlPanel = new RecentTimeControlPanel(duration, RecentTimeControlPanel.UnitRange.MEDIUM); + graphTimeControlPanel.addPropertyChangeListener(RecentTimeControlPanel.PROPERTY_VISIBLE_TIME_RANGE, new PropertyChangeListener() { + @Override + public void propertyChange(final PropertyChangeEvent evt) { + duration = (Duration) evt.getNewValue(); + fireTabSelectedEvent(TabbedPaneAction.METRICS_TAB_SELECTED); + } + }); + + metricsPanel.add(graphTimeControlPanel, c); + metricsPanel.revalidate(); + } + + private void updateGraphPanel(final double weighty, + final double weightx, + JFreeChart graph) { + ThermostatChartPanelBuilder chartBuilder = new ThermostatChartPanelBuilder(); + graphPanel = chartBuilder + .duration(duration) + .chart(graph) + .xyPlotFixedAutoRange(false) + .unitRange(UnitRange.MEDIUM) + .build(); + graphPanel.addPropertyChangeListener(RecentTimeControlPanel.PROPERTY_VISIBLE_TIME_RANGE, new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + duration = (Duration) evt.getNewValue(); + fireGenerateEvent(GenerateAction.GENERATE_GRAPH); + } + }); + GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.BOTH; + c.gridx = 0; + c.gridy = 1; + c.weighty = weighty; + c.weightx = weightx; + graphMainPanel.add(graphPanel, c); + graphMainPanel.revalidate(); + } private void fireGenerateEvent(final GenerateAction action) { fireAction(action, generateListeners); @@ -684,6 +734,7 @@ @SuppressWarnings("unchecked") List<BytemanMetric> graphMetrics = (List<BytemanMetric>)event.getPayload(); updateGraphInView(graphMetrics, xkey, ykey, filter, value, graphtype); + updateMetricsRangeInView(); break; default: throw new AssertionError("Unknown event: " + action); @@ -691,6 +742,25 @@ } + // time range might have changed in graph view. update metrics + // accordingly + private void updateMetricsRangeInView() { + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + double weightx = 1.0; + double weighty = 0.05; + if (graphTimeControlPanel != null) { + graphTimeControlPanel.setVisible(false); + metricsPanel.remove(graphMainPanel); + } + updateGraphControlPanel(weightx, weighty); + } + + }); + } + private void updateRuleInView(final String rule) { SwingUtilities.invokeLater(new Runnable() { @Override @@ -786,7 +856,6 @@ SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - System.out.println("updateGraphInView:"); GraphDataset dataset = makeGraphDataset(ms, xk, yk, f, v); if (dataset != null) { switchGraph(dataset, t); @@ -809,20 +878,21 @@ String ylabel = dataset.getYLabel(); CoordinateType xtype = dataset.getXType(); CoordinateType ytype = dataset.getYType(); + JFreeChart graph = null; switch (xtype) { case CATEGORY: if(ytype == CoordinateType.CATEGORY) { // use a bar chart with multiple bars per category 1 value // where each bar counts the frequency for the second category CategoryDataset categoryDataset = dataset.getCategoryDataset(); - graph = ChartFactory.createBarChart("Byteman Metrics", xlabel, ylabel, + graph = ChartFactory.createBarChart(BYTEMAN_CHART_LABEL, xlabel, ylabel, categoryDataset, PlotOrientation.VERTICAL, true, true, false); } else { // draw as a bar chart with one bar per category where // each bar sums the associated second coordinate values CategoryDataset categoryDataset = dataset.getCategoryDataset(); - graph = ChartFactory.createBarChart("Byteman Metrics", xlabel, ylabel, + graph = ChartFactory.createBarChart(BYTEMAN_CHART_LABEL, xlabel, ylabel, categoryDataset, PlotOrientation.VERTICAL, true, true, false); /* @@ -842,7 +912,7 @@ // from numeric values to symbolic keys String[][] symbolsReturn = new String[1][]; XYDataset xydataset = dataset.getCategoryTimePlot(symbolsReturn); - graph = ChartFactory.createXYStepChart("Byteman Metrics", xlabel, ylabel, + graph = ChartFactory.createXYStepChart(BYTEMAN_CHART_LABEL, xlabel, ylabel, xydataset, PlotOrientation.VERTICAL, true, true, false); // now we change the range axis of the xyplot to draw symbols in @@ -851,7 +921,7 @@ } else { // draw a graph of numeric value against time XYDataset xydataset = dataset.getXYDataset(); - graph = ChartFactory.createTimeSeriesChart("Byteman Metrics", xlabel, ylabel, + graph = ChartFactory.createTimeSeriesChart(BYTEMAN_CHART_LABEL, xlabel, ylabel, xydataset, true, true, false); } break; @@ -861,7 +931,7 @@ // we could treat the numeric values as category values (or ranges?) // and draw this as a bar chart CategoryDataset categoryDataset = dataset.getCategoryDataset(); - graph = ChartFactory.createBarChart("Byteman Metrics", xlabel, ylabel, + graph = ChartFactory.createBarChart(BYTEMAN_CHART_LABEL, xlabel, ylabel, categoryDataset, PlotOrientation.VERTICAL, true, true, false); // for now draw an empty graph @@ -878,26 +948,21 @@ } else { // draw an xy line plot of numeric value against numeric value XYDataset xydataset = dataset.getXYDataset(); - graph = ChartFactory.createXYLineChart("Byteman Metrics", xlabel, ylabel, + graph = ChartFactory.createXYLineChart(BYTEMAN_CHART_LABEL, xlabel, ylabel, xydataset, PlotOrientation.VERTICAL, true, true, false); } break; } - if(graphPanel != null) { - graphPanel.setVisible(false); + if (graph == null) { + throw new AssertionError("Graph must not be null"); + } + if (graphPanel != null) { graphMainPanel.remove(graphPanel); } - graphPanel = new ChartPanel(graph, true); - GridBagConstraints c = new GridBagConstraints(); - c.fill = GridBagConstraints.BOTH; - c.gridx = 0; - c.gridy = 1; - c.weighty = 0.90; - c.weightx = 1.0; - graphMainPanel.add(graphPanel, c); - graphPanel.setVisible(true); - graphMainPanel.revalidate(); + double weighty = 0.95; + double weightx = 1.0; + updateGraphPanel(weighty, weightx, graph); } @Override @@ -934,352 +999,9 @@ }); } + @Override - public long getDurationMillisecs() - { + public long getDurationMillisecs() { return duration.asMilliseconds(); } - - public enum CoordinateType { - INTEGRAL, - REAL, - TIME, - CATEGORY - }; - - /** - * a special coordinate name used to identify the timestamp associated - * with any given Byteman metric. if it is entered as the x or y coordinate - * name in the graph dialogue then it will select the tiemstamp as the value - * to be graphed against tehthe other chosen coordinate. timestamp values - * are stored as longs but are displayed as time values. - * - * n.b. this text string really needs to be localised. - * - * n.b.b. it really only makes sense to use timestamp as the X axis. maybe - * we should reject any attempt to use it for the y axis? - */ - final public static String TIMESTAMP_KEY = "timestamp"; - - /** - * a special coordinate name used to identify the frequency count - * of any given Byteman metric. if it is entered as the x or y coordinate - * name in the graph dialogue then it will count 1 for each occurence of - * other value. frequency values are stored as longs. - * - * n.b. this text string really needs to be localised. - */ - final public static String FREQUENCY_KEY = "frequency"; - - /** - * a special coordinate name used to identify the marker string - * of any given Byteman metric. if it is entered as the x or y coordinate - * name in the graph dialogue then it will select the marker as the value - * to be graphed against the other chosen coordinate. - * - * n.b. this text string really needs to be localised. - */ - final public static String MARKER_KEY = "marker"; - - public static class GraphDataset - { - private List<Pair<Object, Object>> data; - String xkey; - String ykey; - CoordinateType xtype; - CoordinateType ytype; - private static CategoryDataset emptyCategoryDataset = new DefaultCategoryDataset(); - private static PieDataset emptyPieDataset = new DefaultPieDataset(); - private static XYDataset emptyXYDataset = new XYSeriesCollection(); - private static Number frequencyUnit = Long.valueOf(1); - - private Object maybeNumeric(String value) { - if (value == null || value.length() == 0) { - return null; - } - try { - if(value.contains(".")) { - return Double.valueOf(value); - } else { - return Long.valueOf(value); - } - } catch (NumberFormatException nfe) { - return value; - } - } - - public GraphDataset(List<BytemanMetric> metrics, String xkey, String ykey, String filter, String value) - { - this.xkey = xkey; - this.ykey = ykey; - xtype = CoordinateType.INTEGRAL; - ytype = CoordinateType.INTEGRAL; - data = new ArrayList<Pair<Object,Object>>(); - if (TIMESTAMP_KEY.equals(xkey)) { - xtype = CoordinateType.TIME; - } else if (FREQUENCY_KEY.equals(xkey)) { - xtype = CoordinateType.INTEGRAL; - } else if (MARKER_KEY.equals(xkey)) { - xtype = CoordinateType.CATEGORY; - } - if (TIMESTAMP_KEY.equals(ykey)) { - ytype = CoordinateType.TIME; - } else if (FREQUENCY_KEY.equals(ykey)) { - ytype = CoordinateType.INTEGRAL; - } else if (MARKER_KEY.equals(ykey)) { - ytype = CoordinateType.CATEGORY; - } - // if we have a filter value then convert it to a number if it is numeric - Object filterValue = value; - if (filter != null && value != null) { - // may need to convert String to Numeric - filterValue = maybeNumeric(value); - } - if (metrics != null) { - for (BytemanMetric m : metrics) { - Map<String, Object> map = m.getDataAsMap(); - // ensure that lookups for the timestamp key always retrieve - // the Long timestamp value associated with the metric and - // that lookups for the frequency key always retrieve - // the Long value 1. - map.put(TIMESTAMP_KEY, m.getTimeStamp()); - map.put(FREQUENCY_KEY, frequencyUnit); - map.put(MARKER_KEY, m.getMarker()); - // if we have a filter then check for presence of filter key - if (filter != null && filter.length() > 0) { - Object v = map.get(filter); - if (v == null) { - // skip this metric - continue; - } - if (filterValue != null) { - // may need to process String value as Numeric - if (v instanceof String) { - v = maybeNumeric((String)v); - } - if (!filterValue.equals(v)) { - // skip this metric - continue; - } - } - } - Object xval = map.get(xkey); - Object yval = map.get(ykey); - // only include records which contain values for both coordinates - if(xval != null && yval != null) { - // maybe re-present retrieved values as Numeric - // and/or downgrade coordinate type from INTEGRAL - // to REAL or even CATEGORY - xval = newCoordinate(xkey, xval, true); - yval = newCoordinate(ykey, yval, false); - data.add(new Pair<Object, Object>(xval, yval)); - } - } - } - } - - public int size() { - return data.size(); - } - - public XYDataset getXYDataset() - { - if (xtype == CoordinateType.CATEGORY || - ytype == CoordinateType.CATEGORY) { - return emptyXYDataset; - } - - XYSeries xyseries = new XYSeries(ykey + " against " + xkey); - - for (Pair<Object,Object> p : data) { - Number x = (Number)p.getFirst(); - Number y = (Number)p.getSecond(); - int idx = xyseries.indexOf(x); - if (idx >= 0) { - Number y1 = xyseries.getY(idx); - switch (ytype) { - case REAL: - y = y.doubleValue() + y1.doubleValue(); - default: - y = y.longValue() + y1.longValue(); - } - } - xyseries.add(x, y); - } - XYSeriesCollection xycollection = new XYSeriesCollection(); - xycollection.addSeries(xyseries); - return xycollection; - } - - public CategoryDataset getCategoryDataset() - { - if (xtype == CoordinateType.TIME) { - return emptyCategoryDataset; - } - DefaultCategoryDataset dataset = new DefaultCategoryDataset(); - // treat x values as category values by calling toString - // where they are numeric we ought to support binning them into ranges - switch (ytype) { - case CATEGORY: - // graph category against category by frequency - for (Pair<Object, Object> p : data) { - String first = p.getFirst().toString(); - String second = p.getSecond().toString(); - if(dataset.getRowKeys().contains(first) && dataset.getColumnKeys().contains(second)) { - dataset.incrementValue(1.0, first, second); - } else { - dataset.addValue(1.0, first, second); - } - } - break; - case TIME: - // bin time values into ranges and label range with start time - // for now just drop through to treat time value as numeric - default: - // graph category against numeric by summing numeric values - for (Pair<Object, Object> p : data) { - String first = p.getFirst().toString(); - String second = ""; - double increment = ((Number) p.getSecond()).doubleValue(); - if(dataset.getRowKeys().contains(first)) { - dataset.incrementValue(increment, first, second); - } else { - dataset.addValue(increment, first, second); - } - } - break; - } - return dataset; - } - - // alternative option for presenting category xkey with numeric ykey - public PieDataset getPieDataset() - { - if (xtype != CoordinateType.CATEGORY || ytype == CoordinateType.CATEGORY) { - return emptyPieDataset; - } - - DefaultKeyedValues keyedValues = new DefaultKeyedValues(); - - for (Pair<Object,Object> p : data) { - String first = p.getFirst().toString(); - double second = ((Number)p.getSecond()).doubleValue(); - int index = keyedValues.getIndex(first); - if (index >= 0) { - Number existing = keyedValues.getValue(first); - keyedValues.addValue(first, existing.doubleValue() + second); - } else { - keyedValues.addValue(first, second); - } - } - PieDataset pieDataset = new DefaultPieDataset(keyedValues); - return pieDataset; - } - - public XYDataset getCategoryTimePlot(String[][] symbolsReturn) - { - if (xtype != CoordinateType.TIME || ytype != CoordinateType.CATEGORY) { - return emptyXYDataset; - } - - // we need to display changing category state over time - // - // we can create an XYDataSet substituting numeric Y values - // to encode the category data. then we provide the data - // set with a range axis which displays the numeric - // values symbolically. - - XYSeries xyseries = new XYSeries(ykey + " against " + xkey); - int count = 0; - HashMap<String, Number> tickmap = new HashMap<String, Number>(); - - for (Pair<Object,Object> p : data) { - Number x = (Number)p.getFirst(); - String ysym = (String)p.getSecond(); - Number y = tickmap.get(ysym); - if (y == null) { - y = Long.valueOf(count++); - tickmap.put(ysym, y); - } - xyseries.add(x, y); - } - // populate key array - String[] symbols = new String[count]; - for (String key: tickmap.keySet()) { - int value = tickmap.get(key).intValue(); - symbols[value] = key; - } - - symbolsReturn[0] = symbols; - - XYSeriesCollection xycollection = new XYSeriesCollection(); - xycollection.addSeries(xyseries); - - return xycollection; - } - - public String getXLabel() { - return xkey; - } - - public String getYLabel() { - return ykey; - } - - public CoordinateType getXType() { - return xtype; - } - - public CoordinateType getYType() { - return ytype; - } - - /** - * process a newly read x or y coordinate value, which is either a Long timestanp or an unparsed - * numeric or category value String, returning a Long, parsed Numeric or String value. As a side - * effect of attempting to parse an input String the coordinate type for the relevant coordinate - * axis may be downgraded from INTEGRAL (assumed default) to DOUBLE or CATEGORY. - * @param key the label for the coordinate axis which may be the special value timestamp - * @param value the new found coordinate value which may be a Long timestamp or a String yet to be parsed - * @param isX true if this is an x coordinate value false if it is a y coordinate value - * @return an Object repreenting - */ - private Object newCoordinate(String key, Object value, boolean isX) { - - CoordinateType ctype = (isX ? xtype : ytype); - if (ctype == CoordinateType.TIME) { - // guaranteed already to be a Long - return value; - } - - boolean updateCType = false; - - if (value instanceof String && ctype != CoordinateType.CATEGORY) { - String str = (String)value; - // see if we can parse this as a number - try { - if (str.contains(".")) { - value = Double.valueOf(str); - if (ctype != CoordinateType.REAL) { - ctype = CoordinateType.REAL; - updateCType = true; - } - } else { - value = Long.valueOf(str); - } - } catch (NumberFormatException nfe) { - ctype = CoordinateType.CATEGORY; - updateCType = true; - } - } - if (updateCType) { - if (isX) { - xtype = ctype; - } else { - ytype = ctype; - } - } - return value; - } - } }
--- a/vm-byteman/client-swing/src/main/resources/com/redhat/thermostat/vm/byteman/client/swing/internal/strings.properties Fri Sep 16 10:25:53 2016 -0400 +++ b/vm-byteman/client-swing/src/main/resources/com/redhat/thermostat/vm/byteman/client/swing/internal/strings.properties Wed Sep 14 15:05:13 2016 +0200 @@ -13,3 +13,7 @@ LABEL_LOCAL_RULE = Local Rule LABEL_INJECTED_RULE = Injected Rule IMPORT_RULE = Import Rule from File +FILTER = Filter: +FILTER_VALUE_LABEL = == +X_COORD = x: +Y_COORD = y:
--- a/vm-byteman/client-swing/src/test/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanViewTest.java Fri Sep 16 10:25:53 2016 -0400 +++ b/vm-byteman/client-swing/src/test/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanViewTest.java Wed Sep 14 15:05:13 2016 +0200 @@ -55,19 +55,6 @@ import javax.swing.SwingUtilities; import javax.swing.text.JTextComponent; -import com.redhat.thermostat.client.command.RequestQueue; -import com.redhat.thermostat.common.ActionListener; -import com.redhat.thermostat.common.Clock; -import com.redhat.thermostat.storage.core.AgentId; -import com.redhat.thermostat.storage.core.HostRef; -import com.redhat.thermostat.storage.core.VmId; -import com.redhat.thermostat.storage.core.VmRef; -import com.redhat.thermostat.storage.dao.AgentInfoDAO; -import com.redhat.thermostat.storage.dao.VmInfoDAO; -import com.redhat.thermostat.storage.model.AgentInformation; -import com.redhat.thermostat.storage.model.VmInfo; -import com.redhat.thermostat.vm.byteman.client.swing.internal.VmBytemanView.GenerateAction; -import com.redhat.thermostat.vm.byteman.common.VmBytemanDAO; import org.fest.swing.annotation.GUITest; import org.fest.swing.core.NameMatcher; import org.fest.swing.edt.FailOnThreadViolationRepaintManager; @@ -85,12 +72,22 @@ import org.junit.runner.RunWith; import com.redhat.thermostat.annotations.internal.CacioTest; +import com.redhat.thermostat.client.command.RequestQueue; import com.redhat.thermostat.client.swing.components.ActionToggleButton; import com.redhat.thermostat.client.swing.components.Icon; import com.redhat.thermostat.common.ActionEvent; import com.redhat.thermostat.shared.locale.Translate; +import com.redhat.thermostat.storage.core.AgentId; +import com.redhat.thermostat.storage.core.HostRef; +import com.redhat.thermostat.storage.core.VmId; +import com.redhat.thermostat.storage.core.VmRef; +import com.redhat.thermostat.storage.dao.AgentInfoDAO; +import com.redhat.thermostat.storage.dao.VmInfoDAO; +import com.redhat.thermostat.storage.model.AgentInformation; +import com.redhat.thermostat.storage.model.VmInfo; import com.redhat.thermostat.vm.byteman.client.swing.internal.VmBytemanView.BytemanInjectState; import com.redhat.thermostat.vm.byteman.common.BytemanMetric; +import com.redhat.thermostat.vm.byteman.common.VmBytemanDAO; import net.java.openjdk.cacio.ctc.junit.CacioFESTRunner;