blob: 1abd4be2924abab036df3cfac13290687937adb3 [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.benchmarks.rebind;
import com.google.gwt.benchmarks.BenchmarkShell;
import com.google.gwt.benchmarks.client.IterationTimeLimit;
import com.google.gwt.benchmarks.client.RangeEnum;
import com.google.gwt.benchmarks.client.RangeField;
import com.google.gwt.benchmarks.client.Setup;
import com.google.gwt.benchmarks.client.Teardown;
import com.google.gwt.benchmarks.client.impl.BenchmarkResults;
import com.google.gwt.benchmarks.client.impl.IterableAdapter;
import com.google.gwt.benchmarks.client.impl.PermutationIterator;
import com.google.gwt.benchmarks.client.impl.Trial;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.dev.generator.ast.ForLoop;
import com.google.gwt.dev.generator.ast.MethodCall;
import com.google.gwt.dev.generator.ast.Statement;
import com.google.gwt.dev.generator.ast.Statements;
import com.google.gwt.dev.generator.ast.StatementsList;
import com.google.gwt.junit.rebind.JUnitTestCaseStubGenerator;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.IncrementalCommand;
import com.google.gwt.user.rebind.SourceWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Implements a generator for Benchmark classes. Benchmarks require additional
* code generation above and beyond standard JUnit tests.
*/
public class BenchmarkGenerator extends JUnitTestCaseStubGenerator {
private static class MutableLong {
long value;
}
private static final String BEGIN_PREFIX = "begin";
private static final String BENCHMARK_PARAM_META = "gwt.benchmark.param";
private static final String BENCHMARK_RESULTS_CLASS = BenchmarkResults.class.getName();
private static long defaultTimeout = -1;
private static final String EMPTY_FUNC = "__emptyFunc";
private static final String END_PREFIX = "end";
private static final String ESCAPE_LOOP = "__escapeLoop";
private static final String ITERABLE_ADAPTER_CLASS = IterableAdapter.class.getName();
private static final String PERMUTATION_ITERATOR_CLASS = PermutationIterator.class.getName();
private static final String TRIAL_CLASS = Trial.class.getName();
/**
* Returns all the zero-argument JUnit test methods that do not have
* overloads.
*
* @return Map<String,JMethod>
*/
public static Map<String, JMethod> getNotOverloadedTestMethods(
JClassType requestedClass) {
Map<String, List<JMethod>> methods = getAllMethods(requestedClass,
new MethodFilter() {
public boolean accept(JMethod method) {
return isJUnitTestMethod(method, true);
}
});
// Create a new map to store the methods
Map<String, JMethod> notOverloadedMethods = new HashMap<String, JMethod>();
for (Map.Entry<String, List<JMethod>> entry : methods.entrySet()) {
List<JMethod> methodOverloads = entry.getValue();
if (methodOverloads.size() == 1) {
JMethod overload = methodOverloads.get(0);
if (overload.getParameters().length == 0) {
notOverloadedMethods.put(entry.getKey(), overload);
}
}
}
return notOverloadedMethods;
}
/**
* Returns all the JUnit test methods that are overloaded test methods with
* parameters. Does not include the zero-argument test methods.
*
* @return Map<String,JMethod>
*/
public static Map<String, JMethod> getParameterizedTestMethods(
JClassType requestedClass, TreeLogger logger) {
Map<String, List<JMethod>> testMethods = getAllMethods(requestedClass,
new MethodFilter() {
public boolean accept(JMethod method) {
return isJUnitTestMethod(method, true);
}
});
// Create a new mapping to return
Map<String, JMethod> overloadedMethods = new HashMap<String, JMethod>();
// Remove all non-overloaded test methods
for (Map.Entry<String, List<JMethod>> entry : testMethods.entrySet()) {
String name = entry.getKey();
List<JMethod> methods = entry.getValue();
if (methods.size() > 2) {
String msg = requestedClass + "." + name
+ " has more than one overloaded version"
+ "; it will not be included in the test case execution";
logger.log(TreeLogger.WARN, msg, null);
continue;
}
if (methods.size() == 1) {
JMethod method = methods.get(0);
if (method.getParameters().length != 0) {
/*
* User probably goofed - otherwise why create a test method with
* arguments but not the corresponding no-argument version? Would be
* better if our benchmarking system didn't require the no-argument
* test to make the benchmarks run correctly (JUnit artifact).
*/
String msg = requestedClass + "." + name
+ " does not have a zero-argument overload"
+ "; it will not be included in the test case execution";
logger.log(TreeLogger.WARN, msg, null);
}
// Only a zero-argument version, we don't need to process it.
continue;
}
JMethod method1 = methods.get(0);
JMethod method2 = methods.get(1);
JMethod noArgMethod = null;
JMethod overloadedMethod = null;
if (method1.getParameters().length == 0) {
noArgMethod = method1;
} else {
overloadedMethod = method1;
}
if (method2.getParameters().length == 0) {
noArgMethod = method2;
} else {
overloadedMethod = method2;
}
if (noArgMethod == null) {
String msg = requestedClass + "." + name
+ " does not have a zero-argument overload"
+ "; it will not be included in the test case execution";
logger.log(TreeLogger.WARN, msg, null);
continue;
}
overloadedMethods.put(entry.getKey(), overloadedMethod);
}
return overloadedMethods;
}
private static JMethod getBeginMethod(JClassType type, JMethod method) {
Setup setup = method.getAnnotation(Setup.class);
String methodName;
if (setup != null) {
methodName = setup.value();
} else {
methodName = new StringBuffer(method.getName()).replace(0,
"test".length(), BEGIN_PREFIX).toString();
}
return getMethod(type, methodName);
}
private static JMethod getEndMethod(JClassType type, JMethod method) {
Teardown teardown = method.getAnnotation(Teardown.class);
String methodName;
if (teardown != null) {
methodName = teardown.value();
} else {
methodName = new StringBuffer(method.getName()).replace(0,
"test".length(), END_PREFIX).toString();
}
return getMethod(type, methodName);
}
private static JMethod getMethod(JClassType type, MethodFilter filter) {
Map<String, List<JMethod>> map = getAllMethods(type, filter);
Set<Map.Entry<String, List<JMethod>>> entrySet = map.entrySet();
if (entrySet.size() == 0) {
return null;
}
List<JMethod> methods = entrySet.iterator().next().getValue();
return methods.get(0);
}
private static JMethod getMethod(JClassType type, final String name) {
return getMethod(type, new MethodFilter() {
public boolean accept(JMethod method) {
return method.getName().equals(name);
}
});
}
@Override
public void writeSource() throws UnableToCompleteException {
super.writeSource();
generateEmptyFunc(getSourceWriter());
implementZeroArgTestMethods();
implementParameterizedTestMethods();
generateAsyncCode();
BenchmarkShell.getReport().addBenchmark(logger, getRequestedClass());
}
/**
* Generates benchmarking code which wraps <code>stmts</code> The timing
* result is a double in units of milliseconds. It's value is placed in the
* variable named, <code>timeMillisName</code>.
*
* @return The set of Statements containing the benchmark code along with the
* wrapped <code>stmts</code>
*/
private Statements benchmark(Statements stmts, String timeMillisName,
long bound, Statements recordCode, Statements breakCode) {
Statements benchmarkCode = new StatementsList();
List<Statements> benchStatements = benchmarkCode.getStatements();
ForLoop loop = new ForLoop("int numLoops = 1", "true", "");
benchStatements.add(loop);
List<Statements> loopStatements = loop.getStatements();
loopStatements.add(new Statement("long start = System.currentTimeMillis()"));
ForLoop runLoop = new ForLoop("int i = 0", "i < numLoops", "++i", stmts);
loopStatements.add(runLoop);
// Put the rest of the code in 1 big statement to simplify things
String benchCode = "long duration = System.currentTimeMillis() - start;\n\n"
+
"if ( duration < 150 ) {\n"
+ " numLoops += numLoops;\n"
+ " continue;\n"
+ "}\n\n"
+
"double durationMillis = duration * 1.0;\n"
+ "double numLoopsAsDouble = numLoops * 1.0;\n"
+ timeMillisName
+ " = durationMillis / numLoopsAsDouble";
loopStatements.add(new Statement(benchCode));
if (recordCode != null) {
loopStatements.add(recordCode);
}
if (bound != 0) {
loopStatements.add(new Statement("if ( numLoops == 1 && duration > "
+ bound + " ) {\n" + breakCode.toString() + "\n" + "}\n\n"));
}
loopStatements.add(new Statement("break"));
return benchmarkCode;
}
private boolean fieldExists(JClassType type, String fieldName) {
JField field = type.findField(fieldName);
if (field == null) {
JClassType superClass = type.getSuperclass();
// noinspection SimplifiableIfStatement
if (superClass == null) {
return false;
}
return fieldExists(superClass, fieldName);
}
return true;
}
private Statements genBenchTarget(JMethod beginMethod, JMethod endMethod,
List<String> paramNames, Statements test) {
Statements statements = new StatementsList();
List<Statements> statementsList = statements.getStatements();
if (beginMethod != null) {
statementsList.add(new Statement(new MethodCall(beginMethod.getName(),
paramNames)));
}
statementsList.add(test);
if (endMethod != null) {
statementsList.add(new Statement(
new MethodCall(endMethod.getName(), null)));
}
return statements;
}
/**
* Currently, the benchmarking subsystem does not support async Benchmarks, so
* we need to generate some additional code that prevents the user from
* entering async mode in their Benchmark, even though we're using it
* internally.
*
* <p>
* Generates the code for the "supportsAsync" functionality in the
* translatable version of GWTTestCase. This includes:
* <ul>
* <li>the supportsAsync flag</li>
* <li>the supportsAsync method</li>
* <li>the privateDelayTestFinish method</li>
* <li>the privateFinishTest method</li>
* </ul>
* </p>
*/
private void generateAsyncCode() {
SourceWriter writer = getSourceWriter();
writer.println("private boolean supportsAsync;");
writer.println();
writer.println("public boolean supportsAsync() {");
writer.println(" return supportsAsync;");
writer.println("}");
writer.println();
writer.println("private void privateDelayTestFinish(int timeout) {");
writer.println(" supportsAsync = true;");
writer.println(" try {");
writer.println(" delayTestFinish(timeout);");
writer.println(" } finally {");
writer.println(" supportsAsync = false;");
writer.println(" }");
writer.println("}");
writer.println();
writer.println("private void privateFinishTest() {");
writer.println(" supportsAsync = true;");
writer.println(" try {");
writer.println(" finishTest();");
writer.println(" } finally {");
writer.println(" supportsAsync = false;");
writer.println(" }");
writer.println("}");
writer.println();
}
/**
* Generates an empty JSNI function to help us benchmark function call
* overhead.
*
* We prevent our empty function call from being inlined by the compiler by
* making it a JSNI call. This works as of 1.3 RC 2, but smarter versions of
* the compiler may be able to inline JSNI.
*
* Things actually get pretty squirrelly in general when benchmarking function
* call overhead, because, depending upon the benchmark, the compiler may
* inline the benchmark into our benchmark loop, negating the cost we thought
* we were measuring.
*
* The best way to deal with this is for users to write micro-benchmarks such
* that the micro-benchmark does significantly more work than a function call.
* For example, if micro-benchmarking a function call, perform the function
* call 100K times within the micro-benchmark itself.
*/
private void generateEmptyFunc(SourceWriter writer) {
writer.println("private native void " + EMPTY_FUNC + "() /*-{");
writer.println("}-*/;");
writer.println();
}
private Map<String, String> getAnnotationMetaData(JMethod method,
MutableLong bound) throws UnableToCompleteException {
IterationTimeLimit limit = method.getAnnotation(IterationTimeLimit.class);
// noinspection SimplifiableIfStatement
if (limit == null) {
bound.value = getDefaultTimeout();
} else {
bound.value = limit.value();
}
Map<String, String> paramMetaData = new HashMap<String, String>();
JParameter[] params = method.getParameters();
for (JParameter param : params) {
RangeField rangeField = param.getAnnotation(RangeField.class);
if (rangeField != null) {
String fieldName = rangeField.value();
JClassType enclosingType = method.getEnclosingType();
if (!fieldExists(enclosingType, fieldName)) {
logger.log(TreeLogger.ERROR, "The RangeField annotation on "
+ enclosingType + " at " + method + " specifies a field, "
+ fieldName + ", which could not be found. Perhaps it is "
+ "mis-spelled?", null);
throw new UnableToCompleteException();
}
paramMetaData.put(param.getName(), fieldName);
continue;
}
RangeEnum rangeEnum = param.getAnnotation(RangeEnum.class);
if (rangeEnum != null) {
Class<? extends Enum<?>> enumClass = rangeEnum.value();
// Handle inner classes
String className = enumClass.getName().replace('$', '.');
paramMetaData.put(param.getName(), className + ".values()");
continue;
}
String msg = "The parameter, " + param.getName() + ", on method, "
+ method.getName() + ", must have it's range specified"
+ "by a RangeField or RangeEnum annotation.";
logger.log(TreeLogger.ERROR, msg, null);
throw new UnableToCompleteException();
}
return paramMetaData;
}
private synchronized long getDefaultTimeout()
throws UnableToCompleteException {
if (defaultTimeout != -1) {
return defaultTimeout;
}
Method m = null;
try {
m = IterationTimeLimit.class.getDeclaredMethod("value");
defaultTimeout = (Long) m.getDefaultValue();
} catch (Exception e) {
/*
* Possibly one of: - NullPointerException (if somehow TimeLimit weren't
* an annotation or value() didn't have a default). -
* NoSuchMethodException if we somehow spelled value wrong -
* TypeNotPresentException if somehow value were some type of Class that
* couldn't be loaded instead of long It really doesn't make any
* difference, because regardless of what could possibly have failed,
* we'll still need to go this route.
*/
logger.log(TreeLogger.ERROR,
"Unable to retrieve the default benchmark time limit", e);
throw new UnableToCompleteException();
}
return defaultTimeout;
}
private void implementParameterizedTestMethods()
throws UnableToCompleteException {
Map<String, JMethod> parameterizedMethods = getParameterizedTestMethods(
getRequestedClass(), logger);
SourceWriter sw = getSourceWriter();
JClassType type = getRequestedClass();
// For each test method, benchmark its:
// a) overhead (setup + teardown + loop + function calls) and
// b) execution time
// for all possible parameter values
for (Map.Entry<String, JMethod> entry : parameterizedMethods.entrySet()) {
String name = entry.getKey();
JMethod method = entry.getValue();
JMethod beginMethod = getBeginMethod(type, method);
JMethod endMethod = getEndMethod(type, method);
sw.println("public void " + name + "() {");
sw.indent();
sw.println(" privateDelayTestFinish( 2000 );");
sw.println();
MutableLong bound = new MutableLong();
Map<String, String> metaDataByParams = getAnnotationMetaData(method,
bound);
validateParams(method, metaDataByParams);
JParameter[] methodParams = method.getParameters();
List<String> paramNames = new ArrayList<String>(methodParams.length);
for (int i = 0; i < methodParams.length; ++i) {
paramNames.add(methodParams[i].getName());
}
sw.print("final java.util.List<Iterable<?>> iterables = java.util.Arrays.asList( new Iterable<?>[] { ");
for (int i = 0; i < paramNames.size(); ++i) {
String paramName = paramNames.get(i);
sw.print(ITERABLE_ADAPTER_CLASS + ".toIterable("
+ metaDataByParams.get(paramName) + ")");
if (i != paramNames.size() - 1) {
sw.print(",");
} else {
sw.println("} );");
}
sw.print(" ");
}
sw.println("final " + PERMUTATION_ITERATOR_CLASS
+ " permutationIt = new " + PERMUTATION_ITERATOR_CLASS
+ "(iterables);\n" + DeferredCommand.class.getName()
+ ".addCommand( new " + IncrementalCommand.class.getName() + "() {\n"
+ " public boolean execute() {\n"
+ " privateDelayTestFinish( 10000 );\n"
+ " if ( permutationIt.hasNext() ) {\n" + " "
+ PERMUTATION_ITERATOR_CLASS
+ ".Permutation permutation = permutationIt.next();\n");
for (int i = 0; i < methodParams.length; ++i) {
JParameter methodParam = methodParams[i];
String typeName = methodParam.getType().getQualifiedSourceName();
String paramName = paramNames.get(i);
sw.println(" " + typeName + " " + paramName + " = (" + typeName
+ ") permutation.getValues().get(" + i + ");");
}
final String setupTimingName = "__setupTiming";
final String testTimingName = "__testTiming";
sw.println("double " + setupTimingName + " = 0;");
sw.println("double " + testTimingName + " = 0;");
Statements setupBench = genBenchTarget(beginMethod, endMethod,
paramNames, new Statement(new MethodCall(EMPTY_FUNC, null)));
Statements testBench = genBenchTarget(beginMethod, endMethod, paramNames,
new Statement(new MethodCall(method.getName(), paramNames)));
StringBuffer recordResultsCode = new StringBuffer(BENCHMARK_RESULTS_CLASS
+ " results = __getOrCreateTestResult();\n" + TRIAL_CLASS
+ " trial = new " + TRIAL_CLASS + "();\n"
+ "trial.setRunTimeMillis( " + testTimingName + " - "
+ setupTimingName + " );\n"
+ "java.util.Map<String, String> variables = trial.getVariables();\n");
for (String paramName : paramNames) {
recordResultsCode.append("variables.put( \"").append(paramName).append(
"\", ").append(paramName).append(".toString() );\n");
}
recordResultsCode.append("results.getTrials().add( trial )");
Statements recordCode = new Statement(recordResultsCode.toString());
Statements breakCode = new Statement(" permutationIt.skipCurrentRange()");
setupBench = benchmark(setupBench, setupTimingName, 0, null, breakCode);
testBench = benchmark(testBench, testTimingName, bound.value, recordCode,
breakCode);
Statements testAndSetup = new StatementsList();
testAndSetup.getStatements().addAll(setupBench.getStatements());
testAndSetup.getStatements().addAll(testBench.getStatements());
sw.println(testAndSetup.toString());
sw.println(" return true;\n" + " }\n"
+ " privateFinishTest();\n" + " return false;\n" + " }\n"
+ "} );\n");
sw.outdent();
sw.println("}");
}
}
/**
* Overrides the zero-arg test methods that don't have any
* overloaded/parameterized versions.
*
* TODO(tobyr) This code shares a lot of similarity with
* implementParameterizedTestMethods and they should probably be refactored
* into a single function.
*/
private void implementZeroArgTestMethods() throws UnableToCompleteException {
Map<String, JMethod> zeroArgMethods = getNotOverloadedTestMethods(getRequestedClass());
SourceWriter sw = getSourceWriter();
JClassType type = getRequestedClass();
for (Map.Entry<String, JMethod> entry : zeroArgMethods.entrySet()) {
String name = entry.getKey();
JMethod method = entry.getValue();
JMethod beginMethod = getBeginMethod(type, method);
JMethod endMethod = getEndMethod(type, method);
sw.println("public void " + name + "() {");
sw.indent();
final String setupTimingName = "__setupTiming";
final String testTimingName = "__testTiming";
sw.println("double " + setupTimingName + " = 0;");
sw.println("double " + testTimingName + " = 0;");
Statements setupBench = genBenchTarget(beginMethod, endMethod,
Collections.<String> emptyList(), new Statement(new MethodCall(
EMPTY_FUNC, null)));
StatementsList testStatements = new StatementsList();
testStatements.getStatements().add(
new Statement(new MethodCall("super." + method.getName(), null)));
Statements testBench = genBenchTarget(beginMethod, endMethod,
Collections.<String> emptyList(), testStatements);
String recordResultsCode = BENCHMARK_RESULTS_CLASS
+ " results = __getOrCreateTestResult();\n" + TRIAL_CLASS
+ " trial = new " + TRIAL_CLASS + "();\n"
+ "trial.setRunTimeMillis( " + testTimingName + " - "
+ setupTimingName + " );\n" + "results.getTrials().add( trial )";
Statements breakCode = new Statement(" break " + ESCAPE_LOOP);
setupBench = benchmark(setupBench, setupTimingName, 0, null, breakCode);
testBench = benchmark(testBench, testTimingName, getDefaultTimeout(),
new Statement(recordResultsCode), breakCode);
ForLoop loop = (ForLoop) testBench.getStatements().get(0);
loop.setLabel(ESCAPE_LOOP);
sw.println(setupBench.toString());
sw.println(testBench.toString());
sw.outdent();
sw.println("}");
}
}
private void validateParams(JMethod method, Map<String, String> params)
throws UnableToCompleteException {
JParameter[] methodParams = method.getParameters();
for (JParameter methodParam : methodParams) {
String paramName = methodParam.getName();
String paramValue = params.get(paramName);
if (paramValue == null) {
String msg = "Could not find the meta data attribute "
+ BENCHMARK_PARAM_META + " for the parameter " + paramName
+ " on method " + method.getName();
logger.log(TreeLogger.ERROR, msg, null);
throw new UnableToCompleteException();
}
}
}
}