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 "…" >
@@ -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.<graph_id>.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.<graph_id>.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 :
+ ${<graph_id>.<value>} 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>