001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.junit.rebind;
017:
018: import com.google.gwt.core.ext.TreeLogger;
019: import com.google.gwt.core.ext.UnableToCompleteException;
020: import com.google.gwt.core.ext.typeinfo.JClassType;
021: import com.google.gwt.core.ext.typeinfo.JMethod;
022: import com.google.gwt.core.ext.typeinfo.JParameter;
023: import com.google.gwt.junit.JUnitShell;
024: import com.google.gwt.dev.generator.ast.ForLoop;
025: import com.google.gwt.dev.generator.ast.MethodCall;
026: import com.google.gwt.dev.generator.ast.Statement;
027: import com.google.gwt.dev.generator.ast.Statements;
028: import com.google.gwt.dev.generator.ast.StatementsList;
029: import com.google.gwt.user.rebind.SourceWriter;
030:
031: import java.util.Map;
032: import java.util.List;
033: import java.util.Set;
034: import java.util.HashMap;
035: import java.util.ArrayList;
036: import java.util.Collections;
037:
038: /**
039: * Implements a generator for Benchmark classes. Benchmarks require additional
040: * code generation above and beyond standard JUnit tests.
041: */
042: public class BenchmarkGenerator extends JUnitTestCaseStubGenerator {
043:
044: private static class MutableBoolean {
045: boolean value;
046: }
047:
048: private static final String BEGIN_PREFIX = "begin";
049:
050: private static final String BENCHMARK_PARAM_META = "gwt.benchmark.param";
051:
052: private static final String EMPTY_FUNC = "__emptyFunc";
053:
054: private static final String END_PREFIX = "end";
055:
056: private static final String ESCAPE_LOOP = "__escapeLoop";
057:
058: /**
059: * Returns all the zero-argument JUnit test methods that do not have
060: * overloads.
061: *
062: * @return Map<String,JMethod>
063: */
064: public static Map<String, JMethod> getNotOverloadedTestMethods(
065: JClassType requestedClass) {
066: Map<String, List<JMethod>> methods = getAllMethods(
067: requestedClass, new MethodFilter() {
068: public boolean accept(JMethod method) {
069: return isJUnitTestMethod(method, true);
070: }
071: });
072:
073: // Create a new map to store the methods
074: Map<String, JMethod> notOverloadedMethods = new HashMap<String, JMethod>();
075: for (Map.Entry<String, List<JMethod>> entry : methods
076: .entrySet()) {
077: List<JMethod> methodOverloads = entry.getValue();
078: if (methodOverloads.size() <= 1) {
079: notOverloadedMethods.put(entry.getKey(),
080: methodOverloads.get(0));
081: }
082: }
083:
084: return notOverloadedMethods;
085: }
086:
087: /**
088: * Returns all the JUnit test methods that are overloaded test methods with
089: * parameters. Does not include the zero-argument test methods.
090: *
091: * @return Map<String,JMethod>
092: */
093: public static Map<String, JMethod> getParameterizedTestMethods(
094: JClassType requestedClass, TreeLogger logger) {
095:
096: Map<String, List<JMethod>> testMethods = getAllMethods(
097: requestedClass, new MethodFilter() {
098: public boolean accept(JMethod method) {
099: return isJUnitTestMethod(method, true);
100: }
101: });
102:
103: // Create a new mapping to return
104: Map<String, JMethod> overloadedMethods = new HashMap<String, JMethod>();
105:
106: // Remove all non-overloaded test methods
107: for (Map.Entry<String, List<JMethod>> entry : testMethods
108: .entrySet()) {
109: String name = entry.getKey();
110: List<JMethod> methods = entry.getValue();
111:
112: if (methods.size() > 2) {
113: String msg = requestedClass
114: + "."
115: + name
116: + " has more than one overloaded version.\n"
117: + "It will not be included in the test case execution.";
118: logger.log(TreeLogger.WARN, msg, null);
119: continue;
120: }
121:
122: if (methods.size() == 1) {
123: JMethod method = methods.get(0);
124: if (method.getParameters().length != 0) {
125: /* User probably goofed - otherwise why create a test method with
126: * arguments but not the corresponding no-argument version? Would be
127: * better if our benchmarking system didn't require the no-argument
128: * test to make the benchmarks run correctly (JUnit artifact).
129: */
130: String msg = requestedClass
131: + "."
132: + name
133: + " does not have a zero-argument overload.\n"
134: + "It will not be included in the test case execution.";
135: logger.log(TreeLogger.WARN, msg, null);
136: }
137: // Only a zero-argument version, we don't need to process it.
138: continue;
139: }
140:
141: JMethod method1 = methods.get(0);
142: JMethod method2 = methods.get(1);
143: JMethod noArgMethod = null;
144: JMethod overloadedMethod = null;
145:
146: if (method1.getParameters().length == 0) {
147: noArgMethod = method1;
148: } else {
149: overloadedMethod = method1;
150: }
151:
152: if (method2.getParameters().length == 0) {
153: noArgMethod = method2;
154: } else {
155: overloadedMethod = method2;
156: }
157:
158: if (noArgMethod == null) {
159: String msg = requestedClass
160: + "."
161: + name
162: + " does not have a zero-argument overload.\n"
163: + "It will not be included in the test case execution.";
164: logger.log(TreeLogger.WARN, msg, null);
165: continue;
166: }
167:
168: overloadedMethods.put(entry.getKey(), overloadedMethod);
169: }
170:
171: return overloadedMethods;
172: }
173:
174: private static JMethod getBeginMethod(JClassType type, String name) {
175: StringBuffer methodName = new StringBuffer(name);
176: methodName.replace(0, "test".length(), BEGIN_PREFIX);
177: return getMethod(type, methodName.toString());
178: }
179:
180: private static JMethod getEndMethod(JClassType type, String name) {
181: StringBuffer methodName = new StringBuffer(name);
182: methodName.replace(0, "test".length(), END_PREFIX);
183: return getMethod(type, methodName.toString());
184: }
185:
186: private static JMethod getMethod(JClassType type,
187: MethodFilter filter) {
188: Map<String, List<JMethod>> map = getAllMethods(type, filter);
189: Set<Map.Entry<String, List<JMethod>>> entrySet = map.entrySet();
190: if (entrySet.size() == 0) {
191: return null;
192: }
193: List<JMethod> methods = entrySet.iterator().next().getValue();
194: return methods.get(0);
195: }
196:
197: private static JMethod getMethod(JClassType type, final String name) {
198: return getMethod(type, new MethodFilter() {
199: public boolean accept(JMethod method) {
200: return method.getName().equals(name);
201: }
202: });
203: }
204:
205: @Override
206: public void writeSource() throws UnableToCompleteException {
207: super .writeSource();
208:
209: generateEmptyFunc(getSourceWriter());
210: implementZeroArgTestMethods();
211: implementParameterizedTestMethods();
212: generateAsyncCode();
213: JUnitShell.getReport().addBenchmark(getRequestedClass(),
214: getTypeOracle());
215: }
216:
217: /**
218: * Generates benchmarking code which wraps <code>stmts</code> The timing
219: * result is a double in units of milliseconds. It's value is placed in the
220: * variable named, <code>timeMillisName</code>.
221: *
222: * @return The set of Statements containing the benchmark code along with the
223: * wrapped <code>stmts</code>
224: */
225: private Statements benchmark(Statements stmts,
226: String timeMillisName, boolean generateEscape,
227: Statements recordCode, Statements breakCode) {
228: Statements benchmarkCode = new StatementsList();
229: List<Statements> benchStatements = benchmarkCode
230: .getStatements();
231:
232: ForLoop loop = new ForLoop("int numLoops = 1", "true", "");
233: benchStatements.add(loop);
234: List<Statements> loopStatements = loop.getStatements();
235:
236: loopStatements.add(new Statement(
237: "long start = System.currentTimeMillis()"));
238: ForLoop runLoop = new ForLoop("int i = 0", "i < numLoops",
239: "++i", stmts);
240: loopStatements.add(runLoop);
241:
242: // Put the rest of the code in 1 big statement to simplify things
243: String benchCode = "long duration = System.currentTimeMillis() - start;\n\n"
244: +
245:
246: "if ( duration < 150 ) {\n"
247: + " numLoops += numLoops;\n"
248: + " continue;\n"
249: + "}\n\n"
250: +
251:
252: "double durationMillis = duration * 1.0;\n"
253: + "double numLoopsAsDouble = numLoops * 1.0;\n"
254: + timeMillisName
255: + " = durationMillis / numLoopsAsDouble";
256:
257: loopStatements.add(new Statement(benchCode));
258:
259: if (recordCode != null) {
260: loopStatements.add(recordCode);
261: }
262:
263: if (generateEscape) {
264: loopStatements.add(new Statement(
265: "if ( numLoops == 1 && duration > 1000 ) {\n"
266: + breakCode.toString() + "\n" + "}\n\n"));
267: }
268:
269: loopStatements.add(new Statement("break"));
270:
271: return benchmarkCode;
272: }
273:
274: /**
275: * Generates code that executes <code>statements</code> for all possible
276: * values of <code>params</code>. Exports a label named ESCAPE_LOOP that
277: * points to the the "inner loop" that should be escaped to for a limited
278: * variable.
279: *
280: * @return the generated code
281: * TODO: Is this used anywhere?
282: */
283: private Statements executeForAllValues(JParameter[] methodParams,
284: Map<String, String> params, Statements statements) {
285: Statements root = new StatementsList();
286: Statements currentContext = root;
287:
288: // Profile the setup and teardown costs for this test method
289: // but only if 1 of them exists.
290: for (int i = 0; i < methodParams.length; ++i) {
291: JParameter methodParam = methodParams[i];
292: String paramName = methodParam.getName();
293: String paramValue = params.get(paramName);
294: String typeName = methodParam.getType()
295: .getQualifiedSourceName();
296:
297: String iteratorName = "it_" + paramName;
298: String initializer = "java.util.Iterator<" + typeName
299: + "> " + iteratorName + " = " + paramValue
300: + ".iterator()";
301: ForLoop loop = new ForLoop(initializer, iteratorName
302: + ".hasNext()", "");
303: if (i == methodParams.length - 1) {
304: loop.setLabel(ESCAPE_LOOP);
305: }
306: currentContext.getStatements().add(loop);
307: loop.getStatements().add(
308: new Statement(typeName + " " + paramName + " = "
309: + iteratorName + ".next()"));
310: currentContext = loop;
311: }
312:
313: currentContext.getStatements().add(statements);
314:
315: return root;
316: }
317:
318: private Statements genBenchTarget(JMethod beginMethod,
319: JMethod endMethod, List<String> paramNames, Statements test) {
320: Statements statements = new StatementsList();
321: List<Statements> statementsList = statements.getStatements();
322:
323: if (beginMethod != null) {
324: statementsList.add(new Statement(new MethodCall(beginMethod
325: .getName(), paramNames)));
326: }
327:
328: statementsList.add(test);
329:
330: if (endMethod != null) {
331: statementsList.add(new Statement(new MethodCall(endMethod
332: .getName(), null)));
333: }
334:
335: return statements;
336: }
337:
338: /**
339: * Currently, the benchmarking subsystem does not support async Benchmarks,
340: * so we need to generate some additional code that prevents the user
341: * from entering async mode in their Benchmark, even though we're using
342: * it internally.
343: *
344: * Generates the code for the "supportsAsync" functionality in the
345: * translatable version of GWTTestCase. This includes:
346: *
347: * - the supportsAsync flag
348: * - the supportsAsync method
349: * - the privateDelayTestFinish method
350: * - the privateFinishTest method
351: *
352: */
353: private void generateAsyncCode() {
354: SourceWriter writer = getSourceWriter();
355:
356: writer.println("private boolean supportsAsync;");
357: writer.println();
358: writer.println("public boolean supportsAsync() {");
359: writer.println(" return supportsAsync;");
360: writer.println("}");
361: writer.println();
362: writer
363: .println("private void privateDelayTestFinish(int timeout) {");
364: writer.println(" supportsAsync = true;");
365: writer.println(" try {");
366: writer.println(" delayTestFinish(timeout);");
367: writer.println(" } finally {");
368: writer.println(" supportsAsync = false;");
369: writer.println(" }");
370: writer.println("}");
371: writer.println();
372: writer.println("private void privateFinishTest() {");
373: writer.println(" supportsAsync = true;");
374: writer.println(" try {");
375: writer.println(" finishTest();");
376: writer.println(" } finally {");
377: writer.println(" supportsAsync = false;");
378: writer.println(" }");
379: writer.println("}");
380: writer.println();
381: }
382:
383: /**
384: * Generates an empty JSNI function to help us benchmark function call
385: * overhead.
386: *
387: * We prevent our empty function call from being inlined by the compiler by
388: * making it a JSNI call. This works as of 1.3 RC 2, but smarter versions of
389: * the compiler may be able to inline JSNI.
390: *
391: * Things actually get pretty squirrely in general when benchmarking function
392: * call overhead, because, depending upon the benchmark, the compiler may
393: * inline the benchmark into our benchmark loop, negating the cost we thought
394: * we were measuring.
395: *
396: * The best way to deal with this is for users to write micro-benchmarks such
397: * that the micro-benchmark does significantly more work than a function call.
398: * For example, if micro-benchmarking a function call, perform the function
399: * call 100K times within the microbenchmark itself.
400: */
401: private void generateEmptyFunc(SourceWriter writer) {
402: writer.println("private native void " + EMPTY_FUNC + "() /*-{");
403: writer.println("}-*/;");
404: writer.println();
405: }
406:
407: private Map<String, String> getParamMetaData(JMethod method,
408: MutableBoolean isBounded) throws UnableToCompleteException {
409: Map<String, String> params = new HashMap<String, String>();
410:
411: String[][] allValues = method.getMetaData(BENCHMARK_PARAM_META);
412:
413: if (allValues == null) {
414: return params;
415: }
416:
417: for (int i = 0; i < allValues.length; ++i) {
418: String[] values = allValues[i];
419: StringBuffer result = new StringBuffer();
420: for (int j = 0; j < values.length; ++j) {
421: result.append(values[j]);
422: result.append(" ");
423: }
424: String expr = result.toString();
425: String[] lhsAndRhs = expr.split("=");
426: String paramName = lhsAndRhs[0].trim();
427: String[] nameExprs = paramName.split(" ");
428: if (nameExprs.length > 1 && nameExprs[1].equals("-limit")) {
429: paramName = nameExprs[0];
430: // Make sure this is the last parameter
431: JParameter[] parameters = method.getParameters();
432: if (!parameters[parameters.length - 1].getName()
433: .equals(paramName)) {
434: JClassType cls = method.getEnclosingType();
435: String msg = "Error at "
436: + cls
437: + "."
438: + method.getName()
439: + "\n"
440: + "Only the last parameter of a method can be marked with the -limit flag.";
441: logger.log(TreeLogger.ERROR, msg, null);
442: throw new UnableToCompleteException();
443: }
444:
445: isBounded.value = true;
446: }
447: String paramValue = lhsAndRhs[1].trim();
448: params.put(paramName, paramValue);
449: }
450:
451: return params;
452: }
453:
454: private void implementParameterizedTestMethods()
455: throws UnableToCompleteException {
456:
457: Map<String, JMethod> parameterizedMethods = getParameterizedTestMethods(
458: getRequestedClass(), logger);
459: SourceWriter sw = getSourceWriter();
460: JClassType type = getRequestedClass();
461:
462: // For each test method, benchmark its:
463: // a) overhead (setup + teardown + loop + function calls) and
464: // b) execution time
465: // for all possible parameter values
466: for (Map.Entry<String, JMethod> entry : parameterizedMethods
467: .entrySet()) {
468: String name = entry.getKey();
469: JMethod method = entry.getValue();
470: JMethod beginMethod = getBeginMethod(type, name);
471: JMethod endMethod = getEndMethod(type, name);
472:
473: sw.println("public void " + name + "() {");
474: sw.indent();
475: sw.println(" privateDelayTestFinish( 2000 );");
476: sw.println();
477:
478: MutableBoolean isBounded = new MutableBoolean();
479: Map<String, String> params = getParamMetaData(method,
480: isBounded);
481: validateParams(method, params);
482:
483: JParameter[] methodParams = method.getParameters();
484: List<String> paramNames = new ArrayList<String>(
485: methodParams.length);
486: for (int i = 0; i < methodParams.length; ++i) {
487: paramNames.add(methodParams[i].getName());
488: }
489:
490: List<String> paramValues = new ArrayList<String>(
491: methodParams.length);
492: for (int i = 0; i < methodParams.length; ++i) {
493: paramValues.add(params.get(methodParams[i].getName()));
494: }
495:
496: sw
497: .print("final java.util.List<com.google.gwt.junit.client.Range/*<?>*/> ranges = java.util.Arrays.asList( new com.google.gwt.junit.client.Range/*<?>*/[] { ");
498:
499: for (int i = 0; i < paramNames.size(); ++i) {
500: String paramName = paramNames.get(i);
501: sw.print(params.get(paramName));
502: if (i != paramNames.size() - 1) {
503: sw.print(",");
504: } else {
505: sw.println("} );");
506: }
507: sw.print(" ");
508: }
509:
510: sw
511: .println("final com.google.gwt.junit.client.impl.PermutationIterator permutationIt = new com.google.gwt.junit.client.impl.PermutationIterator( ranges );\n"
512: + "com.google.gwt.user.client.DeferredCommand.addCommand( new com.google.gwt.user.client.IncrementalCommand() {\n"
513: + " public boolean execute() {\n"
514: + " privateDelayTestFinish( 10000 );\n"
515: + " if ( permutationIt.hasNext() ) {\n"
516: + " com.google.gwt.junit.client.impl.PermutationIterator.Permutation permutation = permutationIt.next();\n");
517:
518: for (int i = 0; i < methodParams.length; ++i) {
519: JParameter methodParam = methodParams[i];
520: String typeName = methodParam.getType()
521: .getQualifiedSourceName();
522: String paramName = paramNames.get(i);
523: sw.println(" " + typeName + " " + paramName
524: + " = (" + typeName
525: + ") permutation.getValues().get(" + i + ");");
526: }
527:
528: final String setupTimingName = "__setupTiming";
529: final String testTimingName = "__testTiming";
530:
531: sw.println("double " + setupTimingName + " = 0;");
532: sw.println("double " + testTimingName + " = 0;");
533:
534: Statements setupBench = genBenchTarget(beginMethod,
535: endMethod, paramNames, new Statement(
536: new MethodCall(EMPTY_FUNC, null)));
537: Statements testBench = genBenchTarget(beginMethod,
538: endMethod, paramNames,
539: new Statement(new MethodCall(method.getName(),
540: paramNames)));
541:
542: StringBuffer recordResultsCode = new StringBuffer(
543: "com.google.gwt.junit.client.TestResults results = getTestResults();\n"
544: + "com.google.gwt.junit.client.Trial trial = new com.google.gwt.junit.client.Trial();\n"
545: + "trial.setRunTimeMillis( "
546: + testTimingName
547: + " - "
548: + setupTimingName
549: + " );\n"
550: + "java.util.Map<String, String> variables = trial.getVariables();\n");
551:
552: for (String paramName : paramNames) {
553: recordResultsCode.append("variables.put( \"").append(
554: paramName).append("\", ").append(paramName)
555: .append(".toString() );\n");
556: }
557:
558: recordResultsCode
559: .append("results.getTrials().add( trial )");
560: Statements recordCode = new Statement(recordResultsCode
561: .toString());
562:
563: Statements breakCode = new Statement(
564: " permutationIt.skipCurrentRange()");
565: setupBench = benchmark(setupBench, setupTimingName, false,
566: null, breakCode);
567: testBench = benchmark(testBench, testTimingName,
568: isBounded.value, recordCode, breakCode);
569:
570: Statements testAndSetup = new StatementsList();
571: testAndSetup.getStatements().addAll(
572: setupBench.getStatements());
573: testAndSetup.getStatements().addAll(
574: testBench.getStatements());
575:
576: sw.println(testAndSetup.toString());
577:
578: sw.println(" return true;\n" + " }\n"
579: + " privateFinishTest();\n"
580: + " return false;\n" + " }\n" + "} );\n");
581:
582: sw.outdent();
583: sw.println("}");
584: }
585: }
586:
587: /**
588: * Overrides the zero-arg test methods that don't have any
589: * overloaded/parameterized versions.
590: *
591: * TODO(tobyr) This code shares a lot of similarity with
592: * implementParameterizedTestMethods and they should probably be refactored
593: * into a single function.
594: */
595: private void implementZeroArgTestMethods() {
596: Map<String, JMethod> zeroArgMethods = getNotOverloadedTestMethods(getRequestedClass());
597: SourceWriter sw = getSourceWriter();
598: JClassType type = getRequestedClass();
599:
600: for (Map.Entry<String, JMethod> entry : zeroArgMethods
601: .entrySet()) {
602: String name = entry.getKey();
603: JMethod method = entry.getValue();
604: JMethod beginMethod = getBeginMethod(type, name);
605: JMethod endMethod = getEndMethod(type, name);
606:
607: sw.println("public void " + name + "() {");
608: sw.indent();
609:
610: final String setupTimingName = "__setupTiming";
611: final String testTimingName = "__testTiming";
612:
613: sw.println("double " + setupTimingName + " = 0;");
614: sw.println("double " + testTimingName + " = 0;");
615:
616: Statements setupBench = genBenchTarget(beginMethod,
617: endMethod, Collections.<String> emptyList(),
618: new Statement(new MethodCall(EMPTY_FUNC, null)));
619:
620: StatementsList testStatements = new StatementsList();
621: testStatements.getStatements().add(
622: new Statement(new MethodCall("super."
623: + method.getName(), null)));
624: Statements testBench = genBenchTarget(beginMethod,
625: endMethod, Collections.<String> emptyList(),
626: testStatements);
627:
628: String recordResultsCode = "com.google.gwt.junit.client.TestResults results = getTestResults();\n"
629: + "com.google.gwt.junit.client.Trial trial = new com.google.gwt.junit.client.Trial();\n"
630: + "trial.setRunTimeMillis( "
631: + testTimingName
632: + " - "
633: + setupTimingName
634: + " );\n"
635: + "results.getTrials().add( trial )";
636:
637: Statements breakCode = new Statement(" break "
638: + ESCAPE_LOOP);
639:
640: setupBench = benchmark(setupBench, setupTimingName, false,
641: null, breakCode);
642: testBench = benchmark(testBench, testTimingName, true,
643: new Statement(recordResultsCode), breakCode);
644: ForLoop loop = (ForLoop) testBench.getStatements().get(0);
645: loop.setLabel(ESCAPE_LOOP);
646:
647: sw.println(setupBench.toString());
648: sw.println(testBench.toString());
649:
650: sw.outdent();
651: sw.println("}");
652: }
653: }
654:
655: private void validateParams(JMethod method,
656: Map<String, String> params)
657: throws UnableToCompleteException {
658: JParameter[] methodParams = method.getParameters();
659: for (JParameter methodParam : methodParams) {
660: String paramName = methodParam.getName();
661: String paramValue = params.get(paramName);
662:
663: if (paramValue == null) {
664: String msg = "Could not find the meta data attribute "
665: + BENCHMARK_PARAM_META + " for the parameter "
666: + paramName + " on method " + method.getName();
667: logger.log(TreeLogger.ERROR, msg, null);
668: throw new UnableToCompleteException();
669: }
670: }
671: }
672: }
|