blob: d93109b76da1a7bcb5c6be47781045f830b7ee3f [file] [log] [blame]
package org.apache.sling.performance;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections.map.MultiKeyMap;
import org.apache.commons.io.output.FileWriterWithEncoding;
import org.apache.commons.math.stat.descriptive.DescriptiveStatistics;
import org.junit.runner.Description;
import org.junit.runner.notification.Failure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ReportLogger {
private static boolean reportFolderLogged = false;
private static final Logger logger = LoggerFactory.getLogger(ReportLogger.class);
public static final String REPORTS_DIR = "performance-reports";
/** Multi map of all ReportLogger instances created by getOrCreate(..) */
private static final MultiKeyMap reportLoggers = new MultiKeyMap();
public enum ReportType {
TXT
}
/** Name of test suite */
private final String testSuiteName;
/** Name of test case */
private final String testCaseName;
/** Class name */
private final String className;
/** Name of test method to which all other tests will be compared */
private final String referenceMethod;
/** Recorded stats for ran tests */
private final Map<String, PerformanceRecord> records = new LinkedHashMap<String, PerformanceRecord>();
/**
* Do not allow instances to be created directly, use the getOrCreate(..) static method
*/
private ReportLogger() {
this.testSuiteName = null;
this.testCaseName = null;
this.className = null;
this.referenceMethod = null;
}
/**
* Create a new ReportLogger, will be called by getOrCreate(..)
*
* @param testSuiteName
* @param testCaseName
* @param className
* @param referenceMethod
*/
private ReportLogger(final String testSuiteName, final String testCaseName, final String className,
final String referenceMethod) {
this.testSuiteName = testSuiteName;
this.testCaseName = testCaseName;
this.className = className;
this.referenceMethod = referenceMethod;
}
/**
* Factory method for ReportRecorder. Will return an existing ReportLogger for given parameters or create a new
* instance and register it internally.
*
* @param testSuiteName
* @param testCaseName
* @param className
* @param referenceMethod
* @return
*/
public static ReportLogger getOrCreate(final String testSuiteName, final String testCaseName,
final String className, final String referenceMethod) {
Object reportLogger = reportLoggers.get(testSuiteName, testCaseName, className, referenceMethod);
if (reportLogger == null) {
reportLogger = new ReportLogger(testSuiteName, testCaseName, className, referenceMethod);
reportLoggers.put(testSuiteName, testCaseName, className, referenceMethod, reportLogger);
}
return (ReportLogger)reportLogger;
}
/**
* Method the writes the performance report after a test is run
* @param testSuiteName
* @param testCaseName
* @param className
* @param methodName
* @param statistics
* @param reportType
* @param reportLevel
* @throws Exception
*/
public static void writeReport(String testSuiteName, String testCaseName, String className, String methodName,
DescriptiveStatistics statistics, ReportType reportType, PerformanceRunner.ReportLevel reportLevel) throws Exception {
switch (reportType) {
case TXT:
writeReportTxt(testSuiteName, testCaseName, className, methodName, statistics, reportLevel);
break;
default:
throw new Exception("The specified reporting format is not yet supported");
}
}
/**
* Method the writes the performance report after a test is run, in text format
*
* @param testSuiteName
* @param testCaseName
* @param className
* @param methodName
* @param statistics
* @param reportLevel
* @throws Exception
*/
public static void writeReportTxt(String testSuiteName, String testCaseName, String className, String methodName,
DescriptiveStatistics statistics, PerformanceRunner.ReportLevel reportLevel) throws Exception {
writeReportTxt(testSuiteName,
testCaseName,
className,
methodName,
statistics.getMin(),
statistics.getPercentile(10),
statistics.getPercentile(50),
statistics.getPercentile(90),
statistics.getMax(),
reportLevel);
}
/**
* Method that writes the performance report
*
* @param testSuiteName
* @param testCaseName
* @param className
* @param methodName
* @param min
* @param percentile10
* @param percentile50
* @param percentile90
* @param max
* @param reportLevel
* @throws Exception
*/
public static void writeReportTxt(String testSuiteName, String testCaseName, String className, String methodName,
double min, double percentile10, double percentile50, double percentile90, double max,
PerformanceRunner.ReportLevel reportLevel) throws Exception {
writeReportTxt(testSuiteName, testCaseName, className, methodName,
min, percentile10, percentile50, percentile90, max,
reportLevel, false);
}
/**
* Method that writes the performance report
*
* @param testSuiteName
* @param testCaseName
* @param className
* @param methodName
* @param min
* @param percentile10
* @param percentile50
* @param percentile90
* @param max
* @param reportLevel
* @param showDecimals
* @throws Exception
*/
public static void writeReportTxt(String testSuiteName, String testCaseName, String className, String methodName,
double min, double percentile10, double percentile50, double percentile90, double max,
PerformanceRunner.ReportLevel reportLevel, boolean showDecimals) throws Exception {
File reportDir = new File("target/" + REPORTS_DIR);
if (!reportDir.exists() && !reportDir.mkdir()) {
throw new IOException("Unable to create " + REPORTS_DIR + " directory");
}
// need this in the case a user wants to set the suite name from the
// command line
// useful if we run the test cases from the command line for example
// by using maven
if (testSuiteName.equals(ParameterizedTestList.TEST_CASE_ONLY)) {
if (System.getProperty("testsuitename") != null) {
testSuiteName = System.getProperty("testsuitename");
}
}
if (reportLevel.equals(PerformanceRunner.ReportLevel.ClassLevel)) {
String resultFileName = className;
writeReportClassLevel(resultFileName, testSuiteName, min, percentile10, percentile50, percentile90, max);
} else if (reportLevel.equals(PerformanceRunner.ReportLevel.MethodLevel)) {
String resultFileName = className + "." + methodName;
writeReportMethodLevel(resultFileName, testSuiteName, testCaseName, className, methodName,
min, percentile10, percentile50, percentile90, max, showDecimals);
}
}
/**
* Write report for class level tests
*
* @param resultFileName the name of the result file (without extension)
* @param testSuiteName the name of the test suite name
* @param min
* @param percentile10
* @param percentile50
* @param percentile90
* @param max
*/
private static void writeReportClassLevel(String resultFileName, String testSuiteName,
double min, double percentile10, double percentile50, double percentile90, double max) throws IOException {
File report = getReportFile(resultFileName, ".txt");
boolean needsPrefix = !report.exists();
PrintWriter writer = new PrintWriter(
new FileWriterWithEncoding(report, "UTF-8", true));
try {
if (needsPrefix) {
writer.format("# %-50.50s min 10%% 50%% 90%% max%n", resultFileName);
}
writer.format(
"%-52.52s %6.0f %6.0f %6.0f %6.0f %6.0f%n",
testSuiteName,
min,
percentile10,
percentile50,
percentile90,
max);
} finally {
writer.close();
}
}
/**
* Write report for method level tests
*
* @param resultFileName the name of the result file (without extension)
* @param testSuiteName the name of the test suite name
* @param testCaseName
* @param className
* @param methodName
* @param min
* @param percentile10
* @param percentile50
* @param percentile90
* @param max
*/
private static void writeReportMethodLevel(String resultFileName, String testSuiteName,
String testCaseName, String className, String methodName,
double min, double percentile10, double percentile50, double percentile90, double max,
boolean showDecimals) throws IOException {
File report = getReportFile(resultFileName, ".txt");
boolean needsPrefix = !report.exists();
PrintWriter writer = new PrintWriter(
new FileWriterWithEncoding(report, "UTF-8", true));
try {
if (needsPrefix) {
writer.format(
"%-40.40s|%-120.120s|%-80.80s|%-40.40s| DateTime | min | 10%% | 50%% | 90%% | max%n",
"Test Suite",
"Test Case",
"Test Class",
"Test Method");
}
writer.format(
showDecimals ?
"%-40.40s|%-120.120s|%-80.80s|%-40.40s|%-20.20s|%7.2f|%9.2f|%9.2f|%9.2f|%9.2f%n":
"%-40.40s|%-120.120s|%-80.80s|%-40.40s|%-20.20s|%7.0f|%9.0f|%9.0f|%9.0f|%9.0f%n",
testSuiteName,
(testCaseName.length() < 120) ? (testCaseName) : (testCaseName.substring(0, 115) + "[...]"),
className,
methodName,
getDate(),
min,
percentile10,
percentile50,
percentile90,
max);
} finally {
writer.close();
}
}
/**
* Get the date that will be written into the result file
*/
private static String getDate() {
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date date = new Date();
return dateFormat.format(date);
}
private static File getReportFile(String resultFileName, String extension) {
final String folder = "target/" + REPORTS_DIR;
final String filename = resultFileName + extension;
if(!reportFolderLogged) {
logger.info("Writing performance test results under {}", folder);
reportFolderLogged = true;
}
return new File(folder, filename);
}
/**
* Write results from all registered loggers
*
* @throws Exception
*/
public static void writeAllResults() throws Exception {
for (Object reportLogger : reportLoggers.values()) {
((ReportLogger)reportLogger).writeResults();
}
}
/**
* Check all thresholds for all records in all registered loggers
*
* @return
*/
public static List<Failure> checkAllThresholds() throws ClassNotFoundException {
List<Failure> failures = new ArrayList<Failure>();
for (Object reportLogger : reportLoggers.values()) {
failures.addAll(((ReportLogger) reportLogger).checkThresholds());
}
return failures;
}
/**
* Record statistics for given method
*
* @param methodName
* @param statistics
*/
public void recordStatistics(final String methodName, final DescriptiveStatistics statistics, final double threshold) {
records.put(methodName, new PerformanceRecord(statistics, threshold));
}
/**
* Write all records to file in TXT format
*
* @throws Exception
*/
public void writeResults() throws Exception {
PerformanceRecord referenceRecord = records.get(referenceMethod);
for (String methodName : records.keySet()) {
DescriptiveStatistics statistics = records.get(methodName).getStatistics();
double min = statistics.getMin();
double percentile10 = statistics.getPercentile(10);
double percentile50 = statistics.getPercentile(50);
double percentile90 = statistics.getPercentile(90);
double max = statistics.getMax();
boolean showDecimals = false;
if (referenceRecord != null && !referenceMethod.equals(methodName)) {
DescriptiveStatistics referenceStatistics = referenceRecord.getStatistics();
double ref = referenceStatistics.getMin();
min = ref == 0 ? Double.POSITIVE_INFINITY : min/ref;
ref = referenceStatistics.getPercentile(10);
percentile10 = ref == 0 ? Double.POSITIVE_INFINITY : percentile10/ref;
ref = referenceStatistics.getPercentile(50);
percentile50 = ref == 0 ? Double.POSITIVE_INFINITY : percentile50/ref;
ref = referenceStatistics.getPercentile(90);
percentile90 = ref == 0 ? Double.POSITIVE_INFINITY : percentile90/ref;
ref = referenceStatistics.getMax();
max = ref == 0 ? Double.POSITIVE_INFINITY : max /referenceStatistics.getMax();
showDecimals = true;
}
ReportLogger.writeReportTxt(testSuiteName,
testCaseName,
Class.forName(className).getSimpleName(),
methodName,
min,
percentile10,
percentile50,
percentile90,
max,
PerformanceRunner.ReportLevel.MethodLevel,
showDecimals);
}
}
/**
* Test if any of the <link>PerformanceRecord</link> exceeds their threshold against the reference
*
* @return
*/
public List<Failure> checkThresholds() throws ClassNotFoundException {
PerformanceRecord referenceRecord = records.get(referenceMethod);
if (referenceRecord == null) {
return Collections.EMPTY_LIST;
}
DescriptiveStatistics referenceStatistics = referenceRecord.getStatistics();
List<Failure> failures = new ArrayList<Failure>();
for (String methodName : records.keySet()) {
PerformanceRecord performanceRecord = records.get(methodName);
String result = performanceRecord.checkThreshold(referenceStatistics);
if (result != null) {
failures.add(new Failure(Description.createTestDescription(Class.forName(className), methodName),
new Exception(result)));
}
}
return failures;
}
}