package org.radargun.reporting.perfrepo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.perfrepo.client.PerfRepoClient;
import org.perfrepo.model.TestExecution;
import org.perfrepo.model.builder.TestExecutionBuilder;
import org.radargun.config.Configuration;
import org.radargun.config.DefinitionElement;
import org.radargun.config.Init;
import org.radargun.config.MasterConfig;
import org.radargun.config.Property;
import org.radargun.config.PropertyHelper;
import org.radargun.logging.Log;
import org.radargun.logging.LogFactory;
import org.radargun.reporting.Report;
import org.radargun.reporting.Reporter;
import org.radargun.stats.Statistics;
import org.radargun.stats.StatsUtils;
import org.radargun.stats.representation.RepresentationType;
import org.radargun.utils.DateConverter;
import org.radargun.utils.KeyValueListConverter;
import org.radargun.utils.ReflexiveConverters;
import org.radargun.utils.Utils;
/**
* <p>Reporter providing direct upload of benchmarking results to performance repository using
* {@link org.perfrepo.client.PerfRepoClient}.</p>
* <p>Currently supported statistics:
* <ul>
* <li>DefaultOutcome - requests, errors, responseTimeMax, responseTimeMean, throughput.theoretical, throughput.actual</li>
* <li>MeanAndDev - mean, dev</li>
* <li>Maximum relative difference - enabled by appending '.MRD' to statistics name (e.g. BasicOperations.Get.TX.TheoreticalThroughput.MRD)</li>
* </ul></p>
*
* @author Matej Cimbora
*/
public class PerfrepoReporter implements Reporter {
private static final Log log = LogFactory.getLog(PerfrepoReporter.class);
@Property(doc = "Perfrepo host (default localhost)")
private String perfRepoHost = "localhost";
@Property(doc = "Perfrepo port (default 8080)")
private int perfRepoPort = 8080;
@Property(doc = "Perfrepo BASIC Authentication string (required)")
private String perfRepoAuth = null;
@Property(doc = "Perfrepo tags, semicolon separated")
private String perfRepoTag = null;
@Property(doc = "Perfrepo test uid (required)")
private String perfRepoTest = null;
@Property(doc = "ID of jenkins build that produced this report (default not <unknown>)")
private String jenkinsBuild = "<unknown>";
@Property(doc = "URL of jenkins build that produced this report (default <unknown>)")
private String jenkinsBuildUrl = "<unknown>";
@Property(doc = "Date of jenkins build that produced this report (default current date)", converter = DateConverter.class)
private Date jenkinsBuildDate = Calendar.getInstance().getTime();
@Property(doc = "Name mapping between radargun statistics names and perfrepo metric names. Only statistics with defined mapping will be uploaded.",
complexConverter = MetricNameMappingConverter.class)
private List<MetricNameMapping> metricNameMapping = new ArrayList<>();
@Property(doc = "Additional build parameters", converter = KeyValueListConverter.class)
private Map<String, String> buildParams = new HashMap<>();
@Property(doc = "File (in java properties format) from which to load additional build parameters")
private String buildParamsFile = null;
@Property(doc = "Exclude default build parameters from uploaded entity. Parameters specified in 'buildParams' are not excluded. Default is false.")
private boolean excludeBuildParams = false;
@Property(doc = "Exclude service configurations from uploaded entity. Default is false.")
private boolean excludeNormalizedConfigs = false;
@Property(doc = "Exclude attachments from uploaded entity. Default is false.")
private boolean excludeAttachments = false;
@Property(doc = "Create separate test execution for each test iteration. Default is false.")
private boolean separateTestIterations = false;
@Property(doc = "Which configurations should this reporter report. Default is all configurations. Comma separated.")
private List<String> configurations;
@Property(doc = "Which tests should this reporter report. Default is all executed tests. Comma separated.")
private List<String> tests;
private MasterConfig masterConfig;
@Init
public void validate() {
Set<String> mappingTargets = new HashSet<>();
for (MetricNameMapping mapping : metricNameMapping) {
if (!mappingTargets.add(mapping.to)) {
throw new IllegalArgumentException("Multiple mappings to " + mapping.to);
}
}
}
@Override
public void run(MasterConfig masterConfig, Collection<Report> reports) {
this.masterConfig = masterConfig;
if (perfRepoAuth == null) {
log.error("perfRepoAuth parameter has to be set");
return;
}
if (perfRepoTest == null) {
log.error("perfRepoTest parameter has to be set");
return;
}
for (Report report : reports) {
if (configurations == null || configurations.contains(report.getConfiguration().name)) {
reportTests(report);
}
}
}
private void reportTests(Report report) {
String perfRepoAddress = perfRepoHost + ":" + perfRepoPort;
PerfRepoClient perfRepoClient = new PerfRepoClient(perfRepoAddress, "", perfRepoAuth);
if (metricNameMapping == null) {
log.warn("Metric name mapping was not defined!");
return;
}
for (Report.Test test : report.getTests()) {
if (tests != null && !tests.contains(test.name)) {
log.debugf("Test %s is not reported.", test.name);
continue;
} else {
log.debugf("Reporting test %s", test.name);
}
if (separateTestIterations) {
for (Report.TestIteration iteration : test.getIterations()) {
TestExecutionBuilder testExecutionBuilder = createTestExecutionBuilder(report, test);
addIteration(testExecutionBuilder, iteration);
addIterationParameters(testExecutionBuilder, iteration);
testExecutionBuilder.tag("iteration_" + iteration.id);
addConfigsAndUpload(testExecutionBuilder, report, test, perfRepoClient);
}
} else {
TestExecutionBuilder testExecutionBuilder = createTestExecutionBuilder(report, test);
for (Report.TestIteration iteration : test.getIterations()) {
addIteration(testExecutionBuilder, iteration);
addIterationParameters(testExecutionBuilder, iteration);
}
addConfigsAndUpload(testExecutionBuilder, report, test, perfRepoClient);
}
}
}
private void addIteration(TestExecutionBuilder testExecutionBuilder, Report.TestIteration iteration) {
// merge statistics & aggregate mrds
Map<MetricNameMapping, List<Double>> mrdMapping = new HashMap<>();
for (MetricNameMapping mapping : metricNameMapping) {
if (mapping.computeMRD) {
mrdMapping.put(mapping, new ArrayList<>());
}
}
iteration.getStatistics().stream().map(slaveStats -> slaveStats.getValue().stream().reduce(Statistics.MERGE)
.map(statistics -> addRepresentationValues(mrdMapping, statistics)))
.filter(Optional::isPresent).map(Optional::get).reduce(Statistics.MERGE).ifPresent(aggregatedStatistics -> {
long duration = TimeUnit.MILLISECONDS.toNanos(aggregatedStatistics.getEnd() - aggregatedStatistics.getBegin());
String iterationsName = iteration.test.iterationsName == null ? "Iteration" : iteration.test.iterationsName;
String iterationValue = iteration.getValue() == null ? String.valueOf(iteration.id) : iteration.getValue();
for (MetricNameMapping mapping : metricNameMapping) {
if (mapping.computeMRD) {
List<Double> mrds = mrdMapping.get(mapping);
if (!mrds.isEmpty()) {
testExecutionBuilder.value(mapping.to, StatsUtils.calculateMrd(mrds), iterationsName, iterationValue);
}
} else {
double value = mapping.representation.getValue(aggregatedStatistics, mapping.operation, duration);
testExecutionBuilder.value(mapping.to, value, iterationsName, iterationValue);
}
}
});
}
private Statistics addRepresentationValues(Map<MetricNameMapping, List<Double>> mrdMapping, Statistics statistics) {
for (Map.Entry<MetricNameMapping, List<Double>> entry : mrdMapping.entrySet()) {
MetricNameMapping mapping = entry.getKey();
long duration = TimeUnit.MILLISECONDS.toNanos(statistics.getEnd() - statistics.getBegin());
double value = mapping.representation.getValue(statistics, mapping.operation, duration);
entry.getValue().add(value);
}
return statistics;
}
private void addConfigsAndUpload(TestExecutionBuilder testExecutionBuilder, Report report, Report.Test test, PerfRepoClient perfRepoClient) {
// set normalized configs
addNormalizedConfigs(report, testExecutionBuilder);
// create new test execution
TestExecution testExecution = testExecutionBuilder.build();
try {
Long executionId = perfRepoClient.createTestExecution(testExecution);
if (executionId != null) {
// add attachments
uploadAttachments(report, perfRepoClient, executionId);
}
} catch (Exception e) {
log.error("Error while creating test execution for test " + test.name, e);
}
}
private TestExecutionBuilder createTestExecutionBuilder(Report report, Report.Test test) {
String configName = "JDG RG (" + report.getConfiguration().name + ") " + jenkinsBuild;
TestExecutionBuilder testExecutionBuilder =
TestExecution.builder()
.name(configName)
.testUid(perfRepoTest)
.started(jenkinsBuildDate);
addTags(testExecutionBuilder, report);
testExecutionBuilder.tag(test.name);
addBasicParameters(testExecutionBuilder, report);
return testExecutionBuilder;
}
private void addTags(TestExecutionBuilder testExecutionBuilder, Report report) {
if (perfRepoTag != null) {
String[] tags = perfRepoTag.split(";");
for (String tag : tags) {
testExecutionBuilder.tag(tag);
}
}
for (Configuration.Setup setup : report.getConfiguration().getSetups()) {
testExecutionBuilder.tag(setup.plugin + "." + setup.service);
}
testExecutionBuilder.tag(report.getConfiguration().name);
testExecutionBuilder.tag("size" + report.getCluster().getSize());
}
private void addBasicParameters(TestExecutionBuilder testExecutionBuilder, Report report) {
testExecutionBuilder.parameter("exec.config", report.getConfiguration().name);
testExecutionBuilder.parameter("exec.jenkins_build_url", jenkinsBuildUrl);
testExecutionBuilder.parameter("exec.jenkins_build_number", jenkinsBuild);
if (buildParams != null) {
for (Map.Entry<String, String> buildParam : buildParams.entrySet()) {
testExecutionBuilder.parameter(buildParam.getKey(), buildParam.getValue());
}
}
if (buildParamsFile != null) {
Properties paramsFromFile = new Properties();
try (FileInputStream fileInputStream = new FileInputStream(buildParamsFile)) {
paramsFromFile.load(fileInputStream);
} catch (IOException e) {
log.warn("Error while loading build parameters from file", e);
}
for (Map.Entry<Object, Object> entry : paramsFromFile.entrySet()) {
testExecutionBuilder.parameter((String) entry.getKey(), (String) entry.getValue());
}
}
}
private void addIterationParameters(TestExecutionBuilder testExecutionBuilder, Report.TestIteration iteration) {
if (excludeBuildParams) {
return;
}
for (Report.TestResult result : iteration.getResults().values()) {
String resultPrefix = "result." + iteration.id + "." + result.name;
testExecutionBuilder.parameter(resultPrefix + ".aggregated", result.aggregatedValue);
for (Map.Entry<Integer, Report.SlaveResult> slaveResult : result.slaveResults.entrySet()) {
testExecutionBuilder.parameter(resultPrefix + "." + slaveResult.getKey(), slaveResult.getValue().value);
}
}
}
private void addNormalizedConfigs(Report report, TestExecutionBuilder testExecutionBuilder) {
if (excludeNormalizedConfigs) {
return;
}
for (Map.Entry<Integer, Map<String, Properties>> normalizedConfig : report.getNormalizedServiceConfigs().entrySet()) {
for (Map.Entry<String, Properties> configItem : normalizedConfig.getValue().entrySet()) {
if (configItem.getValue() != null) {
for (Map.Entry<Object, Object> property : configItem.getValue().entrySet()) {
StringBuilder key = new StringBuilder();
key.append("slave")
.append(normalizedConfig.getKey())
.append(".");
key.append(configItem.getKey())
.append(".")
.append(property.getKey());
testExecutionBuilder.parameter(key.toString(), property.getValue() == null ? "null" : (String) property.getValue());
}
}
}
}
}
private void uploadAttachments(Report report, PerfRepoClient perfRepoClient, Long executionId) {
if (excludeAttachments) {
return;
}
File file = null;
try {
file = File.createTempFile("configs", ".zip");
} catch (IOException e) {
log.error("Error while creating a temporary file", e);
}
try (FileOutputStream fos = new FileOutputStream(file); ZipOutputStream zos = new ZipOutputStream(fos)) {
if (executionId == null) {
log.debug("No execution ID, attachment not uploaded");
return;
}
boolean contentExists = false;
for (Map.Entry<Integer, Map<String, byte[]>> originalConfig : report.getOriginalServiceConfig().entrySet()) {
for (Map.Entry<String, byte[]> configItem : originalConfig.getValue().entrySet()) {
if (configItem.getValue() != null && configItem.getValue().length > 0) {
zos.putNextEntry(new ZipEntry("slave" + originalConfig.getKey() + "_" + configItem.getKey()));
zos.write(configItem.getValue());
zos.closeEntry();
contentExists = true;
}
}
}
if (masterConfig != null) {
zos.putNextEntry(new ZipEntry("master_config.xml"));
zos.write(masterConfig.getMasterConfigBytes());
zos.closeEntry();
contentExists = true;
if (masterConfig.getScenarioBytes() != null) {
zos.putNextEntry(new ZipEntry("master_scenario.xml"));
zos.write(masterConfig.getScenarioBytes());
zos.closeEntry();
}
}
zos.finish();
Utils.close(zos, fos);
// avoid uploading empty attachments
if (contentExists) {
perfRepoClient.uploadAttachment(executionId, file, "application/zip", "configs.zip");
}
} catch (Exception e) {
log.error("Error while uploading attachment", e);
} finally {
if (file != null) {
if (!file.delete()) {
file.deleteOnExit();
}
}
}
}
@Override
public String toString() {
return "PerfrepoReporter" + PropertyHelper.toString(this);
}
@DefinitionElement(name = "map", doc = "Definition of mapping to PerfRepo metric.")
private static class MetricNameMapping {
@Property(doc = "Operation that should be mapped.", optional = false)
protected String operation;
@Property(doc = "Which representation should be retrieved", optional = false,
converter = RepresentationType.SimpleConverter.class)
protected RepresentationType representation;
@Property(doc = "Name of the target metric.", optional = false)
protected String to;
@Property(doc = "Compute mean relative deviation of the representation values.")
protected boolean computeMRD = false;
}
private static class MetricNameMappingConverter extends ReflexiveConverters.ListConverter {
public MetricNameMappingConverter() {
super(new Class[] {MetricNameMapping.class});
}
}
}