package com.carrotsearch.junitbenchmarks.h2;
import com.carrotsearch.junitbenchmarks.*;
import org.h2.jdbcx.JdbcDataSource;
import java.io.*;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static com.carrotsearch.junitbenchmarks.BenchmarkOptionsSystemProperties.*;
import static java.lang.Math.max;
import static org.junit.Assert.fail;
/**
* {@link IResultsConsumer} that appends records to a H2 database.
*/
public final class H2Consumer extends AutocloseConsumer implements Closeable
{
/*
* Column indexes in the prepared insert statement.
*/
private final static int RUN_ID, CLASSNAME, NAME, BENCHMARK_ROUNDS, WARMUP_ROUNDS,
ROUND_AVG, ROUND_STDDEV, GC_AVG, GC_STDDEV, GC_INVOCATIONS, GC_TIME,
TIME_BENCHMARK, TIME_WARMUP, UNITS;
static
{
int column = 1;
RUN_ID = column++;
CLASSNAME = column++;
NAME = column++;
BENCHMARK_ROUNDS = column++;
WARMUP_ROUNDS = column++;
ROUND_AVG = column++;
ROUND_STDDEV = column++;
GC_AVG = column++;
GC_STDDEV = column++;
GC_INVOCATIONS = column++;
GC_TIME = column++;
TIME_BENCHMARK = column++;
TIME_WARMUP = column++;
UNITS = column++;
}
/* */
private Connection connection;
/** Unique primary key for this consumer in the RUNS table. */
int runId;
/** Insert statement to the tests table. */
private PreparedStatement newTest;
/** Output directory for charts. */
File chartsDir;
/**
* Charting visitors.
*/
private List<? extends IChartAnnotationVisitor> chartVisitors;
/*
*
*/
public H2Consumer()
{
this(getDefaultDbName());
}
/*
*
*/
public H2Consumer(File dbFileName)
{
this(dbFileName, getDefaultChartsDir(), getDefaultCustomKey());
}
/*
*
*/
public H2Consumer(File dbFileName, File chartsDir, String customKeyValue)
{
try
{
final JdbcDataSource ds = new org.h2.jdbcx.JdbcDataSource();
ds.setURL("jdbc:h2:" + dbFileName.getAbsolutePath() + ";DB_CLOSE_ON_EXIT=FALSE");
ds.setUser("sa");
this.chartsDir = chartsDir;
this.chartVisitors = newChartVisitors();
this.connection = ds.getConnection();
connection.setAutoCommit(false);
super.addAutoclose(this);
checkSchema();
runId = getRunID(customKeyValue);
newTest = connection.prepareStatement(getResource("003-new-result.sql"));
newTest.setInt(RUN_ID, runId);
}
catch (SQLException e)
{
throw new RuntimeException("Cannot initialize H2 database.", e);
}
}
/*
*
*/
private List<? extends IChartAnnotationVisitor> newChartVisitors()
{
return Arrays.asList(
new MethodChartVisitor(),
new HistoryChartVisitor());
}
/**
* Accept a single benchmark result.
*/
public void accept(Result result)
{
// Visit chart collectors.
final Class<?> clazz = result.getTestClass();
final Method method = result.getTestMethod();
for (IChartAnnotationVisitor v : chartVisitors)
v.visit(clazz, method, result);
try
{
newTest.setString(CLASSNAME, result.getTestClassName());
newTest.setString(NAME, result.getTestMethodName());
newTest.setInt(BENCHMARK_ROUNDS, result.benchmarkRounds);
newTest.setInt(WARMUP_ROUNDS, result.warmupRounds);
newTest.setDouble(ROUND_AVG, result.roundAverage.avg);
newTest.setDouble(ROUND_STDDEV, result.roundAverage.stddev);
newTest.setDouble(GC_AVG, result.gcAverage.avg);
newTest.setDouble(GC_STDDEV, result.gcAverage.stddev);
newTest.setInt(GC_INVOCATIONS, (int) result.gcInfo.accumulatedInvocations());
newTest.setDouble(GC_TIME, result.gcInfo.accumulatedTime() / 1000.0);
newTest.setDouble(TIME_WARMUP, result.warmupTime / 1000.0);
newTest.setDouble(TIME_BENCHMARK, result.benchmarkTime / 1000.0);
newTest.setLong(UNITS, result.units);
newTest.executeUpdate();
assertThatPerformanceDidNotDegrade(result);
}
catch (SQLException e)
{
throw new RuntimeException(
"Error while saving the benchmark result to H2.", e);
}
}
/**
* Close the database connection and finalize transaction.
*/
public void close()
{
try
{
if (connection != null)
{
if (!connection.isClosed())
{
doClose();
}
connection = null;
}
}
catch (Exception e)
{
throw new RuntimeException("Failed to close H2 consumer.", e);
}
}
/**
* Rollback all performed operations on request.
*/
public void rollback()
{
try
{
connection.rollback();
this.chartVisitors = newChartVisitors();
}
catch (SQLException e)
{
throw new RuntimeException("Could not rollback.", e);
}
}
/**
* Retrieve DB version.
*/
DbVersions getDbVersion() throws SQLException
{
Statement s = connection.createStatement();
ResultSet rs = s.executeQuery("SHOW TABLES");
Set<String> tables = new HashSet<String>();
while (rs.next())
{
tables.add(rs.getString(1));
}
if (!tables.contains("DBVERSION"))
{
if (tables.contains("RUNS"))
{
return DbVersions.VERSION_1;
}
return DbVersions.UNINITIALIZED;
}
DbVersions version;
rs = s.executeQuery("SELECT VERSION FROM DBVERSION");
if (!rs.next())
{
throw new RuntimeException("Missing version row in DBVERSION table.");
}
version = DbVersions.fromInt(rs.getInt(1));
if (rs.next()) {
throw new RuntimeException("More than one row in DBVERSION table.");
}
return version;
}
/**
* Read a given resource from classpath and return UTF-8 decoded string.
*/
static String getResource(String resourceName)
{
try
{
InputStream is = H2Consumer.class.getResourceAsStream(resourceName);
if (is == null) throw new IOException();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
final byte [] buffer = new byte [1024];
int cnt;
while ((cnt = is.read(buffer)) > 0) {
baos.write(buffer, 0, cnt);
}
is.close();
baos.close();
return new String(baos.toByteArray(), "UTF-8");
}
catch (IOException e)
{
throw new RuntimeException("Required resource missing: "
+ resourceName);
}
}
/**
* Return the global default DB name.
*/
private static File getDefaultDbName()
{
final String dbPath = System.getProperty(BenchmarkOptionsSystemProperties.DB_FILE_PROPERTY);
if (dbPath != null && !dbPath.trim().equals(""))
{
return new File(dbPath);
}
throw new IllegalArgumentException("Missing global property: "
+ BenchmarkOptionsSystemProperties.DB_FILE_PROPERTY);
}
private static String getDefaultCustomKey()
{
return System.getProperty(BenchmarkOptionsSystemProperties.CUSTOMKEY_PROPERTY);
}
private static File getDefaultChartsDir()
{
return new File(System.getProperty(BenchmarkOptionsSystemProperties.CHARTS_DIR_PROPERTY, "."));
}
/**
* @return Create a row for this consumer's test run.
*/
private int getRunID(String customKeyValue) throws SQLException
{
PreparedStatement s = connection.prepareStatement(
getResource("002-new-run.sql"), Statement.RETURN_GENERATED_KEYS);
s.setString(1, System.getProperty("java.runtime.version", "?"));
s.setString(2, System.getProperty("os.arch", "?"));
s.setString(3, customKeyValue);
s.executeUpdate();
ResultSet rs = s.getGeneratedKeys();
if (!rs.next()) throw new SQLException("No autogenerated keys?");
final int key = rs.getInt(1);
if (rs.next()) throw new SQLException("More than one autogenerated key?");
rs.close();
s.close();
return key;
}
/**
* Do finalize the consumer; close db connection and emit reports.
*/
private void doClose() throws Exception
{
try
{
for (IChartAnnotationVisitor v : chartVisitors)
{
v.generate(this);
}
}
finally
{
if (!connection.isClosed())
{
connection.commit();
connection.close();
}
}
}
/**
* Check database schema and create it if needed.
*/
private void checkSchema() throws SQLException
{
DbVersions dbVersion = getDbVersion();
Statement s = connection.createStatement();
switch (dbVersion)
{
case UNINITIALIZED:
s.execute(getResource("000-create-runs.sql"));
s.execute(getResource("001-create-tests.sql"));
// fall-through.
case VERSION_1:
s.execute(getResource("004-create-dbversion.sql"));
s.execute(getResource("005-add-custom-key.sql"));
updateDbVersion(DbVersions.VERSION_2);
// fall-through
case VERSION_2:
break;
default:
throw new RuntimeException("Unexpected database version: "
+ dbVersion);
}
connection.commit();
}
/**
* Update database version.
*/
private void updateDbVersion(DbVersions newVersion) throws SQLException
{
Statement s = connection.createStatement();
s.executeUpdate("DELETE FROM DBVERSION");
s.executeUpdate("INSERT INTO DBVERSION (VERSION) VALUES (" + newVersion.version + ")");
}
/**
*
*/
Connection getConnection()
{
return connection;
}
/**
* Extension made by peter.romianowski@optivo.de - assert that performance did not degrade.
*/
void assertThatPerformanceDidNotDegrade(Result result) throws SQLException
{
BenchmarkOptions annotation = result.getTestMethod().getAnnotation(BenchmarkOptions.class);
if (annotation == null) {
annotation = result.getTestClass().getAnnotation(BenchmarkOptions.class);
}
if (runId > 1)
{
// This is not the first run ...
int perfAverageOverRuns = (int) getPerfOption(annotation != null ? annotation.perfAverageOverRuns() : -1, PERF_AVERAGE_OVER_RUNS_PROPERTY);
int perfIgnoreUpToRun = (int) getPerfOption(annotation != null ? annotation.perfIgnoreUpToRun() : -1, PERF_IGNORE_UP_TO_RUN_PROPERTY);
double perfDiffToLastRun = getPerfOption(annotation != null ? annotation.perfDiffToLastRun() : -1, PERF_DIFF_TO_LAST_RUN_PROPERTY);
double perfDiffToAverage = getPerfOption(annotation != null ? annotation.perfDiffToAverage() : -1, PERF_DIFF_TO_AVERAGE_PROPERTY);
double average = getRoundAverageFor(result, max(perfIgnoreUpToRun, runId - perfAverageOverRuns), runId - 1);
double last = getRoundAverageFor(result, max(perfIgnoreUpToRun, runId - 1), max(perfIgnoreUpToRun, runId - 1));
double lastPerformanceDiff = last != 0 ? (result.roundAverage.avg / last) - 1 : 0;
if (perfDiffToLastRun >= 0 && lastPerformanceDiff > perfDiffToLastRun)
fail("Performance degraded by " + (int)(lastPerformanceDiff * 10000) / 100f + "% compared to last run: " + result.roundAverage.avg + " vs. " + (int)(last * 1000) / 1000f);
double averagePerformanceDiff = average != 0 ? (result.roundAverage.avg / average) - 1 : 0;
if (perfAverageOverRuns > 0 && perfDiffToAverage >= 0 && averagePerformanceDiff > perfDiffToAverage)
fail("Performance degraded by " + (int)(averagePerformanceDiff * 10000) / 100f + "% compared to average of the last " + perfAverageOverRuns + " runs: " + result.roundAverage.avg + " vs. " + (int)(average * 1000) / 1000f);
}
}
private double getPerfOption(double value, String systemPropertiesKey)
{
double result = value;
if (result < 0) {
final String s = System.getProperty(systemPropertiesKey);
if (s != null && s.length() > 0) {
result = Double.parseDouble(s);
}
}
return result;
}
private double getRoundAverageFor(Result result, int minRunId, int maxRunId) throws SQLException
{
PreparedStatement s = connection.prepareStatement(
"SELECT AVG(ROUND_AVG) "
+ "FROM tests t, runs r "
+ "WHERE t.run_id = r.id "
+ " AND t.classname = ? AND t.name = ? AND t.run_id >= ? AND t.run_id <= ?");
try
{
s.setString(1, result.getTestClassName());
s.setString(2, result.getTestMethodName());
s.setInt(3, max(0, minRunId));
s.setInt(4, max(0, maxRunId));
final ResultSet resultSet = s.executeQuery();
double average = 0;
try
{
if (resultSet.next())
average = resultSet.getDouble(1);
return average;
}
finally
{
resultSet.close();
}
}
finally
{
s.close();
}
}
}