blob: 637b0c5340885bf47a725036407eb0c3d5d2af41 [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;
import com.google.gwt.benchmarks.client.impl.BenchmarkResults;
import com.google.gwt.benchmarks.client.impl.Trial;
import com.google.gwt.benchmarks.rebind.BenchmarkGenerator;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.HasAnnotations;
import com.google.gwt.core.ext.typeinfo.HasMetaData;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.util.Util;
import com.google.gwt.util.tools.Utility;
import junit.framework.TestCase;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.BreakIterator;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/**
* Generates a detailed report that contains the results of all of the
* benchmark-related unit tests executed during a unit test session. The primary
* user of this class is {@link com.google.gwt.junit.JUnitShell}.
*
* The report is in XML format. To view the XML reports, use benchmarkViewer.
*/
public class BenchmarkReport {
/**
* Converts a set of test results for a single benchmark method into XML.
*/
private class BenchmarkXml {
private MetaData metaData;
private List<BenchmarkResults> results;
private TestCase test;
BenchmarkXml(TestCase test, List<BenchmarkResults> results) {
this.test = test;
this.results = results;
Map<String, MetaData> methodMetaData = testMetaData.get(test.getClass().toString());
metaData = methodMetaData.get(test.getName());
}
Element toElement(Document doc) {
Element benchmark = doc.createElement("benchmark");
benchmark.setAttribute("class", test.getClass().getName());
benchmark.setAttribute("name", metaData.getTestName());
benchmark.setAttribute("description", metaData.getTestDescription());
String sourceCode = metaData.getSourceCode();
if (sourceCode != null) {
Element sourceCodeElement = doc.createElement("source_code");
sourceCodeElement.appendChild(doc.createTextNode(sourceCode));
benchmark.appendChild(sourceCodeElement);
}
// TODO(tobyr): create target_code element
for (BenchmarkResults result : results) {
benchmark.appendChild(toElement(doc, result));
}
return benchmark;
}
private Element toElement(Document doc, BenchmarkResults result) {
Element resultElement = doc.createElement("result");
resultElement.setAttribute("host", result.getHost());
resultElement.setAttribute("agent", result.getAgent());
Throwable exception = result.getException();
if (exception != null) {
Element exceptionElement = doc.createElement("exception");
exceptionElement.appendChild(doc.createTextNode(exception.toString()));
resultElement.appendChild(exceptionElement);
}
List<Trial> trials = result.getTrials();
for (Trial trial : trials) {
Element trialElement = toElement(doc, trial);
resultElement.appendChild(trialElement);
}
return resultElement;
}
private Element toElement(Document doc, Trial trial) {
Element trialElement = doc.createElement("trial");
Map<String, String> variables = trial.getVariables();
for (Map.Entry<String, String> entry : variables.entrySet()) {
Object name = entry.getKey();
Object value = entry.getValue();
Element variableElement = doc.createElement("variable");
variableElement.setAttribute("name", name.toString());
variableElement.setAttribute("value", value.toString());
trialElement.appendChild(variableElement);
}
trialElement.setAttribute("timing",
String.valueOf(trial.getRunTimeMillis()));
return trialElement;
}
}
/**
* Parses .java source files to get source code for methods.
*/
private class Parser {
/**
* Maps classes to the contents of their source files.
*/
private Map<JClassType, String> classSources = new HashMap<JClassType, String>();
/**
* Returns the source code for the method of the given name.
*
* @param logger to log the process
* @param method a not <code>null</code> method
* @return <code>null</code> if the source code for the method cannot be
* located
*/
public String getMethod(TreeLogger logger, JMethod method) {
JClassType clazz = method.getEnclosingType();
if (!classSources.containsKey(clazz)) {
char[] sourceContents = null;
File sourceFile = findSourceFile(clazz);
if (sourceFile != null) {
sourceContents = Util.readFileAsChars(sourceFile);
classSources.put(clazz, new String(sourceContents));
}
if (sourceContents == null) {
classSources.put(clazz, null);
String msg = "An unknown I/O exception occured while trying to read "
+ sourceFile.getAbsolutePath();
logger.log(TreeLogger.WARN, msg, null);
} else {
classSources.put(clazz, new String(sourceContents));
String msg = "BenchmarkReport read the contents of "
+ sourceFile.getAbsolutePath();
TreeLogger branch = logger.branch(TreeLogger.DEBUG, msg, null);
if (logger.isLoggable(TreeLogger.SPAM)) {
branch.log(TreeLogger.SPAM, new String(sourceContents), null);
}
}
}
String source = classSources.get(clazz);
if (source == null) {
return source;
}
// TODO: search for the method manually?
return null;
}
}
/**
* Converts an entire report into XML.
*/
private class ReportXml {
private Map<String, Element> categoryElementMap = new HashMap<String, Element>();
private Date date = new Date();
private String version = "unknown";
Element toElement(Document doc) {
Element report = doc.createElement("gwt_benchmark_report");
String dateString = DateFormat.getDateTimeInstance().format(date);
report.setAttribute("date", dateString);
report.setAttribute("gwt_version", version);
// Add each test result into the report.
// Add the category for the test result, if necessary.
for (Map.Entry<TestCase, List<BenchmarkResults>> entry : testResults.entrySet()) {
TestCase test = entry.getKey();
List<BenchmarkResults> results = entry.getValue();
BenchmarkXml xml = new BenchmarkXml(test, results);
Element categoryElement = getCategoryElement(doc, report,
xml.metaData.getCategory().getClassName());
categoryElement.appendChild(xml.toElement(doc));
}
return report;
}
/**
* Locates or creates the category element by the specified name.
*
* @param doc The document to search
* @param report The report to which the category belongs
* @param name The name of the category
*
* @return The matching category element
*/
private Element getCategoryElement(Document doc, Element report, String name) {
Element e = categoryElementMap.get(name);
if (e != null) {
return e;
}
Element categoryElement = doc.createElement("category");
categoryElementMap.put(name, categoryElement);
CategoryImpl category = testCategories.get(name);
categoryElement.setAttribute("name", category.getName());
categoryElement.setAttribute("description", category.getDescription());
report.appendChild(categoryElement);
return categoryElement;
}
}
private static final String GWT_BENCHMARK_CATEGORY = "gwt.benchmark.category";
private static final String GWT_BENCHMARK_DESCRIPTION = "gwt.benchmark.description";
private static final String GWT_BENCHMARK_NAME = "gwt.benchmark.name";
private static File findSourceFile(JClassType clazz) {
final char separator = File.separator.charAt(0);
String filePath = clazz.getPackage().getName().replace('.', separator)
+ separator + clazz.getSimpleSourceName() + ".java";
String[] paths = getClassPath();
for (int i = 0; i < paths.length; ++i) {
File maybeSourceFile = new File(paths[i] + separator + filePath);
if (maybeSourceFile.exists()) {
return maybeSourceFile;
}
}
return null;
}
private static String[] getClassPath() {
String path = System.getProperty("java.class.path");
return path.split(File.pathSeparator);
}
private static String getSimpleMetaData(HasMetaData hasMetaData, String name) {
String[][] allValues = hasMetaData.getMetaData(name);
if (allValues == null) {
return null;
}
StringBuffer result = new StringBuffer();
for (int i = 0; i < allValues.length; ++i) {
String[] values = allValues[i];
for (int j = 0; j < values.length; ++j) {
result.append(values[j]);
result.append(" ");
}
}
String resultString = result.toString().trim();
return resultString.equals("") ? null : resultString;
}
private Parser parser = new Parser();
private Map<String, CategoryImpl> testCategories = new HashMap<String, CategoryImpl>();
private Map<String, Map<String, MetaData>> testMetaData = new HashMap<String, Map<String, MetaData>>();
private Map<TestCase, List<BenchmarkResults>> testResults = new HashMap<TestCase, List<BenchmarkResults>>();
private TypeOracle typeOracle;
public BenchmarkReport() {
}
/**
* Adds the Benchmark to the report. All of the metadata about the benchmark
* (category, name, description, etc...) is recorded from the TypeOracle.
*
* @param logger to log the process
* @param deprecationBranch to log usages of old-style metadata
* @param benchmarkClass The benchmark class to record. Must not be
* <code>null</code>.
* @param typeOracle The <code>TypeOracle</code> for the compilation session
* must not be <code>null</code>.
*/
public void addBenchmark(TreeLogger logger, TreeLogger deprecationBranch,
JClassType benchmarkClass, TypeOracle typeOracle) {
this.typeOracle = typeOracle;
String categoryType = getBenchmarkCategory(deprecationBranch,
benchmarkClass);
Map<String, JMethod> zeroArgMethods = BenchmarkGenerator.getNotOverloadedTestMethods(benchmarkClass);
Map<String, JMethod> parameterizedMethods = BenchmarkGenerator.getParameterizedTestMethods(
benchmarkClass, TreeLogger.NULL);
List<JMethod> testMethods = new ArrayList<JMethod>(zeroArgMethods.size()
+ parameterizedMethods.size());
testMethods.addAll(zeroArgMethods.values());
testMethods.addAll(parameterizedMethods.values());
Map<String, MetaData> metaDataMap = testMetaData.get(benchmarkClass.toString());
if (metaDataMap == null) {
metaDataMap = new HashMap<String, MetaData>();
testMetaData.put(benchmarkClass.toString(), metaDataMap);
}
// Add all of the benchmark methods
for (JMethod method : testMethods) {
String methodName = method.getName();
String methodCategoryType = getBenchmarkCategory(deprecationBranch,
method);
if (methodCategoryType == null) {
methodCategoryType = categoryType;
}
CategoryImpl methodCategory = getCategory(deprecationBranch,
methodCategoryType);
String methodSource = parser.getMethod(logger, method);
StringBuffer sourceBuffer = (methodSource == null) ? null
: new StringBuffer(methodSource);
StringBuffer summary = new StringBuffer();
StringBuffer comment = new StringBuffer();
getComment(sourceBuffer, summary, comment);
MetaData metaData = new MetaData(benchmarkClass.toString(), methodName,
(sourceBuffer != null) ? sourceBuffer.toString() : null,
methodCategory, methodName, summary.toString());
metaDataMap.put(methodName, metaData);
}
}
public void addBenchmarkResults(TestCase test, BenchmarkResults results) {
List<BenchmarkResults> currentResults = testResults.get(test);
if (currentResults == null) {
currentResults = new ArrayList<BenchmarkResults>();
testResults.put(test, currentResults);
}
currentResults.add(results);
}
/**
* Generates reports for all of the benchmarks which were added to the
* generator.
*
* @param outputPath The path to write the reports to.
* @throws ParserConfigurationException If an error occurs during xml parsing
* @throws IOException If anything goes wrong writing to outputPath
*/
public void generate(String outputPath) throws ParserConfigurationException,
IOException {
// Don't generate a new report if no tests were actually run.
if (testResults.size() == 0) {
return;
}
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.newDocument();
doc.appendChild(new ReportXml().toElement(doc));
byte[] xmlBytes = Util.toXmlUtf8(doc);
FileOutputStream fos = null;
try {
fos = new FileOutputStream(outputPath);
fos.write(xmlBytes);
} finally {
Utility.close(fos);
}
// TODO(bruce) The code below is commented out because of GWT Issue 958.
// // TODO(tobyr) Looks like indenting is busted
// // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6296446
// // Not a big deal, since we don't intend to read the XML by hand anyway
// TransformerFactory transformerFactory = TransformerFactory.newInstance();
// // Think this can be used with JDK 1.5
// // transformerFactory.setAttribute( "indent-number", new Integer(2) );
// Transformer serializer = transformerFactory.newTransformer();
// serializer.setOutputProperty(OutputKeys.METHOD, "xml");
// serializer.setOutputProperty(OutputKeys.INDENT, "yes");
// serializer
// .setOutputProperty("{ http://xml.apache.org/xslt }indent-amount", "2");
// BufferedOutputStream docOut = new BufferedOutputStream(
// new FileOutputStream(outputPath));
// serializer.transform(new DOMSource(doc), new StreamResult(docOut));
// docOut.close();
}
private <T extends HasMetaData & HasAnnotations> String getBenchmarkCategory(
TreeLogger deprecationBranch, T element) {
String category = getSimpleMetaData(element, GWT_BENCHMARK_CATEGORY);
if (category != null && deprecationBranch != null) {
deprecationBranch.log(TreeLogger.WARN, GWT_BENCHMARK_CATEGORY + " has "
+ "been deprecated with no replacement.", null);
}
return category;
}
private CategoryImpl getCategory(TreeLogger deprecationBranch, String name) {
CategoryImpl c = testCategories.get(name);
if (c != null) {
return c;
}
c = getCategoryMetaData(deprecationBranch, name);
testCategories.put(name, c);
return c;
}
private CategoryImpl getCategoryMetaData(TreeLogger deprecationBranch,
String typeName) {
if (typeName == null) {
return new CategoryImpl(null, "", "");
}
JClassType categoryType = typeOracle.findType(typeName);
if (categoryType == null) {
return new CategoryImpl(typeName, "", "");
}
String categoryName = getSimpleMetaData(categoryType, GWT_BENCHMARK_NAME);
String categoryDescription = getSimpleMetaData(categoryType,
GWT_BENCHMARK_DESCRIPTION);
if ((categoryName != null || categoryDescription != null)
&& deprecationBranch != null) {
deprecationBranch.log(TreeLogger.WARN, GWT_BENCHMARK_NAME + " and "
+ GWT_BENCHMARK_DESCRIPTION + " have been deprecated with no "
+ "replacement", null);
}
categoryName = categoryName == null ? "" : categoryName;
categoryDescription = categoryDescription == null ? ""
: categoryDescription;
return new CategoryImpl(typeName, categoryName, categoryDescription);
}
/**
* Parses out the JavaDoc comment from a string of source code. Returns the
* first sentence summary in <code>summary</code> and the body of the entire
* comment (including the summary) in <code>comment</code>.
*
* @param sourceCode The source code of a function, including its comment.
* Modified to remove leading whitespace.
* @param summary Modified to contain the first sentence of the comment.
* @param comment Modified to contain the entire comment.
*/
private void getComment(StringBuffer sourceCode, StringBuffer summary,
StringBuffer comment) {
if (sourceCode == null) {
return;
}
summary.setLength(0);
comment.setLength(0);
String regex = "/\\*\\*(.(?!}-\\*/))*\\*/";
Pattern p = Pattern.compile(regex, Pattern.DOTALL);
Matcher m = p.matcher(sourceCode);
// Early out if there is no javadoc comment.
if (!m.find()) {
return;
}
String commentStr = m.group();
p = Pattern.compile("(/\\*\\*\\s*)" + // The comment header
"(((\\s*\\**\\s*)([^\n\r]*)[\n\r]+)*)" // The comment body
);
m = p.matcher(commentStr);
if (!m.find()) {
return;
}
String stripped = m.group(2);
p = Pattern.compile("^\\p{Blank}*\\**\\p{Blank}*", Pattern.MULTILINE);
String bareComment = p.matcher(stripped).replaceAll("");
BreakIterator iterator = BreakIterator.getSentenceInstance();
iterator.setText(bareComment);
int firstSentenceEnd = iterator.next();
if (firstSentenceEnd == BreakIterator.DONE) {
summary.append(bareComment);
} else {
summary.append(bareComment.substring(0, firstSentenceEnd));
}
comment.append(bareComment);
// Measure the indentation width on the second line to infer what the
// first line indent should be.
p = Pattern.compile("[^\\r\\n]+[\\r\\n]+(\\s+)\\*", Pattern.MULTILINE);
m = p.matcher(sourceCode);
int indentLen = 0;
if (m.find()) {
String indent = m.group(1);
indentLen = indent.length() - 1;
}
StringBuffer leadingIndent = new StringBuffer();
for (int i = 0; i < indentLen; ++i) {
leadingIndent.append(' ');
}
// By inserting at 0 here, we are assuming that sourceCode begins with
// /**, which is actually a function of how JDT sees a declaration start.
// If in the future, you see bogus indentation here, it means that this
// assumption is bad.
sourceCode.insert(0, leadingIndent);
}
}