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(); } } }