/*
* -----------------------------------------------------------------------\
* PerfCake
*
* Copyright (C) 2010 - 2016 the original author or authors.
*
* Licensed 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.
* -----------------------------------------------------------------------/
*/
package org.perfcake.reporting.destination.c3chart;
import org.perfcake.PerfCakeException;
import org.perfcake.common.PeriodType;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Compiles the resulting chart report combiting all possible charts created now or during previous runs in the same target location.
*
* @author <a href="mailto:marvenec@gmail.com">Martin Večeřa</a>
*/
public class C3ChartReport {
/**
* Prefix of the files of the charts created as a combination of measured results.
*/
private static final String COMBINED_PREFIX = "combined_";
/**
* File counter for the stored combined chart.
*/
private static int fileCounter = 1;
/**
* Creates the final report in the given location.
*
* @param target
* Root path to an existing chart report.
* @param mainChart
* The chart that was added during this run of the performance test.
* @param autoCombine
* True if we should combine the new results with the previous reports.
* @throws PerfCakeException
* When it was not possible to create the report.
*/
static void createReport(final Path target, final C3Chart mainChart, final boolean autoCombine) throws PerfCakeException {
final File outputDir = Paths.get(target.toString(), "data").toFile();
final List<C3Chart> charts = new ArrayList<>();
charts.add(mainChart);
try {
deletePreviousCombinedCharts(outputDir);
final File[] files = outputDir.listFiles(new DescriptionFileFilter());
if (files != null) {
final List<File> descriptionFiles = Arrays.asList(files);
for (final File f : descriptionFiles) {
final C3Chart c = new C3ChartDataFile(f).getChart();
if (!c.getBaseName().equals(mainChart.getBaseName())) {
charts.add(c);
}
}
}
charts.sort(Comparator.comparingLong(C3Chart::getCreated));
if (autoCombine) {
charts.addAll(analyzeMatchingCharts(target, charts));
}
} catch (final IOException e) {
throw new PerfCakeException("Unable to parse stored results: ", e);
}
C3ChartHtmlTemplates.writeIndex(target, charts);
}
/**
* Deletes all previously generated chart combinations. We are going to refresh them.
*
* @param descriptionsDirectory
* The directory with existing generated charts.
* @throws IOException
* When it was not possible to delete any of the charts.
*/
private static void deletePreviousCombinedCharts(final File descriptionsDirectory) throws IOException {
final StringBuilder issues = new StringBuilder();
final File[] files = descriptionsDirectory.listFiles(new CombinedJsFileFilter());
if (files != null) {
for (final File f : files) {
if (!f.delete()) {
issues.append(String.format("Cannot delete file %s. %n", f.getAbsolutePath()));
}
}
}
if (issues.length() > 0) {
throw new IOException(issues.toString());
}
}
/**
* Finds all the attributes among the given charts that has a match. I.e. that are present at least in two of the charts.
*
* @param charts
* The charts for inspection.
* @return A map of chart group to a list of attributes that are present at least twice among the charts.
*/
private static Map<String, List<String>> findMatchingAttributes(final List<C3Chart> charts) {
final Map<String, List<String>> seen = new HashMap<>();
final Map<String, List<String>> result = new HashMap<>();
for (final C3Chart c : charts) {
if (!seen.containsKey(c.getGroup())) {
seen.put(c.getGroup(), new ArrayList<>());
}
if (!result.containsKey(c.getGroup())) {
result.put(c.getGroup(), new ArrayList<>());
}
for (final String attribute : c.getAttributes()) {
if (seen.get(c.getGroup()).contains(attribute) && !result.get(c.getGroup()).contains(attribute)) {
result.get(c.getGroup()).add(attribute);
} else {
seen.get(c.getGroup()).add(attribute);
}
}
result.get(c.getGroup()).remove(C3Chart.COLUMN_TIME);
result.get(c.getGroup()).remove(C3Chart.COLUMN_ITERATION);
result.get(c.getGroup()).remove(C3Chart.COLUMN_PERCENT);
}
return result;
}
/**
* Generates new charts based on the matching attributes of existing charts.
*
* @param charts
* Existing chart for inspection.
* @return The list of newly created charts.
* @throws PerfCakeException
* When it was not possible to store any of the charts.
*/
static List<C3Chart> analyzeMatchingCharts(final Path target, final List<C3Chart> charts) throws PerfCakeException {
final Map<String, List<String>> matches = findMatchingAttributes(charts);
final List<C3Chart> newCharts = new ArrayList<>();
for (final Map.Entry<String, List<String>> entry : matches.entrySet()) {
for (final String match : entry.getValue()) {
final List<C3Chart> matchingCharts = new ArrayList<>();
PeriodType xAxisType = null;
boolean compatible = true; // all charts have compatible xAxisType
for (final C3Chart c : charts) {
if (entry.getKey().equals(c.getGroup()) && c.getAttributes().contains(match)) {
if (xAxisType == null) {
xAxisType = c.getxAxisType();
matchingCharts.add(c);
} else if (c.getxAxisType() == xAxisType) {
matchingCharts.add(c);
} else {
compatible = false;
}
}
}
if (compatible) { // there are charts with different xAxisType, we won't combine them
newCharts.add(combineCharts(target, match, matchingCharts));
}
}
}
return newCharts;
}
/**
* Combines the charts in the target path according to the matching attribute. That means a new chart containing
* the given attribute from all the charts generated in the given location is generated.
*
* @param target
* Root path to an existing chart report.
* @param matchingAttribute
* The name of the attribute present in all of the charts.
* @param matchingCharts
* The charts to be combined.
* @return The newly created chart meta-data.
* @throws PerfCakeException
* When it was not possible to write the new chart data.
*/
private static C3Chart combineCharts(final Path target, final String matchingAttribute, final List<C3Chart> matchingCharts) throws PerfCakeException {
final C3Chart newChart = new C3Chart();
final List<String> attributes = new ArrayList<>();
attributes.add(matchingCharts.get(0).getAttributes().get(0));
attributes.addAll(matchingCharts.stream().map(ch -> String.format("%s (%s)", ch.getName(), C3ChartHtmlTemplates.getCreatedAsString(ch))).collect(Collectors.toList()));
newChart.setBaseName(COMBINED_PREFIX + fileCounter++);
newChart.setxAxisType(matchingCharts.get(0).getxAxisType());
newChart.setxAxis(matchingCharts.get(0).getxAxis());
newChart.setyAxis(matchingCharts.get(0).getyAxis());
newChart.setName("Group: " + matchingCharts.get(0).getGroup() + ", Match of axis: " + matchingAttribute);
newChart.setGroup(matchingCharts.get(0).getGroup());
newChart.setAttributes(attributes);
C3ChartData chartData = null;
for (final C3Chart chart : matchingCharts) {
C3ChartData tmpChartData = new C3ChartData(chart.getBaseName(), target);
tmpChartData = tmpChartData.filter(chart.getAttributes().indexOf(matchingAttribute));
if (tmpChartData.getData().size() > 0) {
if (chartData == null) {
chartData = tmpChartData;
} else {
chartData = chartData.combineWith(tmpChartData);
}
}
}
new C3ChartDataFile(newChart, target, chartData);
return newChart;
}
/**
* A file filter for description files.
*/
private static class DescriptionFileFilter implements FileFilter {
@Override
public boolean accept(final File pathname) {
return pathname.getName().toLowerCase().endsWith(".json");
}
}
/**
* A file filter for chart files created as a combination of existing charts.
*/
private static class CombinedJsFileFilter implements FileFilter {
@Override
public boolean accept(final File pathname) {
return pathname.getName().toLowerCase().endsWith(".js") && pathname.getName().startsWith(COMBINED_PREFIX);
}
}
}