blob: d4f0a8f9052e07bbf4295afad5ab6b94e23901d9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014 Ericsson
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License 2.0 which
* accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Marc-Andre Laperle - Initial API and implementation
*******************************************************************************/
package org.eclipse.tracecompass.alltests.perf;
import java.io.FileWriter;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.test.internal.performance.PerformanceTestPlugin;
import org.eclipse.test.internal.performance.data.Dim;
import org.eclipse.test.internal.performance.db.DB;
import org.eclipse.test.internal.performance.db.Scenario;
import org.eclipse.test.internal.performance.db.SummaryEntry;
import org.eclipse.test.internal.performance.db.TimeSeries;
import org.eclipse.test.internal.performance.db.Variations;
import org.eclipse.tracecompass.alltests.Activator;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Test;
/**
* Convert results from the database to JSON suitable for display.
*
* Normal charts:
*
* Individual charts are generated into JSON files in the form chart#.json where
* # is incremented for each new chart. A chart contains data points consisting
* of X and Y values suitable for a line chart. Each point can also have
* additional data, for example the commit id. This format is compatible with
* nvd3. For example:
*
* <pre>
* <code>
* [{
* "key": "Experiment Benchmark:84 traces",
* "values": [{
* "label": {"commit": "fe3c142"},
* "x": 1405024320000,
* "y": 17592
* }]
* }]
* </code>
* </pre>
*
* Normal charts metadata:
*
* Each chart has an entry in the metada.js file which organizes the charts per
* component and contains additional information to augment the format expected
* by nvd3. Each entry contains the combination of OS and JVM, the filename (in
* JSON format), the title of the chart, the unit (seconds, etc) and the
* dimension (CPU time, used heap, etc).
*
* <pre>
* <code>
* var MetaData = {
* "applicationComponents": {
* "Experiment benchmark": {
* "name": "Experiment benchmark",
* "tests": [
* {
* "dimension": "CPU Time",
* "file": "chart12",
* "jvm": "1.7",
* "os": "linux",
* "title": "Experiment Benchmark:84 traces",
* "unit": "s"
* },
* {
* "dimension": "CPU Time",
* "file": "chart11",
* "jvm": "1.7",
* "os": "linux",
* "title": "Experiment Benchmark:6 traces",
* "unit": "s"
* },
* ...
* </code>
* </pre>
*
* Overview charts:
*
* In addition to the normal charts, overview charts are generated. An overview
* chart presents a summary of the scenarios ran for a given OS and JVM
* combination. Only scenarios marked as "global" are added to the overview
* because of space concerns. Overview charts are generated under the
* chart_overview#.json name and look similar in structure to the normal charts
* except that they contain more than one series.
*
* <pre>
* <code>
* [
* {
* "key": "CTF Read & Seek Benchmark (500 seeks):tr",
* "values": [
* {
* "label": {"commit": "4d34345"},
* "x": 1405436820000,
* "y": 5382.5
* },
* ...
* ]
* },
* {
* "key": "CTF Read Benchmark:trace-kernel",
* "values": [
* {
* "label": {"commit": "4d34345"},
* "x": 1405436820000,
* "y": 1311.5
* },
* ...
* ]
* },
* ...
* </code>
* </pre>
*
* Overview charts metadata:
*
* Overview charts also have similar metadata entries to normal charts except
* they are not organized by component.
*
* <pre>
* <code>
* var MetaData = {
* ...
* "overviews": {
* "1": {
* "dimension": "",
* "file": "chart_overview0",
* "jvm": "1.7",
* "os": "linux",
* "title": "linux / 1.7",
* "unit": ""
* },
* "2": {
* "dimension": "",
* "file": "chart_overview1",
* "jvm": "1.7",
* "os": "windows",
* "title": "windows / 1.7",
* "unit": ""
* },
* ...
* </code>
* </pre>
*
* Finally, since we want to be able to filter all the charts by OS/JVM
* combination, there is a section in the metadata that lists all the
* combinations:
*
* <pre>
* <code>
* "osjvm": {
* "1": {
* "description": "linux / 1.7",
* "jvm": "1.7",
* "os": "linux"
* },
* "2": {
* "description": "windows / 1.7",
* "jvm": "1.7",
* "os": "windows"
* },
* "3": {
* "description": "mac / 1.7",
* "jvm": "1.7",
* "os": "mac"
* }
* },
* </code>
* </pre>
*
* All of this data is meant to be view on a website. Specifically, the source
* code for our implementation is available on GitHub at
* https://github.com/PSRCode/ITCFYWebsite
*
* It makes use of the NVD3 project to display the charts based on the data
* generated by this class.
*/
public class PerfResultsToJSon {
/*
* Labels
*/
private static final String APPLICATION_COMPONENTS_LABEL = "applicationComponents";
private static final String BUILD_LABEL = "build";
private static final String COMMIT_LABEL = "commit";
private static final String CONFIG_LABEL = "config";
private static final String DESCRIPTION_LABEL = "description";
private static final String DIMENSION_LABEL = "dimension";
private static final String FILE_LABEL = "file";
private static final String HOST_LABEL = "host";
private static final String JVM_LABEL = "jvm";
private static final String KEY_LABEL = "key";
private static final String LABEL_LABEL = "label";
private static final String NAME_LABEL = "name";
private static final String OS_LABEL = "os";
private static final String OSJVM_LABEL = "osjvm";
private static final String OVERVIEWS_LABEL = "overviews";
private static final String TESTS_LABEL = "tests";
private static final String TITLE_LABEL = "title";
private static final String UNIT_LABEL = "unit";
private static final String VALUES_LABEL = "values";
private static final String X_LABEL = "x";
private static final String Y_LABEL = "y";
private static final String BUILD_DATE_FORMAT = "yyyyMMdd-HHmm";
private static final String OVERVIEW_CHART_FILE_NAME = "chart_overview";
private static final String METADATA_FILE_NAME = "meta";
private static final String METADATA_FILE_NAME_EXTENSION = ".js";
private static final String CHART_FILE_NAME = "chart";
private static final String CHART_FILE_NAME_EXTENSION = ".json";
private static final String WILDCARD_PATTERN = "%";
private static final @NonNull String COMPONENT_SEPARATOR = "#";
private static final String META_DATA_JAVASCRIPT_START = "var MetaData = ";
private static Pattern BUILD_DATE_PATTERN = Pattern.compile("(\\w+-\\w+)(-\\w+)?");
private static Pattern COMMIT_PATTERN = Pattern.compile(".*-.*-(.*)");
private JSONObject fApplicationComponents = new JSONObject();
private JSONObject fOverviews = new JSONObject();
private int fNumChart = 0;
private int fNumOverviewChart = 0;
/**
* Convert results from the database to JSON suitable for display
*
* <pre>
* For each variant (os/jvm combination)
* - For each summary entry (scenario)
* - Generate a chart
* - Add it to global summary (if needed)
* - Create the metadata for this test
* - Create an overview chart for this os/jvm
* </pre>
*
* @throws JSONException
* JSON error
* @throws IOException
* IO error
*/
@Test
public void parseResults() throws JSONException, IOException {
Variations configVariations = PerformanceTestPlugin.getVariations();
JSONObject osJvmVariants = createOsJvm();
Iterator<?> keysIt = osJvmVariants.keys();
while (keysIt.hasNext()) {
JSONArray overviewSummarySeries = new JSONArray();
JSONObject variant = osJvmVariants.getJSONObject((String) keysIt.next());
String seriesKey = PerformanceTestPlugin.BUILD;
// Clone the variations from the environment because it might have
// extra parameters like host=, etc.
Variations buildVariations = (Variations) configVariations.clone();
buildVariations.setProperty(JVM_LABEL, variant.getString(JVM_LABEL));
buildVariations.setProperty(CONFIG_LABEL, variant.getString(OS_LABEL));
buildVariations.setProperty(BUILD_LABEL, WILDCARD_PATTERN);
Scenario[] scenarios = DB.queryScenarios(buildVariations, WILDCARD_PATTERN, seriesKey, null);
SummaryEntry[] summaryEntries = DB.querySummaries(buildVariations, WILDCARD_PATTERN);
for (SummaryEntry entry : summaryEntries) {
Scenario scenario = getScenario(entry.scenarioName, scenarios);
JSONObject scenarioSeries = createScenarioChart(scenario, entry, buildVariations);
// Add to global summary
if (scenarioSeries != null && entry.isGlobal) {
overviewSummarySeries.put(scenarioSeries);
}
}
JSONObject overviewMetadata = createOverviewChart(overviewSummarySeries, buildVariations);
fOverviews.put(Integer.toString(fNumOverviewChart), overviewMetadata);
}
// Create the matadata javascript file that includes OS/JVM combinations
// (for filtering), application components and overviews (one of OS/JVM
// combination)
JSONObject rootMetadata = new JSONObject();
rootMetadata.put(OSJVM_LABEL, osJvmVariants);
rootMetadata.put(APPLICATION_COMPONENTS_LABEL, fApplicationComponents);
rootMetadata.put(OVERVIEWS_LABEL, fOverviews);
try (FileWriter fw1 = new FileWriter(METADATA_FILE_NAME + METADATA_FILE_NAME_EXTENSION)) {
fw1.write(META_DATA_JAVASCRIPT_START + rootMetadata.toString(4));
}
}
/**
* Create chart for a scenario instance and add it to the relevant metadatas
*
* @param scenario
* the scenario. For example,
* "CTF Read & Seek Benchmark (500 seeks)".
* @param entry
* an entry from the summary. Only scenarios that are part of the
* summary are processed.
* @param variations
* all variations to consider to create the scenario chart. For
* example build=%;jvm=1.7;config=linux will generate a chart for
* all builds on Linux / JVM 1.7
*
* @return
* @throws JSONException
* JSON error
* @throws IOException
* IO error
*/
private JSONObject createScenarioChart(Scenario scenario, SummaryEntry entry, Variations variations) throws JSONException, IOException {
if (scenario == null) {
return null;
}
String[] split = entry.scenarioName.split(COMPONENT_SEPARATOR);
if (split.length < 3) {
Activator.logError("Invalid scenario name \"" + entry.scenarioName + "\", it must be in format: org.package.foo#component#test");
return null;
}
// Generate individual chart
JSONArray rootScenario = new JSONArray();
JSONObject series = createSerie(scenario, variations, entry.shortName, entry.dimension);
rootScenario.put(series);
int numChart = fNumChart++;
try (FileWriter fw = new FileWriter(CHART_FILE_NAME + numChart + CHART_FILE_NAME_EXTENSION)) {
fw.write(rootScenario.toString(4));
}
// Create the metadata
JSONObject testMetadata = new JSONObject();
testMetadata.put(TITLE_LABEL, entry.shortName);
testMetadata.put(FILE_LABEL, CHART_FILE_NAME + numChart);
testMetadata.put(OS_LABEL, variations.getProperty(CONFIG_LABEL));
testMetadata.put(JVM_LABEL, variations.getProperty(JVM_LABEL));
testMetadata.put(DIMENSION_LABEL, entry.dimension.getName());
testMetadata.put(UNIT_LABEL, entry.dimension.getUnit().getShortName());
// Add the scenario to the metadata, under the correct component
String componentName = split[1];
JSONObject componentObject = null;
if (fApplicationComponents.has(componentName)) {
componentObject = fApplicationComponents.getJSONObject(componentName);
} else {
componentObject = new JSONObject();
componentObject.put(NAME_LABEL, componentName);
componentObject.put(TESTS_LABEL, new JSONArray());
fApplicationComponents.put(componentName, componentObject);
}
JSONArray tests = componentObject.getJSONArray(TESTS_LABEL);
tests.put(testMetadata);
return series;
}
/**
* Create an overview chart for this OS / JVM combination. The chart is made
* of multiple series (scenarios) that were marked as global.
*
* @param overviewSummarySeries
* an array of series to include in the chart (multiple
* scenarios)
* @param variations
* the variations used to generate the series to be included in
* this overview chart. For example build=%;jvm=1.7;config=linux
* will generate an overview chart for Linux / JVM 1.7
* @return the overview metadata JSON object
* @throws JSONException
* JSON error
* @throws IOException
* io error
*/
private JSONObject createOverviewChart(JSONArray overviewSummarySeries, Variations variations) throws IOException, JSONException {
int numOverviewChart = fNumOverviewChart++;
try (FileWriter fw = new FileWriter(OVERVIEW_CHART_FILE_NAME + numOverviewChart + CHART_FILE_NAME_EXTENSION)) {
fw.write(overviewSummarySeries.toString(4));
}
String os = variations.getProperty(CONFIG_LABEL);
String jvm = variations.getProperty(JVM_LABEL);
// Create the overview metadata
JSONObject overviewMetadata = new JSONObject();
overviewMetadata.put(TITLE_LABEL, os + " / " + jvm);
overviewMetadata.put(FILE_LABEL, OVERVIEW_CHART_FILE_NAME + numOverviewChart);
overviewMetadata.put(OS_LABEL, os);
overviewMetadata.put(JVM_LABEL, jvm);
overviewMetadata.put(DIMENSION_LABEL, StringUtils.EMPTY);
overviewMetadata.put(UNIT_LABEL, StringUtils.EMPTY);
return overviewMetadata;
}
private static Scenario getScenario(String scenarioName, Scenario[] scenarios) {
for (int i = 0; i < scenarios.length; i++) {
Scenario s = scenarios[i];
if (s.getScenarioName().equals(scenarioName)) {
return s;
}
}
return null;
}
/**
* Get all combinations of OS / JVM. This will be used for filtering.
*
* @return the JSON object containing all the combinations
* @throws JSONException
* JSON error
*/
private static JSONObject createOsJvm() throws JSONException {
JSONObject osjvm = new JSONObject();
List<String> oses = getDistinctOses();
int osJvmIndex = 1;
for (String os : oses) {
String key = JVM_LABEL;
Variations v = new Variations();
v.setProperty(BUILD_LABEL, WILDCARD_PATTERN);
v.setProperty(HOST_LABEL, WILDCARD_PATTERN);
v.setProperty(CONFIG_LABEL, os);
v.setProperty(JVM_LABEL, WILDCARD_PATTERN);
List<String> jvms = new ArrayList<>();
DB.queryDistinctValues(jvms, key, v, WILDCARD_PATTERN);
for (String jvm : jvms) {
JSONObject osjvmItem = new JSONObject();
osjvmItem.put(OS_LABEL, os);
osjvmItem.put(JVM_LABEL, jvm);
osjvmItem.put(DESCRIPTION_LABEL, os + " / " + jvm);
osjvm.put(Integer.toString(osJvmIndex), osjvmItem);
osJvmIndex++;
}
}
return osjvm;
}
/**
* Get all the distinct OS values
*
* @return the distinct OS values
*/
private static List<String> getDistinctOses() {
List<String> configs = new ArrayList<>();
String key = PerformanceTestPlugin.CONFIG;
Variations v = new Variations();
v.setProperty(WILDCARD_PATTERN, WILDCARD_PATTERN);
DB.queryDistinctValues(configs, key, v, WILDCARD_PATTERN);
return configs;
}
/**
* This main can be run from within Eclipse provided everything is on the
* class path.
*
* @param args
* the arguments
* @throws JSONException
* JSON error
* @throws IOException
* io error
*/
public static void main(String[] args) throws JSONException, IOException {
new PerfResultsToJSon().parseResults();
}
/**
* Create a series of data points for a given scenario through variations
*
* @param scenario
* the scenario. For example,
* "CTF Read & Seek Benchmark (500 seeks)".
* @param variations
* all variations to consider to create the series. For example
* build=%;jvm=1.7;config=linux will generate the series for all
* builds on Linux / JVM 1.7
* @param shortName
* the short name of the scenario
* @param dimension
* the dimension of interest (CPU time, used java heap, etc).
* @return the generated JSON object representing a series of data points
* for this scenario
* @throws JSONException
*/
private static JSONObject createSerie(Scenario scenario, Variations variations, String shortName, Dim dimension) throws JSONException {
JSONObject o = new JSONObject();
o.putOpt(KEY_LABEL, shortName);
o.putOpt(VALUES_LABEL, createDataPoints(scenario, variations, dimension));
return o;
}
/**
* Create data points for a given scenario and variations.
*
* @param s
* the scenario. For example,
* "CTF Read & Seek Benchmark (500 seeks)".
* @param variations
* all variations to consider to create the data points. For
* example build=%;jvm=1.7;config=linux will generate the data
* points for all builds on Linux / JVM 1.7
* @param dimension
* the dimension of interest (CPU time, used java heap, etc).
*
* @return the generated JSON array of points
* @throws JSONException
* JSON error
*/
private static JSONArray createDataPoints(Scenario s, Variations variations, Dim dimension) throws JSONException {
// Can be uncommented to see raw dump
//s.dump(System.out, PerformanceTestPlugin.BUILD);
String[] builds = DB.querySeriesValues(s.getScenarioName(), variations, PerformanceTestPlugin.BUILD);
Date[] dates = new Date[builds.length];
String[] commits = new String[builds.length];
for (int i = 0; i < builds.length; i++) {
dates[i] = parseBuildDate(builds[i]);
commits[i] = parseCommit(builds[i]);
}
TimeSeries timeSeries = s.getTimeSeries(dimension);
JSONArray dataPoints = new JSONArray();
int length = timeSeries.getLength();
for (int i = 0; i < length; i++) {
JSONObject point = new JSONObject();
if (dates[i] == null) {
continue;
}
point.put(X_LABEL, dates[i].getTime());
double value = 0;
if (timeSeries.getCount(i) > 0) {
value = timeSeries.getValue(i);
if (Double.isNaN(value)) {
value = 0;
}
}
point.put(Y_LABEL, value);
dataPoints.put(point);
point.put(LABEL_LABEL, createLabel(commits[i]));
}
return dataPoints;
}
/**
* Create a label JSONObject which is used to attach more information to a
* data point.
*
* @param commit
* the commit id for this data point
* @return the resulting JSON object
* @throws JSONException
* JSON error
*/
private static JSONObject createLabel(String commit) throws JSONException {
/*
* Here we could add more information about this specific data point
* like the commit author, the commit message, etc.
*/
JSONObject label = new JSONObject();
if (commit != null && !commit.isEmpty()) {
label.put(COMMIT_LABEL, commit);
}
return label;
}
/**
* Get the commit id out of the build= string
*
* @param build
* the build string
* @return the parsed commit id
*/
private static String parseCommit(String build) {
Matcher matcher = COMMIT_PATTERN.matcher(build);
if (matcher.matches()) {
return matcher.group(1);
}
return null;
}
/**
* Get the Date out of the build= string
*
* @param build
* the build string
* @return the parsed Date
*/
private static Date parseBuildDate(String build) {
Matcher matcher = BUILD_DATE_PATTERN.matcher(build);
Date date = null;
if (matcher.matches()) {
String dateStr = matcher.group(1);
SimpleDateFormat f = new SimpleDateFormat(BUILD_DATE_FORMAT);
try {
date = dateStr.length() > BUILD_DATE_FORMAT.length() ?
f.parse(dateStr.substring(dateStr.length() - BUILD_DATE_FORMAT.length())) :
f.parse(dateStr);
} catch (ParseException e) {
return null;
}
}
return date;
}
}