/* * 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.JClassType; import com.google.gwt.core.ext.typeinfo.JMethod; 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 static 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 == null ? "class " + clazz + " (not found)" : 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 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); 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) { Element categoryElement = doc.createElement("category"); categoryElement.setAttribute("name", ""); categoryElement.setAttribute("description", ""); report.appendChild(categoryElement); return categoryElement; } } 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 Parser parser = new Parser(); private Map<String, Map<String, MetaData>> testMetaData = new HashMap<String, Map<String, MetaData>>(); private Map<TestCase, List<BenchmarkResults>> testResults = new HashMap<TestCase, List<BenchmarkResults>>(); 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 benchmarkClass The benchmark class to record. Must not be * <code>null</code>. */ public void addBenchmark(TreeLogger logger, JClassType 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 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, 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(); } /** * 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); } }