You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jmeter.apache.org by pm...@apache.org on 2016/02/08 12:50:42 UTC

svn commit: r1729145 [1/2] - in /jmeter/trunk: bin/report-template/content/js/ src/core/org/apache/jmeter/report/config/ src/core/org/apache/jmeter/report/dashboard/ src/core/org/apache/jmeter/report/processor/graph/ xdocs/ xdocs/usermanual/

Author: pmouawad
Date: Mon Feb  8 11:50:41 2016
New Revision: 1729145

URL: http://svn.apache.org/viewvc?rev=1729145&view=rev
Log:
Bug 58932 - Report / Dashboard: Document clearly and log what report are not generated when saveservice options are not correct
#resolve #112
Bugzilla Id: 58932

Modified:
    jmeter/trunk/bin/report-template/content/js/graph.js.fmkr
    jmeter/trunk/src/core/org/apache/jmeter/report/config/ReportGeneratorConfiguration.java
    jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/AbstractDataExporter.java
    jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/HtmlTemplateExporter.java
    jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/ReportGenerator.java
    jmeter/trunk/src/core/org/apache/jmeter/report/processor/graph/AbstractGraphConsumer.java
    jmeter/trunk/xdocs/changes.xml
    jmeter/trunk/xdocs/devguide-dashboard.xml
    jmeter/trunk/xdocs/usermanual/generating-dashboard.xml

Modified: jmeter/trunk/bin/report-template/content/js/graph.js.fmkr
URL: http://svn.apache.org/viewvc/jmeter/trunk/bin/report-template/content/js/graph.js.fmkr?rev=1729145&r1=1729144&r2=1729145&view=diff
==============================================================================
--- jmeter/trunk/bin/report-template/content/js/graph.js.fmkr (original)
+++ jmeter/trunk/bin/report-template/content/js/graph.js.fmkr Mon Feb  8 11:50:41 2016
@@ -119,21 +119,17 @@ function prepareOptions(options, data) {
         var xOffset = options.xaxis.mode === "time" ? ${(timeZoneOffset?c)!0} : 0;
         var yOffset = options.yaxis.mode === "time" ? ${(timeZoneOffset?c)!0} : 0;
 
-        var minX = extraOptions.minX;
-        if(minX !== undefined)
-            options.xaxis.min = minX + xOffset;
-
-        var maxX = extraOptions.maxX;
-        if(maxX !== undefined)
-            options.xaxis.max = maxX + xOffset;
-
-        var minY = extraOptions.minY;
-        if(minY !== undefined)
-            options.yaxis.min = minY + yOffset;
-
-        var maxY = extraOptions.maxY;
-        if(maxY !== undefined)
-            options.yaxis.max = maxY + yOffset;
+        if(!isNaN(extraOptions.minX))
+        	options.xaxis.min = parseFloat(extraOptions.minX) + xOffset;
+        
+        if(!isNaN(extraOptions.maxX))
+        	options.xaxis.max = parseFloat(extraOptions.maxX) + xOffset;
+        
+        if(!isNaN(extraOptions.minY))
+        	options.yaxis.min = parseFloat(extraOptions.minY) + yOffset;
+        
+        if(!isNaN(extraOptions.maxY))
+        	options.yaxis.max = parseFloat(extraOptions.maxY) + yOffset;
     }
 }
 

Modified: jmeter/trunk/src/core/org/apache/jmeter/report/config/ReportGeneratorConfiguration.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/report/config/ReportGeneratorConfiguration.java?rev=1729145&r1=1729144&r2=1729145&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/report/config/ReportGeneratorConfiguration.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/report/config/ReportGeneratorConfiguration.java Mon Feb  8 11:50:41 2016
@@ -79,7 +79,7 @@ public class ReportGeneratorConfiguratio
 
     // Title
     public static final String GRAPH_KEY_TITLE = "title";
-    public static final String GRAPH_KEY_TITLE_DEFAULT = "Generic graph title";
+    public static final String GRAPH_KEY_TITLE_DEFAULT = "";
 
     // Required exporter properties
     // Filters only sample series ?
@@ -91,8 +91,8 @@ public class ReportGeneratorConfiguratio
     public static final String EXPORTER_KEY_SERIES_FILTER_DEFAULT = "";
 
     // Show controllers only
-    private static final String EXPORTER_KEY_SHOW_CONTROLLERS_ONLY = "show_controllers_only";
-    private static final Boolean EXPORTER_KEY_SHOW_CONTROLLERS_ONLY_DEFAULT = Boolean.FALSE;
+    public static final String EXPORTER_KEY_SHOW_CONTROLLERS_ONLY = "show_controllers_only";
+    public static final Boolean EXPORTER_KEY_SHOW_CONTROLLERS_ONLY_DEFAULT = Boolean.FALSE;
 
     // Optional exporter properties
     public static final String EXPORTER_KEY_GRAPH_EXTRA_OPTIONS = "graph_options";

Modified: jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/AbstractDataExporter.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/AbstractDataExporter.java?rev=1729145&r1=1729144&r2=1729145&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/AbstractDataExporter.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/AbstractDataExporter.java Mon Feb  8 11:50:41 2016
@@ -17,6 +17,11 @@
  */
 package org.apache.jmeter.report.dashboard;
 
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jmeter.report.processor.MapResultData;
+import org.apache.jmeter.report.processor.ResultData;
+import org.apache.jmeter.report.processor.ValueResultData;
+
 /**
  * The Class AbstractDataExporter provides a base class for DataExporter.
  */
@@ -30,6 +35,66 @@ public abstract class AbstractDataExport
     protected AbstractDataExporter() {
     }
 
+    /**
+     * Finds a value matching the specified data name in a ResultData tree.
+     * Supports only MapResultData walking.
+     * 
+     * @param clazz
+     *            the type of the value
+     * @param data
+     *            the name of the data containing the value
+     * @param root
+     *            the root of the tree
+     * @return the value matching the data name
+     */
+    protected static <TValue> TValue findValue(Class<TValue> clazz, String data,
+            ResultData root) {
+        TValue value = null;
+        ResultData result = findData(data, root);
+        if (result instanceof ValueResultData) {
+            ValueResultData valueResult = (ValueResultData) result;
+            Object object = valueResult.getValue();
+            if (object != null && clazz.isAssignableFrom(object.getClass())) {
+                value = clazz.cast(object);
+            }
+        }
+        return value;
+    }
+
+    /**
+     * Finds a inner ResultData matching the specified data name in a ResultData
+     * tree. Supports only MapResultData walking.
+     * 
+     * @param data
+     *            the name of the data containing the value
+     * @param root
+     *            the root of the tree
+     * @return the ResultData matching the data name
+     */
+    protected static ResultData findData(String data, ResultData root) {
+        ResultData result = null;
+        String[] pathItems = StringUtils.split(data, '.');
+        if (pathItems != null) {
+            if (root instanceof MapResultData) {
+                int count = pathItems.length;
+                int index = 0;
+                MapResultData map = (MapResultData) root;
+                while (map != null && index < count && result == null) {
+                    ResultData current = map.getResult(pathItems[index]);
+                    if (index == count - 1) {
+                        result = current;
+                    } else {
+                        if (current instanceof MapResultData) {
+                            map = (MapResultData) current;
+                            index++;
+                        }
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
     /*
      * (non-Javadoc)
      * 

Modified: jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/HtmlTemplateExporter.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/HtmlTemplateExporter.java?rev=1729145&r1=1729144&r2=1729145&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/HtmlTemplateExporter.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/HtmlTemplateExporter.java Mon Feb  8 11:50:41 2016
@@ -24,20 +24,26 @@ import java.util.Map;
 import java.util.TimeZone;
 
 import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.Validate;
 import org.apache.jmeter.report.config.ConfigurationException;
 import org.apache.jmeter.report.config.ExporterConfiguration;
+import org.apache.jmeter.report.config.GraphConfiguration;
 import org.apache.jmeter.report.config.SubConfiguration;
 import org.apache.jmeter.report.config.ReportGeneratorConfiguration;
 import org.apache.jmeter.report.core.DataContext;
 import org.apache.jmeter.report.core.TimeHelper;
+import org.apache.jmeter.report.processor.ListResultData;
 import org.apache.jmeter.report.processor.MapResultData;
 import org.apache.jmeter.report.processor.ResultData;
 import org.apache.jmeter.report.processor.ResultDataVisitor;
 import org.apache.jmeter.report.processor.SampleContext;
 import org.apache.jmeter.report.processor.ValueResultData;
+import org.apache.jmeter.report.processor.graph.AbstractGraphConsumer;
 import org.apache.jorphan.logging.LoggingManager;
 import org.apache.log.Logger;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 
 import freemarker.template.Configuration;
 import freemarker.template.TemplateExceptionHandler;
@@ -70,10 +76,12 @@ public class HtmlTemplateExporter extend
     public static final String TIMESTAMP_FORMAT_MS = "ms";
     private static final String INVALID_TEMPLATE_DIRECTORY_FMT = "\"%s\" is not a valid template directory";
     private static final String INVALID_PROPERTY_CONFIG_FMT = "Wrong property \"%s\" in \"%s\" export configuration";
+    private static final String EMPTY_GRAPH_FMT = "The graph \"%s\" will be empty : %s";
 
     // Template directory
     private static final String TEMPLATE_DIR = "template_dir";
-    private static final File TEMPLATE_DIR_DEFAULT = new File("report-template");
+    private static final File TEMPLATE_DIR_DEFAULT = new File(
+            "report-template");
 
     // Output directory
     private static final String OUTPUT_DIR = "output_dir";
@@ -86,22 +94,196 @@ public class HtmlTemplateExporter extend
         context.put(key, value);
     }
 
+    /**
+     * This class allows to customize data before exporting them
+     *
+     */
     private interface ResultCustomizer {
         ResultData customizeResult(ResultData result);
     }
 
+    /**
+     * This class allows to inject graph_options properties to the exported data
+     *
+     */
+    private class ExtraOptionsResultCustomizer implements ResultCustomizer {
+        private SubConfiguration extraOptions;
+
+        /**
+         * Sets the extra options to inject in the result data
+         * 
+         * @param extraOptions
+         */
+        public final void setExtraOptions(SubConfiguration extraOptions) {
+            this.extraOptions = extraOptions;
+        }
+
+        /*
+         * (non-Javadoc)
+         * 
+         * @see org.apache.jmeter.report.dashboard.HtmlTemplateExporter.
+         * ResultCustomizer#customizeResult(org.apache.jmeter.report.processor.
+         * ResultData)
+         */
+        @Override
+        public ResultData customizeResult(ResultData result) {
+            MapResultData customizedResult = new MapResultData();
+            customizedResult.setResult(DATA_CTX_RESULT, result);
+            if (extraOptions != null) {
+                MapResultData extraResult = new MapResultData();
+                for (Map.Entry<String, String> extraEntry : extraOptions
+                        .getProperties().entrySet()) {
+                    extraResult.setResult(extraEntry.getKey(),
+                            new ValueResultData(extraEntry.getValue()));
+                }
+                customizedResult.setResult(DATA_CTX_EXTRA_OPTIONS, extraResult);
+            }
+            return customizedResult;
+        }
+
+    }
+
+    /**
+     * This class allows to check exported data
+     *
+     */
+    private interface ResultChecker {
+        void checkResult(ResultData result);
+    }
+
+    /**
+     * This class allows to detect empty graphs
+     *
+     */
+    private class EmptyGraphChecker implements ResultChecker {
+
+        private final boolean filtersOnlySampleSeries;
+        private final boolean showControllerSeriesOnly;
+        private final Pattern filterPattern;
+
+        private boolean excludesControllers;
+        private String graphId;
+
+        public final void setExcludesControllers(boolean excludesControllers) {
+            this.excludesControllers = excludesControllers;
+        }
+
+        public final void setGraphId(String graphId) {
+            this.graphId = graphId;
+        }
+
+        /**
+         * Instantiates a new EmptyGraphChecker.
+         * 
+         * @param filtersOnlySampleSeries
+         * @param showControllerSeriesOnly
+         * @param filterPattern
+         */
+        public EmptyGraphChecker(boolean filtersOnlySampleSeries,
+                boolean showControllerSeriesOnly, Pattern filterPattern) {
+            this.filtersOnlySampleSeries = filtersOnlySampleSeries;
+            this.showControllerSeriesOnly = showControllerSeriesOnly;
+            this.filterPattern = filterPattern;
+        }
+
+        /*
+         * (non-Javadoc)
+         * 
+         * @see
+         * org.apache.jmeter.report.dashboard.HtmlTemplateExporter.ResultChecker
+         * #checkResult(org.apache.jmeter.report.processor.ResultData)
+         */
+        @Override
+        public void checkResult(ResultData result) {
+            Boolean supportsControllerDiscrimination = findValue(Boolean.class,
+                    AbstractGraphConsumer.RESULT_SUPPORTS_CONTROLLERS_DISCRIMINATION,
+                    result);
+
+            String message = null;
+            if (supportsControllerDiscrimination && showControllerSeriesOnly
+                    && excludesControllers) {
+                // Exporter shows controller series only
+                // whereas the current graph support controller
+                // discrimination and excludes
+                // controllers
+                message = ReportGeneratorConfiguration.EXPORTER_KEY_SHOW_CONTROLLERS_ONLY
+                        + " is set while the graph excludes controllers.";
+            } else {
+                if (filterPattern != null) {
+                    // Detect whether none series matches
+                    // the series filter.
+                    ResultData seriesResult = findData(
+                            AbstractGraphConsumer.RESULT_SERIES, result);
+                    if (seriesResult instanceof ListResultData) {
+
+                        // Try to find at least one pattern matching
+                        ListResultData seriesList = (ListResultData) seriesResult;
+                        int count = seriesList.getSize();
+                        int index = 0;
+                        boolean matches = false;
+                        while (index < count && !matches) {
+                            ResultData currentResult = seriesList.get(index);
+                            if (currentResult instanceof MapResultData) {
+                                MapResultData seriesData = (MapResultData) currentResult;
+                                String name = findValue(String.class,
+                                        AbstractGraphConsumer.RESULT_SERIES_NAME,
+                                        seriesData);
+
+                                // Is the current series a controller series ?
+                                boolean isController = findValue(Boolean.class,
+                                        AbstractGraphConsumer.RESULT_SERIES_IS_CONTROLLER,
+                                        seriesData).booleanValue();
+
+                                matches = filterPattern.matcher(name).matches();
+                                if (matches) {
+                                    // If the name matches pattern, other
+                                    // properties can discard the series
+                                    matches = !filtersOnlySampleSeries
+                                            || !supportsControllerDiscrimination
+                                            || isController
+                                            || !showControllerSeriesOnly;
+                                } else {
+                                    // If the name does not match the pattern,
+                                    // other properties can hold the series
+                                    matches = filtersOnlySampleSeries
+                                            && !supportsControllerDiscrimination;
+                                }
+                            }
+                            index++;
+                        }
+                        if (!matches) {
+                            // None series matches the pattern
+                            message = "None series matches the "
+                                    + ReportGeneratorConfiguration.EXPORTER_KEY_SERIES_FILTER;
+                        }
+                    }
+                }
+            }
+
+            // Log empty graph when needed.
+            if (message != null) {
+                LOG.warn(String.format(EMPTY_GRAPH_FMT, graphId, message));
+            }
+        }
+    }
+
     private <TVisit> void addResultToContext(String resultKey,
             Map<String, Object> storage, DataContext dataContext,
             ResultDataVisitor<TVisit> visitor) {
-        addResultToContext(resultKey, storage, dataContext, visitor, null);
+        addResultToContext(resultKey, storage, dataContext, visitor, null,
+                null);
     }
 
     private <TVisit> void addResultToContext(String resultKey,
             Map<String, Object> storage, DataContext dataContext,
-            ResultDataVisitor<TVisit> visitor, ResultCustomizer customizer) {
+            ResultDataVisitor<TVisit> visitor, ResultCustomizer customizer,
+            ResultChecker checker) {
         Object data = storage.get(resultKey);
         if (data instanceof ResultData) {
             ResultData result = (ResultData) data;
+            if (checker != null) {
+                checker.checkResult(result);
+            }
             if (customizer != null) {
                 result = customizer.customizeResult(result);
             }
@@ -120,12 +302,12 @@ public class HtmlTemplateExporter extend
 
     private <TProperty> TProperty getPropertyFromConfig(SubConfiguration cfg,
             String property, TProperty defaultValue, Class<TProperty> clazz)
-            throws ExportException {
+                    throws ExportException {
         try {
             return cfg.getProperty(property, defaultValue, clazz);
         } catch (ConfigurationException ex) {
-            throw new ExportException(String.format(
-                    INVALID_PROPERTY_CONFIG_FMT, property, getName()), ex);
+            throw new ExportException(String.format(INVALID_PROPERTY_CONFIG_FMT,
+                    property, getName()), ex);
         }
     }
 
@@ -170,23 +352,36 @@ public class HtmlTemplateExporter extend
 
         // Add the flag defining whether only sample series are filtered to the
         // context
+        final boolean filtersOnlySampleSeries = exportCfg
+                .filtersOnlySampleSeries();
         addToContext(DATA_CTX_FILTERS_ONLY_SAMPLE_SERIES,
-                Boolean.valueOf(exportCfg.filtersOnlySampleSeries()), dataContext);
+                Boolean.valueOf(filtersOnlySampleSeries), dataContext);
 
         // Add the series filter to the context
-        addToContext(DATA_CTX_SERIES_FILTER, exportCfg.getSeriesFilter(),
-                dataContext);
+        final String seriesFilter = exportCfg.getSeriesFilter();
+        Pattern filterPattern = null;
+        if (StringUtils.isNotBlank(seriesFilter)) {
+            try {
+                filterPattern = Pattern.compile(seriesFilter);
+            } catch (PatternSyntaxException ex) {
+                LOG.error(String.format("Invalid series filter: \"%s\", %s",
+                        seriesFilter, ex.getDescription()));
+            }
+        }
+        addToContext(DATA_CTX_SERIES_FILTER, seriesFilter, dataContext);
 
         // Add the flag defining whether only controller series are displayed
+        final boolean showControllerSeriesOnly = exportCfg
+                .showControllerSeriesOnly();
         addToContext(DATA_CTX_SHOW_CONTROLLERS_ONLY,
-                Boolean.valueOf(exportCfg.showControllerSeriesOnly()), dataContext);
+                Boolean.valueOf(showControllerSeriesOnly), dataContext);
 
         JsonizerVisitor jsonizer = new JsonizerVisitor();
         Map<String, Object> storedData = context.getData();
 
         // Add begin date consumer result to the data context
-        addResultToContext(ReportGenerator.BEGIN_DATE_CONSUMER_NAME,
-                storedData, dataContext, jsonizer);
+        addResultToContext(ReportGenerator.BEGIN_DATE_CONSUMER_NAME, storedData,
+                dataContext, jsonizer);
 
         // Add end date summary consumer result to the data context
         addResultToContext(ReportGenerator.END_DATE_CONSUMER_NAME, storedData,
@@ -210,31 +405,26 @@ public class HtmlTemplateExporter extend
 
         // Collect graph results from sample context and transform them into
         // Json strings to inject in the data context
-        for (String graphId : configuration.getGraphConfigurations().keySet()) {
+        ExtraOptionsResultCustomizer customizer = new ExtraOptionsResultCustomizer();
+        EmptyGraphChecker checker = new EmptyGraphChecker(
+                filtersOnlySampleSeries, showControllerSeriesOnly,
+                filterPattern);
+        for (Map.Entry<String, GraphConfiguration> graphEntry : configuration
+                .getGraphConfigurations().entrySet()) {
+            final String graphId = graphEntry.getKey();
+            final GraphConfiguration graphConfiguration = graphEntry.getValue();
             final SubConfiguration extraOptions = exportCfg
                     .getGraphExtraConfigurations().get(graphId);
-            addResultToContext(graphId, storedData, dataContext, jsonizer,
-                    new ResultCustomizer() {
 
-                        @Override
-                        public ResultData customizeResult(ResultData result) {
-                            MapResultData customizedResult = new MapResultData();
-                            customizedResult.setResult(DATA_CTX_RESULT,result);
-                            if (extraOptions != null) {
-                                MapResultData extraResult = new MapResultData();
-                                for (Map.Entry<String, String> extraEntry : extraOptions
-                                        .getProperties().entrySet()) {
-                                    extraResult.setResult(
-                                            extraEntry.getKey(),
-                                            new ValueResultData(extraEntry
-                                                    .getValue()));
-                                }
-                                customizedResult.setResult(
-                                        DATA_CTX_EXTRA_OPTIONS, extraResult);
-                            }
-                            return customizedResult;
-                        }
-                    });
+            // Initialize customizer and checker
+            customizer.setExtraOptions(extraOptions);
+            checker.setExcludesControllers(
+                    graphConfiguration.excludesControllers());
+            checker.setGraphId(graphId);
+
+            // Export graph data
+            addResultToContext(graphId, storedData, dataContext, jsonizer,
+                    customizer, checker);
         }
 
         // Replace the begin date with its formatted string and store the old
@@ -262,13 +452,15 @@ public class HtmlTemplateExporter extend
                 Configuration.getVersion());
         try {
             templateCfg.setDirectoryForTemplateLoading(templateDirectory);
-            templateCfg
-                    .setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
-            LOG.info("Report will be generated in:"+outputDir.getAbsolutePath()+", creating folder structure");
+            templateCfg.setTemplateExceptionHandler(
+                    TemplateExceptionHandler.RETHROW_HANDLER);
+            LOG.info(
+                    "Report will be generated in:" + outputDir.getAbsolutePath()
+                            + ", creating folder structure");
             FileUtils.forceMkdir(outputDir);
             TemplateVisitor visitor = new TemplateVisitor(
-                    templateDirectory.toPath(), outputDir.toPath(),
-                    templateCfg, dataContext);
+                    templateDirectory.toPath(), outputDir.toPath(), templateCfg,
+                    dataContext);
             Files.walkFileTree(templateDirectory.toPath(), visitor);
         } catch (IOException ex) {
             throw new ExportException("Unable to process template files.", ex);

Modified: jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/ReportGenerator.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/ReportGenerator.java?rev=1729145&r1=1729144&r2=1729145&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/ReportGenerator.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/report/dashboard/ReportGenerator.java Mon Feb  8 11:50:41 2016
@@ -276,6 +276,9 @@ public class ReportGenerator {
             Object obj = clazz.newInstance();
             AbstractGraphConsumer graph = (AbstractGraphConsumer) obj;
             graph.setName(graphName);
+            
+            // Set the graph title
+            graph.setTitle(graphConfiguration.getTitle());
 
             // Set graph properties using reflection
             Method[] methods = clazz.getMethods();

Modified: jmeter/trunk/src/core/org/apache/jmeter/report/processor/graph/AbstractGraphConsumer.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/core/org/apache/jmeter/report/processor/graph/AbstractGraphConsumer.java?rev=1729145&r1=1729144&r2=1729145&view=diff
==============================================================================
--- jmeter/trunk/src/core/org/apache/jmeter/report/processor/graph/AbstractGraphConsumer.java (original)
+++ jmeter/trunk/src/core/org/apache/jmeter/report/processor/graph/AbstractGraphConsumer.java Mon Feb  8 11:50:41 2016
@@ -74,6 +74,7 @@ public abstract class AbstractGraphConsu
     public static final String RESULT_MAX_X = "maxX";
     public static final String RESULT_MIN_Y = "minY";
     public static final String RESULT_MAX_Y = "maxY";
+    public static final String RESULT_TITLE = "title";
     public static final String RESULT_SUPPORTS_CONTROLLERS_DISCRIMINATION = "supportsControllersDiscrimination";
 
     public static final String RESULT_SERIES = "series";
@@ -106,6 +107,9 @@ public abstract class AbstractGraphConsu
     /** Renders percentiles in the results. */
     private boolean renderPercentiles;
 
+    /** The title of the graph. */
+    private String title;
+
     /**
      * Gets the group information.
      *
@@ -202,6 +206,25 @@ public abstract class AbstractGraphConsu
     }
 
     /**
+     * Gets the title of the graph.
+     * 
+     * @return the title of the graph
+     */
+    public final String getTitle() {
+        return title;
+    }
+
+    /**
+     * Sets the title of the graph.
+     * 
+     * @param title
+     *            the title to set
+     */
+    public final void setTitle(String title) {
+        this.title = title;
+    }
+
+    /**
      * Instantiates a new abstract graph consumer.
      */
     protected AbstractGraphConsumer() {
@@ -215,14 +238,16 @@ public abstract class AbstractGraphConsu
 
     private void setMinResult(MapResultData result, String name, Double value) {
         ValueResultData valueResult = (ValueResultData) result.getResult(name);
-        valueResult.setValue(Double.valueOf(Math.min(((Double) valueResult.getValue()).doubleValue(), 
-                value.doubleValue())));
+        valueResult.setValue(Double.valueOf(
+                Math.min(((Double) valueResult.getValue()).doubleValue(),
+                        value.doubleValue())));
     }
 
     private void setMaxResult(MapResultData result, String name, Double value) {
         ValueResultData valueResult = (ValueResultData) result.getResult(name);
-        valueResult.setValue(Double.valueOf(Math.max(((Double) valueResult.getValue()).doubleValue(), 
-                value.doubleValue())));
+        valueResult.setValue(Double.valueOf(
+                Math.max(((Double) valueResult.getValue()).doubleValue(),
+                        value.doubleValue())));
     }
 
     /**
@@ -250,8 +275,9 @@ public abstract class AbstractGraphConsu
         int size = seriesList.getSize();
         while (seriesResult == null && index < size) {
             MapResultData currSeries = (MapResultData) seriesList.get(index);
-            String name = String.valueOf(((ValueResultData) currSeries
-                    .getResult(RESULT_SERIES_NAME)).getValue());
+            String name = String.valueOf(
+                    ((ValueResultData) currSeries.getResult(RESULT_SERIES_NAME))
+                            .getValue());
             if (Objects.equals(name, series)) {
                 seriesResult = currSeries;
             }
@@ -261,12 +287,14 @@ public abstract class AbstractGraphConsu
         // Create series result if not found
         if (seriesResult == null) {
             seriesResult = new MapResultData();
-            seriesResult.setResult(RESULT_SERIES_NAME, new ValueResultData(
-                    series));
+            seriesResult.setResult(RESULT_SERIES_NAME,
+                    new ValueResultData(series));
             seriesResult.setResult(RESULT_SERIES_IS_CONTROLLER,
-                    new ValueResultData(Boolean.valueOf(seriesData.isControllersSeries())));
+                    new ValueResultData(
+                            Boolean.valueOf(seriesData.isControllersSeries())));
             seriesResult.setResult(RESULT_SERIES_IS_OVERALL,
-                    new ValueResultData(Boolean.valueOf(seriesData.isOverallSeries())));
+                    new ValueResultData(
+                            Boolean.valueOf(seriesData.isOverallSeries())));
             seriesResult.setResult(RESULT_SERIES_DATA, new ListResultData());
             seriesList.addResult(seriesResult);
         }
@@ -278,7 +306,8 @@ public abstract class AbstractGraphConsu
         Map<Double, Aggregator> aggInfo;
         if (aggregated) {
             aggInfo = new HashMap<>();
-            aggInfo.put(Double.valueOf(seriesData.getKeysAggregator().getResult()),
+            aggInfo.put(
+                    Double.valueOf(seriesData.getKeysAggregator().getResult()),
                     seriesData.getValuesAggregator());
         } else {
             aggInfo = seriesData.getAggregatorInfo();
@@ -321,7 +350,8 @@ public abstract class AbstractGraphConsu
                     double percentile = (double) rank / 10;
                     while (percentile < percent) {
                         ListResultData coordResult = new ListResultData();
-                        coordResult.addResult(new ValueResultData(Double.valueOf(percentile)));
+                        coordResult.addResult(new ValueResultData(
+                                Double.valueOf(percentile)));
                         coordResult.addResult(new ValueResultData(value));
                         dataResult.addResult(coordResult);
                         percentile = (double) ++rank / 10;
@@ -341,7 +371,8 @@ public abstract class AbstractGraphConsu
                     while (percentile < percent) {
                         ListResultData coordResult = new ListResultData();
                         coordResult.addResult(new ValueResultData(value));
-                        coordResult.addResult(new ValueResultData(Double.valueOf(percentile)));
+                        coordResult.addResult(new ValueResultData(
+                                Double.valueOf(percentile)));
                         dataResult.addResult(coordResult);
                         percentile = (double) ++rank / 10;
                     }
@@ -396,10 +427,15 @@ public abstract class AbstractGraphConsu
 
     private MapResultData createResult() {
         MapResultData result = new MapResultData();
-        result.setResult(RESULT_MIN_X, new ValueResultData(Double.valueOf(Double.MAX_VALUE)));
-        result.setResult(RESULT_MAX_X, new ValueResultData(Double.valueOf(Double.MIN_VALUE)));
-        result.setResult(RESULT_MIN_Y, new ValueResultData(Double.valueOf(Double.MAX_VALUE)));
-        result.setResult(RESULT_MAX_Y, new ValueResultData(Double.valueOf(Double.MIN_VALUE)));
+        result.setResult(RESULT_MIN_X,
+                new ValueResultData(Double.valueOf(Double.MAX_VALUE)));
+        result.setResult(RESULT_MAX_X,
+                new ValueResultData(Double.valueOf(Double.MIN_VALUE)));
+        result.setResult(RESULT_MIN_Y,
+                new ValueResultData(Double.valueOf(Double.MAX_VALUE)));
+        result.setResult(RESULT_MAX_Y,
+                new ValueResultData(Double.valueOf(Double.MIN_VALUE)));
+        result.setResult(RESULT_TITLE, new ValueResultData(getTitle()));
         result.setResult(RESULT_SERIES, new ListResultData());
 
         boolean supportsControllersDiscrimination = true;
@@ -409,7 +445,8 @@ public abstract class AbstractGraphConsu
                     .allowsControllersDiscrimination();
         }
         result.setResult(RESULT_SUPPORTS_CONTROLLERS_DISCRIMINATION,
-                new ValueResultData(Boolean.valueOf(supportsControllersDiscrimination)));
+                new ValueResultData(
+                        Boolean.valueOf(supportsControllersDiscrimination)));
 
         initializeExtraResults(result);
         return result;
@@ -463,20 +500,22 @@ public abstract class AbstractGraphConsu
             boolean aggregatedKeysSeries = groupInfo
                     .enablesAggregatedKeysSeries();
 
-            for (String seriesName : groupInfo.getSeriesSelector().select(
-                    sample)) {
+            for (String seriesName : groupInfo.getSeriesSelector()
+                    .select(sample)) {
                 Map<String, SeriesData> seriesInfo = groupData.getSeriesInfo();
                 SeriesData seriesData = seriesInfo.get(seriesName);
                 if (seriesData == null) {
                     seriesData = new SeriesData(factory, aggregatedKeysSeries,
                             groupInfo.getSeriesSelector()
-                                    .allowsControllersDiscrimination() ? sample
-                                    .isController() : false, false);
+                                    .allowsControllersDiscrimination()
+                                            ? sample.isController() : false,
+                            false);
                     seriesInfo.put(seriesName, seriesData);
                 }
 
                 // Get the value to aggregate and dispatch it to the groupData
-                double value = groupInfo.getValueSelector().select(seriesName, sample);
+                double value = groupInfo.getValueSelector().select(seriesName,
+                        sample);
 
                 aggregateValue(factory, seriesData, key, value);
                 if (overallSeries) {

Modified: jmeter/trunk/xdocs/changes.xml
URL: http://svn.apache.org/viewvc/jmeter/trunk/xdocs/changes.xml?rev=1729145&r1=1729144&r2=1729145&view=diff
==============================================================================
--- jmeter/trunk/xdocs/changes.xml (original)
+++ jmeter/trunk/xdocs/changes.xml Mon Feb  8 11:50:41 2016
@@ -296,6 +296,7 @@ Summary
     <li><bug>58913</bug>When closing jmeter should not interpret cancel as "destroy my test plan". Contributed by Benoit Wiart (benoit dot wiart at gmail.com)</li>
     <li><bug>58952</bug>Report/Dashboard: Generation of aggregated series in graphs does not work. Developed by Florent Sabbe (f dot sabbe at ubik-ingenierie.com) and contributed by Ubik-Ingenierie</li>
     <li><bug>58931</bug>New Report/Dashboard : Getting font errors under Firefox and Chrome (not Safari)</li>
+    <li><bug>58932</bug>Report / Dashboard: Document clearly and log what report are not generated when saveservice options are not correct. Developed by Florent Sabbe (f dot sabbe at ubik-ingenierie.com) and contributed by Ubik-Ingenierie</li>
 </ul>
 
  <!--  =================== Thanks =================== -->

Modified: jmeter/trunk/xdocs/devguide-dashboard.xml
URL: http://svn.apache.org/viewvc/jmeter/trunk/xdocs/devguide-dashboard.xml?rev=1729145&r1=1729144&r2=1729145&view=diff
==============================================================================
--- jmeter/trunk/xdocs/devguide-dashboard.xml (original)
+++ jmeter/trunk/xdocs/devguide-dashboard.xml Mon Feb  8 11:50:41 2016
@@ -1,14 +1,14 @@
 <?xml version="1.0"?>
 <!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor 
-  license agreements. See the NOTICE file distributed with this work for additional 
-  information regarding copyright ownership. The ASF licenses this file to 
-  You under the Apache License, Version 2.0 (the "License"); you may not use 
-  this file except in compliance with the License. You may obtain a copy of 
-  the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required 
-  by applicable law or agreed to in writing, software distributed under the 
-  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 
-  OF ANY KIND, either express or implied. See the License for the specific 
-  language governing permissions and limitations under the License. -->
+	license agreements. See the NOTICE file distributed with this work for additional 
+	information regarding copyright ownership. The ASF licenses this file to 
+	You under the Apache License, Version 2.0 (the "License"); you may not use 
+	this file except in compliance with the License. You may obtain a copy of 
+	the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required 
+	by applicable law or agreed to in writing, software distributed under the 
+	License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 
+	OF ANY KIND, either express or implied. See the License for the specific 
+	language governing permissions and limitations under the License. -->
 
 <!DOCTYPE document[
 <!ENTITY hellip   "&#x02026;" >
@@ -16,184 +16,331 @@
 
 <document id="$Id$">
 
-  <properties>
-    <title>Developer's guide: Dashboard generator</title>
-  </properties>
-
-  <body>
-
-    <section name="Dashboard generator">
-      <p>
-        This document describes the architecture and operation of the
-        dashboard generation engine.
-      </p>
-      <subsection name="1 Overview" anchor="overview">
-        <subsection name="1.1 Architecture" anchor="overview_architecture">
-          <p>
-            The dashboard generation engine is a modular feature based on
-            samples operation processes.
-            <br />
-            The processes can be represented by the following diagram:
-          </p>
-          <figure image="dashboard.png">Figure 1 - Dashboard generation overview</figure>
-          <p>
-            In this view, you can see:
-            <ul>
-              <li>
-                A source from where samples are produced (e.g. CSV file).
-              </li>
-              <li>
-                A chain of items, named consumers, that operate
-                on the samples that go through the chain
-                (e.g. Filtering, sorting, calculation, ...).
-              </li>
-              <li>
-                An execution context, named sample context, where the results
-                of consumers calculations are stored.
-              </li>
-              <li>
-                A set of items, named exporters, that use the content of the
-                sample context to generate a final result to the user (e.g.
-                HTML page generation).
-              </li>
-            </ul>
-          </p>
-        </subsection>
-        <subsection name="1.2 Operation" anchor="overview_operation">
-          <p>
-            Before producing samples, the source is associated with a sample
-            context that will be used to store the consumers results.
-          </p>
-          <p>
-            Then a chain of consumers is built using JMeter properties
-            (prefixed by
-            <code>jmeter.reportgenerator</code>
-            ) in order to enable the user to customize it.
-          </p>
-          <p>
-            When the source emits a sample, it sends it to the first consumer
-            of the chain.
-            <br />
-            The consumer can have different behaviors:
-            <ul>
-              <li>It can process the sample and send it to the next
-                consumers.</li>
-              <li>It cannot process the sample, so it stores it and
-                continues to receive other samples. When it can process the
-                stored samples, it does so and sends the whole to the next
-                consumers (e.g. sorting).</li>
-              <li>It can choose to discard the sample (e.g.
-                filtering).</li>
-            </ul>
-            When the source stops producing samples, consumers can publish a
-            result in the sample context.
-            <br />
-            The latter is send to the set of exporters in order to create
-            results used by final user.
-          </p>
-        </subsection>
-      </subsection>
-
-      <subsection name="2 Consumers chain details" anchor="consumers_chain">
-        <p>
-
-        </p>
-        <figure image="chain.png">Figure 2 - Consumers chain</figure>
-        <p>
-          The chain begins with a normalizer consumer in charge of
-          standardizing the timestamp of each sample because JMeter allows
-          different timestamp formats (See
-          <code>jmeter.save.saveservice.timestamp_format</code>
-          ).
-        </p>
-        <p>
-          Then two consumers have to define the start time and end time of
-          the load tests.
-        </p>
-        <p>
-          At the same level a filter consumer keeps or
-          discards samples depending on the
-          <code>jmeter.reportgenerator.sample_filter</code>
-          property.
-        </p>
-        <p> Another filter is plugged after to discard controller
-          samples.
-        </p>
-        <p>
-          Depending on the property
-          <code>jmeter.reportgenerator.graph.&lt;graph_id&gt;.exclude_controllers</code>
-          , the graph consumer matching the <code>graph_id</code> identifier will be
-          set at position <code>A</code> or <code>B</code>.
-        </p>
-      </subsection>
-
-      <subsection name="3 Limitations and Outlooks" anchor="outlooks">
-        <ul>
-          <li>
-            <p>Till now, there is only one sample source implementation which
-              is strongly coupled with the CSV file format, we should allow
-              other kinds of source by using a sample source interface.</p>
-          </li>
-          <li>
-            <p>To add customized graph, users must extend the
-              <code>AbstractGraphConsumer</code> or use one of the implementations provided
-              in the package <code>org.apache.jmeter.report.processor.graph.impl</code>”.
-              This could be enhanced by making concrete the base class and give
-              public access to additional properties (like selectors). But first
-              we have to resolve the issue of shared properties (e.g. over time
-              graphs must dispatch the same granularity property to the keys
-              selector and time rate aggregator).</p>
-          </li>
-          <li>
-            <p>
-              The chain building is dispatched between the
-              <code>org.apache.jmeter.report.dashboard.ReporGenerator.generate</code> method
-              and the implementation of the consumers. So the code in charge of
-              the building is split and furthermore some consumers can be
-              redundant and harm the performance of report generation, not
-              load testing.
-            </p>
-            <p>
-              E.g. Each <code>LatencyVSRequestGraphConsumer</code> and
-              <code>ResponseTimeVSRequestGraphConsumer</code> instances use an embedded
-              consumer that could be shared depending on <code>granularity</code> and
-              <code>exclude_controllers</code> properties.
-            </p>
-            <p>
-              So we should enable the consumers to define the chain they
-              require and provide a single chain builder that processes these
-              chain requirements to instantiate needed consumers on demand. I.e.
-              for the same chain requirement declaration, the same consumer
-              instances are used. Otherwise if the declaration differs, a new
-              branch of consumers is created.
-            </p>
-          </li>
-          <li>
-            <p>
-              The graphs (DOM elements) in the generated HTML page should be
-              dynamically build in order to match the graphs defined in JMeter
-              properties.
-            </p>
-          </li>
-          <li>
-            <p>
-              Some improvements can be done on the generated html pages:
-              <ul>
-                <li>Using a single page, and hide graphs depending on the
-                  navigation menu selection.</li>
-                <li>Adding a loading animation when graphs are build or
-                  refreshed.</li>
-                <li>Let the user determine if a graph is zoomable using a JMeter
-                  property.</li>
-                <li>Using the <code>jquery.plot.setData()</code> method to handle series
-                  activation/deactivation rather than rebuild the graph.</li>
-              </ul>
-            </p>
-          </li>
-        </ul>
-      </subsection>
-    </section>
+	<properties>
+		<title>Developer's guide: Dashboard generator</title>
+	</properties>
 
-  </body>
+	<body>
+
+		<section name="Dashboard generator">
+			<p>
+				This document describes the architecture and operation of the
+				dashboard generation engine.
+			</p>
+			<subsection name="1 Overview" anchor="overview">
+				<subsection name="1.1 Architecture" anchor="overview_architecture">
+					<p>
+						The dashboard generation engine is a modular feature based on
+						samples operation processes.
+						<br />
+						The processes can be represented by the following diagram:
+					</p>
+					<figure image="dashboard.png">Figure 1 - Dashboard generation overview</figure>
+					<p>
+						In this view, you can see:
+						<ul>
+							<li>
+								A source from where samples are produced (e.g. CSV file).
+							</li>
+							<li>
+								A chain of items, named consumers, that operate
+								on the samples
+								that go through the chain
+								(e.g. Filtering, sorting, calculation,
+								...).
+							</li>
+							<li>
+								An execution context, named sample context, where the results
+								of consumers calculations are stored.
+							</li>
+							<li>
+								A set of items, named exporters, that use the content of the
+								sample context to generate a final result to the user (e.g.
+								HTML
+								page generation).
+							</li>
+						</ul>
+					</p>
+				</subsection>
+				<subsection name="1.2 Operation" anchor="overview_operation">
+					<p>
+						Before producing samples, the source is associated with a sample
+						context that will be used to store the consumers results.
+					</p>
+					<p>
+						Then a chain of consumers is built using JMeter properties
+						(prefixed by
+						<code>jmeter.reportgenerator</code>
+						) in order to enable the user to customize it.
+					</p>
+					<p>
+						When the source emits a sample, it sends it to the first consumer
+						of the chain.
+						<br />
+						The consumer can have different behaviors:
+						<ul>
+							<li>It can process the sample and send it to the next
+								consumers.</li>
+							<li>It cannot process the sample, so it stores it and
+								continues to
+								receive other samples. When it can process the
+								stored samples, it
+								does so and sends the whole to the next
+								consumers (e.g. sorting).</li>
+							<li>It can choose to discard the sample (e.g.
+								filtering).</li>
+						</ul>
+						When the source stops producing samples, consumers can publish a
+						result in the sample context.
+						<br />
+						The latter is send to the set of exporters in order to create
+						results used by final user.
+					</p>
+				</subsection>
+			</subsection>
+
+			<subsection name="2 Consumers chain details" anchor="consumers_chain">
+				<p>
+
+				</p>
+				<figure image="chain.png">Figure 2 - Consumers chain</figure>
+				<p>
+					The chain begins with a normalizer consumer in charge of
+					standardizing the timestamp of each sample because JMeter allows
+					different timestamp formats (See
+					<code>jmeter.save.saveservice.timestamp_format</code>
+					).
+				</p>
+				<p>
+					Then two consumers have to define the start time and end time of
+					the load tests.
+				</p>
+				<p>
+					At the same level a filter consumer keeps or
+					discards samples
+					depending on the
+					<code>jmeter.reportgenerator.sample_filter</code>
+					property.
+				</p>
+				<p> Another filter is plugged after to discard controller
+					samples.
+				</p>
+				<p>
+					Depending on the property
+					<code>jmeter.reportgenerator.graph.&lt;graph_id&gt;.exclude_controllers</code>
+					, the graph consumer matching the
+					<code>graph_id</code>
+					identifier will be
+					set at position
+					<code>A</code>
+					or
+					<code>B</code>
+					.
+				</p>
+			</subsection>
+
+			<subsection name="3 Template processing" anchor="process_template">
+				<subsection name="3.1 Overview" anchor="template_overview">
+					<p>
+						The default exporter of the generator use the template engine
+						<a href="http://freemarker.org/">freemarker</a>
+						to produce html pages.
+						<br />
+						Templated files are located in the template
+						directory defined by
+						the JMeter property
+						"jmeter.reportgenerator.template_dir"
+						and have
+						the extension ".fmkr".
+					</p>
+					<p>
+						The graph references in the templated
+						files use the syntax :
+						${&lt;graph_id&gt;.&lt;value&gt;} where :
+						<ul>
+							<li>"graph_id"
+								is the identifier of the graph matching the jmeter
+								properties
+								definition</li>
+							<li>"value" is the name of the value where data are stored.</li>
+						</ul>
+					</p>
+					<p>
+						Each graph produces the following values :
+						<ul>
+							<li>
+								<p>
+									<code>maxX</code>
+									:
+									<br />
+									The maximum abscissa of the graph (double).
+								</p>
+							</li>
+							<li>
+								<p>
+									<code>maxY</code>
+									:
+									<br />
+									The maximum ordinate of the graph (double).
+								</p>
+							</li>
+							<li>
+								<p>
+									<code>minX</code>
+									:
+									<br />
+									The minimum abscissa of the graph
+									(double).
+								</p>
+							</li>
+							<li>
+								<p>
+									<code>minY</code>
+									:
+									<br />
+									The maximum ordinate
+									of the graph (double).
+								</p>
+							</li>
+							<li>
+								<p>
+									<code>title</code>
+									:
+									<br />
+									The
+									title of the graph (string).
+								</p>
+							</li>
+							<li>
+								<p>
+									<code>values</code>
+									:
+									<br />
+									A json object representing the data of the graph series
+									(string).
+								</p>
+							</li>
+						</ul>
+					</p>
+					<!-- <note> -->
+					<!-- Graph can provide some other values, see -->
+					<!-- <a href="#default_graphs">Default -->
+					<!-- graph section</a> -->
+					<!-- to get the list of these values for default graphs. -->
+					<!-- </note> -->
+				</subsection>
+				<subsection name="3.2 Customization" anchor="template_customization">
+					<p>You can customize the dashboard generation by modifying the
+						files in the
+						template directory.
+					</p>
+					<p>
+						If you want to add a graph to the dahsboard,
+						you have to
+						<a href="#configure_graph">declare it among the JMeter properties</a>
+						and use its references in the templated files.
+					</p>
+					<p>If you want to remove
+						a graph from the dashboard, you must remove
+						all its references in
+						the templated
+						files and clear JMeter
+						properties.</p>
+				</subsection>
+			</subsection>
+
+			<subsection name="4 Limitations and Outlooks" anchor="outlooks">
+				<ul>
+					<li>
+						<p>Till now, there is only one sample source implementation which
+							is strongly coupled with the CSV file format, we should allow
+							other kinds of source by using a sample source interface.</p>
+					</li>
+					<li>
+						<p>
+							To add customized graph, users must extend the
+							<code>AbstractGraphConsumer</code>
+							or use one of the implementations provided
+							in the package
+							<code>org.apache.jmeter.report.processor.graph.impl</code>
+							”.
+							This could be enhanced by making concrete the base class and
+							give
+							public access to additional properties (like selectors). But
+							first
+							we have to resolve the issue of shared properties (e.g. over
+							time
+							graphs must dispatch the same granularity property to the
+							keys
+							selector and time rate aggregator).
+						</p>
+					</li>
+					<li>
+						<p>
+							The chain building is dispatched between the
+							<code>org.apache.jmeter.report.dashboard.ReporGenerator.generate</code>
+							method
+							and the implementation of the consumers. So the code in
+							charge of
+							the building is split and furthermore some consumers can
+							be
+							redundant and harm the performance of report generation, not
+							load testing.
+						</p>
+						<p>
+							E.g. Each
+							<code>LatencyVSRequestGraphConsumer</code>
+							and
+							<code>ResponseTimeVSRequestGraphConsumer</code>
+							instances use an embedded
+							consumer that could be shared depending
+							on
+							<code>granularity</code>
+							and
+							<code>exclude_controllers</code>
+							properties.
+						</p>
+						<p>
+							So we should enable the consumers to define the chain they
+							require and provide a single chain builder that processes these
+							chain requirements to instantiate needed consumers on demand.
+							I.e.
+							for the same chain requirement declaration, the same consumer
+							instances are used. Otherwise if the declaration differs, a new
+							branch of consumers is created.
+						</p>
+					</li>
+					<li>
+						<p>
+							The graphs (DOM elements) in the generated HTML page should be
+							dynamically build in order to match the graphs defined in JMeter
+							properties.
+						</p>
+					</li>
+					<li>
+						<p>
+							Some improvements can be done on the generated html pages:
+							<ul>
+								<li>Using a single page, and hide graphs depending on the
+									navigation menu selection.</li>
+								<li>Adding a loading animation when graphs are build or
+									refreshed.</li>
+								<li>Let the user determine if a graph is zoomable using a JMeter
+									property.</li>
+								<li>
+									Using the
+									<code>jquery.plot.setData()</code>
+									method to handle series
+									activation/deactivation rather than
+									rebuild the graph.
+								</li>
+							</ul>
+						</p>
+					</li>
+				</ul>
+			</subsection>
+		</section>
+
+	</body>
 </document>