/* * Javolution - Java(TM) Solution for Real-Time and Embedded Systems * Copyright (C) 2007 - Javolution (http://javolution.org/) * All rights reserved. * * Permission to use, copy, modify, and distribute this software is * freely granted, provided that this notice is preserved. */ package javolution.testing; import j2me.lang.CharSequence; import javolution.context.Context; import javolution.context.LogContext; import javolution.context.ObjectFactory; import javolution.lang.Configurable; import javolution.lang.MathLib; import javolution.text.Text; import javolution.text.TextBuilder; /** * <p> This class represents a {@link TestContext test context} specialized * for measuring execution time.</p> * * <p> {@link TimeContext} implementations may perform assertions based upon the * execution time. For example:[code] * class MyTestCase extends TestCase() { * ... * protected void validate() { * long ns = TimeContext.getAverageTime("ns"); * TestContext.assertTrue(ns < 100); // Error if execution time is more than 100 ns. * ... * } * }[/code]</p> * * @author <a href="mailto:jean-marie@dautelle.com">Jean-Marie Dautelle</a> * @version 5.2, August 5, 2007 */ public abstract class TimeContext extends TestContext { /** * Holds an implementation which does not perform any logging but raises an * {@link AssertionException} when an assertion fails, including any timing * assertion. */ public static final Class/*<? extends TimeContext>*/ REGRESSION = Regression.class; /** * Holds the minimum duration of each test execution (default 1000 ms). * The larger the number the more accurate is the average time result; but * the longer it takes to run the tests. */ public static final Configurable/*<Integer>*/ TEST_DURATION_MS = new Configurable(new Integer(1000)) {}; /** * Holds the time context default implementation (by default logs * average and minimum execution time to <code>System.out</code>). */ public static final Configurable/*<Class<? extends TimeContext>>*/ DEFAULT = new Configurable(Default.class) {}; /** * Holds the minimum execution time in picoseconds. */ private long _minimumPs; /** * Holds the average execution time in picoseconds. */ private long _averagePs; /** * Holds the maximum execution time in picoseconds. */ private long _maximumPs; /** * Enters the {@link #DEFAULT} time context. */ public static void enter() { Context.enter((Class) DEFAULT.get()); } /** * Exits the current time context. * * @throws ClassCastException if the context is not a time context. */ public static void exit() { Context.exit(TimeContext.class); } /** * Returns the minimum execution time of the latest execution performed or * <code>-1</code> if the current context is not a time context. * * @param unit one of <code>"s", "ms", "us", "ns", "ps"</code> * @return the minimum execution time stated in the specified unit. */ public static long getMinimumTime(String unit) { LogContext ctx = (LogContext) LogContext.getCurrentLogContext(); if (ctx instanceof TimeContext) return TimeContext.picosecondTo(unit, ((TimeContext) ctx).getMinimumTimeInPicoSeconds()); else return -1; } /** * Returns the average execution time of the latest execution performed or * <code>-1</code> if the current context is not a time context. * * @param unit one of <code>"s", "ms", "us", "ns", "ps"</code> * @return the average execution time stated in the specified unit. */ public static long getAverageTime(String unit) { LogContext ctx = (LogContext) LogContext.getCurrentLogContext(); if (ctx instanceof TimeContext) return TimeContext.picosecondTo(unit, ((TimeContext) ctx).getAverageTimeInPicoSeconds()); else return -1; } /** * Returns the maximum execution time of the latest execution performed or * <code>-1</code> if the current context is not a time context. * * @param unit one of <code>"s", "ms", "us", "ns", "ps"</code> * @return the maximum execution time stated in the specified unit. */ public static long getMaximumTime(String unit) { LogContext ctx = (LogContext) LogContext.getCurrentLogContext(); if (ctx instanceof TimeContext) return TimeContext.picosecondTo(unit, ((TimeContext) ctx).getMaximumTimeInPicoSeconds()); else return -1; } private static long picosecondTo(String unit, long picoseconds) { if (unit.equals("ps")) return picoseconds; if (unit.equals("ns")) return picoseconds / 1000; if (unit.equals("us")) return picoseconds / 1000000; if (unit.equals("ms")) return picoseconds / 1000000000; if (unit.equals("s")) return picoseconds / 1000000000000L; throw new IllegalArgumentException("Unit " + unit + " not recognized"); } /** * Returns the minimum execution time of the latest execution stated in * pico-seconds. * * @return the time in pico-seconds. */ public long getMinimumTimeInPicoSeconds() { return _minimumPs; } /** * Returns the average execution time of the latest execution stated in * pico-seconds. * * @return the time in pico-seconds. */ public long getAverageTimeInPicoSeconds() { return _averagePs; } /** * Returns the maximmum execution time of the latest execution stated in * pico-seconds. * * @return the time in pico-seconds. */ public long getMaximumTimeInPicoSeconds() { return _maximumPs; } /** * Benchmarks the specified test case and logs the results. * * @param testCase the test case being executed if not marked ignored. */ protected void doRun(TestCase testCase) throws Exception { if (testCase.isIgnored()) return; System.gc(); try { Thread.sleep(200); // For GC to run. } catch (InterruptedException e) { } _minimumPs = Long.MAX_VALUE; _maximumPs = 0; _averagePs = 0; long totalCount = 0; long totalDuration = 0; long maximumDurationPs = ((Integer) TEST_DURATION_MS.get()).intValue() * 1000000000L; do { testCase.setUp(); // Prepare try { long start = TimeContext.nanoTime(); testCase.execute(); // Execute. long duration = (TimeContext.nanoTime() - start) * 1000; // Picoseconds int count = testCase.count(); totalCount += count; totalDuration += duration; long singleExecutionDuration = duration / count; if (singleExecutionDuration < _minimumPs) _minimumPs = singleExecutionDuration; if (singleExecutionDuration > _maximumPs) _maximumPs = singleExecutionDuration; if (totalDuration >= maximumDurationPs) { _averagePs = totalDuration / totalCount; testCase.validate(); // Validate only at last iteration. break; } } finally { testCase.tearDown(); // Cleanup. } } while (true); } // Holds the default implementation. private static final class Default extends TimeContext { private int _passedCount; private int _failedCount; private int _ignoredCount; private boolean _isPassed; protected void enterAction() { _passedCount = _failedCount = _ignoredCount = 0; } protected void exitAction() { logMessage("test", Text.valueOf("---------------------------------------------------")); logMessage("test", Text.valueOf("SUMMARY - PASSED: " + _passedCount + ", FAILED: " + _failedCount + ", IGNORED: " + _ignoredCount)); } protected void doRun(TestSuite testSuite) throws Exception { logMessage("test", Text.valueOf("---------------------------------------------------")); logMessage("test", Text.valueOf("Executes Test Suite: ").plus(testSuite.getName())); logMessage("test", Text.valueOf("")); super.doRun(testSuite); } protected boolean doAssert(boolean value, CharSequence message) { if (!value) { _isPassed = false; return super.doAssert(value, message); // Logs error. } return value; } protected void logMessage(String category, CharSequence message) { if (category.equals("error")) { System.err.print("["); System.err.print(category); System.err.print("] "); System.err.println(message); System.err.flush(); } else { System.out.print("["); System.out.print(category); System.out.print("] "); System.out.println(message); System.out.flush(); } } protected void doRun(TestCase testCase) { if (testCase.isIgnored()) { logWarning(Text.valueOf("Ignore ").plus(testCase.getName())); _ignoredCount++; return; } _isPassed = true; try { super.doRun(testCase); } catch (Throwable error) { _isPassed = false; logError(error, null); } finally { // Updates statistics. if (_isPassed) _passedCount++; else _failedCount++; } TextBuilder tmp = TextBuilder.newInstance(); try { // Formats time report. tmp.append(testCase.getName()); tmp.setLength(40, ' '); tmp.append(" - Average: "); Default.appendTime(this.getAverageTimeInPicoSeconds(), tmp); tmp.append(", Minimum: "); Default.appendTime(this.getMinimumTimeInPicoSeconds(), tmp); tmp.append(", Maximum: "); Default.appendTime(this.getMaximumTimeInPicoSeconds(), tmp); logMessage("time", tmp); } finally { TextBuilder.recycle(tmp); } } private static TextBuilder appendTime(long picoseconds, TextBuilder tb) { long divisor; String unit; if (picoseconds > 1000 * 1000 * 1000 * 1000L) { // 1 s unit = " s"; divisor = 1000 * 1000 * 1000 * 1000L; } else if (picoseconds > 1000 * 1000 * 1000L) { unit = " ms"; divisor = 1000 * 1000 * 1000L; } else if (picoseconds > 1000 * 1000L) { unit = " us"; divisor = 1000 * 1000L; } else if (picoseconds > 1000L) { unit = " ns"; divisor = 1000L; } else { unit = " ps"; divisor = 1L; } long value = picoseconds / divisor; tb.append(value); int fracDigits = 3 - MathLib.digitLength(value); // 3 digits // precision if (fracDigits > 0) tb.append("."); for (int i = 0, j = 10; i < fracDigits; i++, j *= 10) { tb.append((picoseconds * j / divisor) % 10); } return tb.append(unit); } } private static long nanoTime() { /*@JVM-1.5+@ if (true) return System.nanoTime(); /**/ return System.currentTimeMillis() * 1000000; } // TestContext implementation with no output (just validation). private static final class Regression extends TimeContext { protected boolean doAssert(boolean value, CharSequence message) { if (!value) throw new AssertionException(message.toString()); return value; } protected void logMessage(String category, CharSequence message) { // Do nothing. } public boolean isLogged(String category) { return false; } } // Allows instances of private classes to be factory produced. static { ObjectFactory.setInstance(new ObjectFactory() { protected Object create() { return new Default(); } }, Default.class); ObjectFactory.setInstance(new ObjectFactory() { protected Object create() { return new Regression(); } }, Regression.class); } }