Fixes issue #702.  Adds benchmarking capability to GWT as an extension to JUnit.

Patch by: tobyr
Review by: mmendez



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@813 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/build.xml b/build.xml
index d00b037..0cecb31 100755
--- a/build.xml
+++ b/build.xml
@@ -6,7 +6,7 @@
 	<!-- "build" is the default when subprojects are directly targetted -->
 	<property name="target" value="build" />
 
-	<target name="dist" depends="dev, user, servlet, jni, doc, samples" description="Run the distributions">
+	<target name="dist" depends="dev, user, servlet, tools, jni, doc, samples" description="Run the distributions">
 		<gwt.ant dir="distro-source" />
 	</target>
 
@@ -18,6 +18,10 @@
 		<gwt.ant dir="user" />
 	</target>
 
+	<target name="tools" depends="buildtools, user" description="Run tools">
+		<gwt.ant dir="tools" />
+	</target>
+
 	<target name="servlet" depends="buildtools, user" description="Run servlet">
 		<gwt.ant dir="servlet" />
 	</target>
diff --git a/dev/core/src/com/google/gwt/dev/generator/ast/BaseNode.java b/dev/core/src/com/google/gwt/dev/generator/ast/BaseNode.java
new file mode 100644
index 0000000..ef26c7c
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/generator/ast/BaseNode.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2007 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.dev.generator.ast;
+
+/**
+ * A simple base class for implementing an AST node.
+ */
+public abstract class BaseNode implements Node {
+
+  public abstract String toCode();
+
+  public String toString() {
+    return toCode();
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/generator/ast/Expression.java b/dev/core/src/com/google/gwt/dev/generator/ast/Expression.java
new file mode 100644
index 0000000..db851ca
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/generator/ast/Expression.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2007 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.dev.generator.ast;
+
+/**
+ * A Node that represents a Java expression. An expression is a parsable value
+ * that is a subset of a statement. For example,
+ *
+ * <ul> <li>foo( a, b )</li> <li>14</li> <li>11 / 3</li> <li>x</li> </ul>
+ *
+ * are all Expressions.
+ */
+public class Expression extends BaseNode {
+
+  String code;
+
+  public Expression() {
+    code = "";
+  }
+
+  public Expression(String code) {
+    this.code = code;
+  }
+
+  public String toCode() {
+    return code;
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/generator/ast/ForLoop.java b/dev/core/src/com/google/gwt/dev/generator/ast/ForLoop.java
new file mode 100644
index 0000000..c048be4
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/generator/ast/ForLoop.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2007 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.dev.generator.ast;
+
+import java.util.List;
+
+/**
+ * A Node that represents a for loop.
+ */
+public class ForLoop implements Statements {
+
+  StatementsList body;
+
+  String initializer;
+
+  String label;
+
+  String step;
+
+  String test;
+
+  /**
+   * Creates a ForLoop with a null body.
+   *
+   */
+  public ForLoop(String initializer, String test, String step) {
+    this(initializer, test, step, null);
+  }
+
+  /**
+   * Constructs a new ForLoop node.
+   *
+   * @param initializer The initializer Expression.
+   * @param test        The test Expression.
+   * @param step        The step Expression. May be null.
+   * @param statements The statements for the body of the loop.
+   * May be null.
+   */
+  public ForLoop(String initializer, String test, String step,
+      Statements statements) {
+    this.initializer = initializer;
+    this.test = test;
+    this.step = step;
+    this.body = new StatementsList();
+
+    if (statements != null) {
+      body.getStatements().add(statements);
+    }
+  }
+
+  public List getStatements() {
+    return body.getStatements();
+  }
+
+  public void setLabel(String label) {
+    this.label = label;
+  }
+
+  public String toCode() {
+    String loop = "for ( " + initializer + "; " + test + "; " + step + " ) {\n"
+        +
+        body.toCode() + "\n" +
+        "}\n";
+
+    return label != null ? label + ": " + loop : loop;
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/generator/ast/MethodCall.java b/dev/core/src/com/google/gwt/dev/generator/ast/MethodCall.java
new file mode 100644
index 0000000..8b0a2c3
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/generator/ast/MethodCall.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2007 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.dev.generator.ast;
+
+import java.util.List;
+
+/**
+ * A Node that represents a method call Expression, for example,
+ * foo( a, b, c ).
+ */
+public class MethodCall extends Expression {
+
+  List arguments;
+
+  String name;
+
+  /**
+   * Creates a new MethodCall Expression.
+   *
+   * @param name The name of the method. This must contain the qualified
+   * target expression if it is not implicitly this. For example, "foo.bar".
+   *
+   * @param arguments The list of Expressions that are the arguments for the
+   * call.
+   */
+  public MethodCall(String name, List arguments) {
+    this.name = name;
+    this.arguments = arguments;
+
+    StringBuffer call = new StringBuffer(name + "(");
+
+    if (arguments != null) {
+      call.append(" ");
+      for (int i = 0; i < arguments.size(); ++i) {
+        call.append(arguments.get(i));
+        if (i < arguments.size() - 1) {
+          call.append(", ");
+        }
+      }
+      call.append(" ");
+    }
+
+    call.append(")");
+    super.code = call.toString();
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/generator/ast/Node.java b/dev/core/src/com/google/gwt/dev/generator/ast/Node.java
new file mode 100644
index 0000000..b254865
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/generator/ast/Node.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2007 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.dev.generator.ast;
+
+/**
+ * An AST node. Must be able to return it's code representation as a String.
+ *
+ */
+public interface Node {
+
+  /**
+   * The Java code representation of this Node.
+   *
+   * @return a non-null String.
+   */
+  public String toCode();
+}
diff --git a/dev/core/src/com/google/gwt/dev/generator/ast/Statement.java b/dev/core/src/com/google/gwt/dev/generator/ast/Statement.java
new file mode 100644
index 0000000..84b2653
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/generator/ast/Statement.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2007 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.dev.generator.ast;
+
+import java.util.List;
+import java.util.Arrays;
+
+/**
+ * A Node that represents a single Java statement.
+ */
+public class Statement extends BaseNode implements Statements {
+
+  String code;
+
+  Expression expression;
+
+  private List list;
+
+  /**
+   * Creates a new statement from a String of code representing an Expression.
+   * Automatically appends a semicolon to <code>code</code>.
+   *
+   * @param code An Expression. Should not end with a semicolon.
+   */
+  public Statement(String code) {
+    this.code = code;
+    this.list = Arrays.asList(new Statement[]{this});
+  }
+
+  /**
+   * Creates a new statement from an Expression.
+   *
+   * @param expression A non-null Expression.
+   */
+  public Statement(Expression expression) {
+    this.expression = expression;
+    this.list = Arrays.asList(new Statement[]{this});
+  }
+
+  /**
+   * Returns this single Statement as a List of Statements of size, one.
+   *
+   */
+  public List getStatements() {
+    return list;
+  }
+
+  public String toCode() {
+    if (expression != null) {
+      return expression.toCode() + ";";
+    } else {
+      return code + ";";
+    }
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/generator/ast/Statements.java b/dev/core/src/com/google/gwt/dev/generator/ast/Statements.java
new file mode 100644
index 0000000..e383c8f
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/generator/ast/Statements.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2007 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.dev.generator.ast;
+
+import java.util.List;
+
+/**
+ * Represents one or more groups of Statements. Can optionally be added to.
+ *
+ */
+public interface Statements extends Node {
+
+  /**
+   * Returns a list of Statements.
+   *
+   * @return a non-null list of Statements.
+   */
+  public List getStatements();
+}
diff --git a/dev/core/src/com/google/gwt/dev/generator/ast/StatementsList.java b/dev/core/src/com/google/gwt/dev/generator/ast/StatementsList.java
new file mode 100644
index 0000000..dbbf0b9
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/generator/ast/StatementsList.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2007 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.dev.generator.ast;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * An implementation of <code>Statements</code> that is composed of a list of
+ * <code>Statements</code>.
+ */
+public class StatementsList extends BaseNode implements Statements {
+
+  List/*<Statements>*/ statements;
+
+  /**
+   * Creates a new StatementsList with no Statements.
+   *
+   */
+  public StatementsList() {
+    statements = new ArrayList();
+  }
+
+  /**
+   * Returns the Statements that are in this list.
+   *
+   */
+  public List getStatements() {
+    return statements;
+  }
+
+  public String toCode() {
+    StringBuffer code = new StringBuffer();
+    for (Iterator it = statements.iterator(); it.hasNext();) {
+      Statements stmts = (Statements) it.next();
+      code.append(stmts.toCode()).append("\n");
+    }
+    return code.toString();
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/generator/ast/WhileLoop.java b/dev/core/src/com/google/gwt/dev/generator/ast/WhileLoop.java
new file mode 100644
index 0000000..d1e34ff
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/generator/ast/WhileLoop.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2007 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.dev.generator.ast;
+
+import java.util.List;
+
+/**
+ * A Node that represents a Java while loop.
+ */
+public class WhileLoop implements Statements {
+
+  StatementsList body;
+
+  String test;
+
+  /**
+   * Creates a new while loop with <code>test</code> as the test Expression.
+   * The WhileLoop has an empty body.
+   *
+   * @param test An Expression that must be of type boolean. Must be non-null.
+   */
+  public WhileLoop(String test) {
+    this.test = test;
+    this.body = new StatementsList();
+  }
+
+  public List getStatements() {
+    return body.getStatements();
+  }
+
+  public String toCode() {
+    return "while ( " + test + " ) {\n" +
+        body.toCode() + "\n" +
+        "}\n";
+  }
+}
diff --git a/distro-source/common.ant.xml b/distro-source/common.ant.xml
index 9daa178..e5904f5 100755
--- a/distro-source/common.ant.xml
+++ b/distro-source/common.ant.xml
@@ -8,10 +8,12 @@
 
 	<patternset id="chmod.executables">
 		<include name="*Creator" />
+		<include name="benchmarkViewer" />
 	</patternset>
 
 	<patternset id="chmod.not.executables">
 		<exclude name="*Creator" />
+		<exclude name="benchmarkViewer" />
 	</patternset>
 
 	<target name="filter" description="Filters distro files for versioning">
diff --git a/distro-source/linux/build.xml b/distro-source/linux/build.xml
index ac158c4..28a0ca4 100755
--- a/distro-source/linux/build.xml
+++ b/distro-source/linux/build.xml
@@ -11,6 +11,7 @@
 			<tarfileset file="${gwt.build.lib}/gwt-dev-${dist.platform}.jar" prefix="${project.distname}" />
 			<tarfileset file="${gwt.build.lib}/gwt-user.jar" prefix="${project.distname}" />
 			<tarfileset file="${gwt.build.lib}/gwt-servlet.jar" prefix="${project.distname}" />
+			<tarfileset file="${gwt.build.lib}/gwt-benchmark-viewer.jar" prefix="${project.distname}" />
 
 			<!-- jni libs-->
 			<tarfileset dir="${gwt.build.jni}/${dist.platform}" prefix="${project.distname}" />
diff --git a/distro-source/linux/src/benchmarkViewer b/distro-source/linux/src/benchmarkViewer
new file mode 100755
index 0000000..6530de5
--- /dev/null
+++ b/distro-source/linux/src/benchmarkViewer
@@ -0,0 +1,3 @@
+#!/bin/sh
+APPDIR=`dirname $0`;
+java  -Dcom.google.gwt.junit.reportPath="$1" -cp "$APPDIR/gwt-user.jar:$APPDIR/gwt-dev-linux.jar:$APPDIR/gwt-benchmark-viewer.jar" com.google.gwt.dev.GWTShell com.google.gwt.junit.viewer.ReportViewer/ReportViewer.html;
diff --git a/distro-source/mac/build.xml b/distro-source/mac/build.xml
index 96c5217..00f111f 100755
--- a/distro-source/mac/build.xml
+++ b/distro-source/mac/build.xml
@@ -11,6 +11,7 @@
 			<tarfileset file="${gwt.build.lib}/gwt-dev-${dist.platform}.jar" prefix="${project.distname}" />
 			<tarfileset file="${gwt.build.lib}/gwt-user.jar" prefix="${project.distname}" />
 			<tarfileset file="${gwt.build.lib}/gwt-servlet.jar" prefix="${project.distname}" />
+                        <tarfileset file="${gwt.build.lib}/gwt-benchmark-viewer.jar" prefix="${project.distname}" />
 
 			<!-- jni libs-->
 			<tarfileset dir="${gwt.build.jni}/${dist.platform}" prefix="${project.distname}" />
diff --git a/distro-source/mac/src/benchmarkViewer b/distro-source/mac/src/benchmarkViewer
new file mode 100755
index 0000000..d82dbf5
--- /dev/null
+++ b/distro-source/mac/src/benchmarkViewer
@@ -0,0 +1,3 @@
+#!/bin/sh
+APPDIR=`dirname $0`;
+java  -Dcom.google.gwt.junit.reportPath="$1" -XstartOnFirstThread -cp "$APPDIR/gwt-user.jar:$APPDIR/gwt-dev-mac.jar:$APPDIR/gwt-benchmark-viewer.jar" com.google.gwt.dev.GWTShell com.google.gwt.junit.viewer.ReportViewer/ReportViewer.html;
diff --git a/distro-source/windows/build.xml b/distro-source/windows/build.xml
index 9939c4c..8fce756 100755
--- a/distro-source/windows/build.xml
+++ b/distro-source/windows/build.xml
@@ -11,6 +11,7 @@
 			<zipfileset file="${gwt.build.lib}/gwt-dev-${dist.platform}.jar" prefix="${project.distname}" />
 			<zipfileset file="${gwt.build.lib}/gwt-user.jar" prefix="${project.distname}" />
 			<zipfileset file="${gwt.build.lib}/gwt-servlet.jar" prefix="${project.distname}" />
+                        <zipfileset file="${gwt.build.lib}/gwt-benchmark-viewer.jar" prefix="${project.distname}" />
 
 			<!-- jni libs-->
 			<zipfileset dir="${gwt.build.jni}/${dist.platform}" prefix="${project.distname}" />
diff --git a/distro-source/windows/src/benchmarkViewer.cmd b/distro-source/windows/src/benchmarkViewer.cmd
new file mode 100755
index 0000000..81753bf
--- /dev/null
+++ b/distro-source/windows/src/benchmarkViewer.cmd
@@ -0,0 +1 @@
+@java -Dcom.google.gwt.junit.reportPath="%1" -cp "%~dp0/gwt-user.jar;%~dp0/gwt-dev-windows.jar;%~dp0/gwt-benchmark-viewer.jar" com.google.gwt.dev.GWTShell com.google.gwt.junit.viewer.ReportViewer/ReportViewer.html;
diff --git a/doc/build.xml b/doc/build.xml
index d9015ca..74bc02b 100644
--- a/doc/build.xml
+++ b/doc/build.xml
@@ -9,7 +9,7 @@
 	<!-- Platform shouldn't matter here, just picking one -->
 	<property.ensure name="gwt.dev.jar" location="${gwt.build.lib}/gwt-dev-linux.jar" />
 
-	<property name="USER_PKGS" value="com.google.gwt.core.client;com.google.gwt.core.ext;com.google.gwt.core.ext.typeinfo;com.google.gwt.i18n.client;com.google.gwt.json.client;com.google.gwt.junit.client;com.google.gwt.user.client;com.google.gwt.user.client.rpc;com.google.gwt.user.client.ui;com.google.gwt.user.server.rpc;com.google.gwt.xml.client;com.google.gwt.http.client" />
+	<property name="USER_PKGS" value="com.google.gwt.core.client;com.google.gwt.core.ext;com.google.gwt.core.ext.typeinfo;com.google.gwt.i18n.client;com.google.gwt.json.client;com.google.gwt.junit.client;com.google.gwt.user.client;com.google.gwt.user.client.rpc;com.google.gwt.user.client.ui;com.google.gwt.user.server.rpc;com.google.gwt.xml.client;com.google.gwt.http.client;com.google.gwt.junit.viewer.client" />
 	<property name="LANG_PKGS" value="java.lang;java.util" />
 	<property name="DOC_PKGS" value="com.google.gwt.doc" />
 
@@ -29,6 +29,7 @@
 		<pathelement location="${gwt.user.jar}" />
 		<pathelement location="${gwt.dev.jar}" />
 		<pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
+                <pathelement location="${gwt.tools.lib}/jfreechart/jfreechart-1.0.3.jar" />
 	</path>
 
 	<!--
@@ -245,7 +246,7 @@
 					<arg value="-sourcepath" />
 					<arg pathref="USER_SOURCE_PATH" />
 					<arg value="-examplepackages" />
-					<arg value="com.google.gwt.examples;com.google.gwt.examples.i18n;com.google.gwt.examples.http.client;com.google.gwt.examples.rpc.server" />
+					<arg value="com.google.gwt.examples;com.google.gwt.examples.i18n;com.google.gwt.examples.http.client;com.google.gwt.examples.rpc.server;com.google.gwt.examples.benchmarks" />
 					<arg value="-packages" />
 					<arg value="${USER_PKGS}" />
 				</java>
diff --git a/doc/src/com/google/gwt/doc/DeveloperGuide.java b/doc/src/com/google/gwt/doc/DeveloperGuide.java
index 23bdc1d..51c4699 100644
--- a/doc/src/com/google/gwt/doc/DeveloperGuide.java
+++ b/doc/src/com/google/gwt/doc/DeveloperGuide.java
@@ -2229,6 +2229,34 @@
     public static class JUnitAsync {
     }
 
+    /**
+     * GWT's <a href="http://www.junit.org">JUnit</a> integration provides
+     * special support for creating and reporting on benchmarks. Specifically,
+     * GWT has introduced a new {@link com.google.gwt.junit.client.Benchmark}
+     * class which provides built-in facilities for common benchmarking needs.
+     *
+     * To take advantage of benchmarking support, take the following steps:
+     * <ol>
+     *   <li>Review the documentation on
+     * {@link com.google.gwt.junit.client.Benchmark}. Take a look at the
+     * example benchmark code.</li>
+     *   <li>Create your own benchmark by subclassing
+     * {@link com.google.gwt.junit.client.Benchmark}.
+     * Execute your benchmark like you would any normal JUnit test. By default,
+     * the test results are written to a report XML file in your working
+     * directory.</li>
+     *   <li>Run <code>benchmarkViewer</code> to browse visualizations
+     * (graphs/charts) of your report data. The <code>benchmarkViewer</code> is
+     * a GWT tool in the root of your GWT installation directory that displays
+     * benchmark reports.</li>
+     * </ol>
+     *
+     * @title Benchmarking
+     * @synopsis How to use GWT's JUnit support to create and report on
+     * benchmarks to help you optimize your code.
+     */
+    public static class JUnitBenchmarking {
+    }
   }
 
   /**
diff --git a/eclipse/tools/benchmark-viewer/.classpath b/eclipse/tools/benchmark-viewer/.classpath
new file mode 100644
index 0000000..3b6e066
--- /dev/null
+++ b/eclipse/tools/benchmark-viewer/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry combineaccessrules="false" kind="src" path="/gwt-user"/>
+	<classpathentry kind="var" path="GWT_TOOLS/lib/jfreechart/jfreechart-1.0.3.jar"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/eclipse/tools/benchmark-viewer/.project b/eclipse/tools/benchmark-viewer/.project
new file mode 100644
index 0000000..5e12fb6
--- /dev/null
+++ b/eclipse/tools/benchmark-viewer/.project
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>benchmark-viewer</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+	<linkedResources>
+		<link>
+			<name>src</name>
+			<type>2</type>
+			<locationURI>GWT_ROOT/tools/benchmark-viewer/src</locationURI>
+		</link>
+	</linkedResources>
+</projectDescription>
diff --git a/eclipse/user/.checkstyle b/eclipse/user/.checkstyle
index b705c56..a3f85e6 100644
--- a/eclipse/user/.checkstyle
+++ b/eclipse/user/.checkstyle
@@ -1,11 +1,11 @@
-<?xml version="1.0" encoding="UTF-8"?>

-<fileset-config file-format-version="1.2.0" simple-config="false">

-    <fileset name="Java source for production code" enabled="true" check-config-name="GWT Checks" local="false">

-        <file-match-pattern match-pattern=".*src.*\.java" include-pattern="true"/>

-    </fileset>

-    <fileset name="Java source for test cases" enabled="true" check-config-name="GWT Checks for Tests" local="false">

-        <file-match-pattern match-pattern=".*test.*com[/\\]google[/\\].*\.java$" include-pattern="true"/>

-        <file-match-pattern match-pattern=".*test.*test[/\\].*\.java$" include-pattern="true"/>

-    </fileset>

-    <filter name="NonSrcDirs" enabled="true"/>

-</fileset-config>

+<?xml version="1.0" encoding="UTF-8"?>
+<fileset-config file-format-version="1.2.0" simple-config="false">
+    <fileset name="Java source for production code" enabled="true" check-config-name="GWT Checks" local="false">
+        <file-match-pattern match-pattern=".*src.*\.java" include-pattern="true"/>
+    </fileset>
+    <fileset name="Java source for test cases" enabled="true" check-config-name="GWT Checks (Tests)" local="false">
+        <file-match-pattern match-pattern=".*test.*com[/\\]google[/\\].*\.java$" include-pattern="true"/>
+        <file-match-pattern match-pattern=".*test.*test[/\\].*\.java$" include-pattern="true"/>
+    </fileset>
+    <filter name="NonSrcDirs" enabled="true"/>
+</fileset-config>
diff --git a/eclipse/user/.classpath b/eclipse/user/.classpath
index 3b1d4bc..2591d4c 100644
--- a/eclipse/user/.classpath
+++ b/eclipse/user/.classpath
@@ -4,9 +4,10 @@
 	<classpathentry kind="src" path="core/javadoc"/>
 	<classpathentry kind="src" path="core/test"/>
 	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
-	<classpathentry exported="true" sourcepath="/GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2-src.zip" kind="var" path="GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2.jar"/>
-	<classpathentry exported="true" sourcepath="/GWT_TOOLS/lib/junit/junit-3.8.1-src.zip" kind="var" path="GWT_TOOLS/lib/junit/junit-3.8.1.jar"/>
+	<classpathentry exported="true" kind="var" path="GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2.jar" sourcepath="/GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2-src.zip"/>
+	<classpathentry exported="true" kind="var" path="GWT_TOOLS/lib/junit/junit-3.8.1.jar" sourcepath="/GWT_TOOLS/lib/junit/junit-3.8.1-src.zip"/>
 	<classpathentry exported="true" kind="var" path="GWT_TOOLS/lib/tomcat/servlet-api-2.4.jar"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/gwt-dev-linux"/>
+	<classpathentry kind="var" path="GWT_TOOLS/lib/eclipse/jdt-3.1.1.jar" sourcepath="/GWT_TOOLS/lib/eclipse/jdt-3.1.1-src.zip"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/eclipse/user/.project b/eclipse/user/.project
index 3bd7525..26f6979 100644
--- a/eclipse/user/.project
+++ b/eclipse/user/.project
@@ -24,7 +24,7 @@
 		<link>
 			<name>core</name>
 			<type>2</type>
-			<location>GWT_ROOT/user</location>
+			<locationURI>GWT_ROOT/user</locationURI>
 		</link>
 	</linkedResources>
 </projectDescription>
diff --git a/tools/benchmark-viewer/build.xml b/tools/benchmark-viewer/build.xml
new file mode 100755
index 0000000..8b78fce
--- /dev/null
+++ b/tools/benchmark-viewer/build.xml
@@ -0,0 +1,95 @@
+<project name="benchmark-viewer" default="build" basedir=".">
+	<property name="gwt.root" location="../.." />
+	<property name="project.tail" value="tools/benchmark-viewer" />
+	<import file="${gwt.root}/common.ant.xml" />
+
+	<!--
+		Default hosted mode test cases
+	-->
+	<fileset id="default.hosted.tests" dir="${javac.junit.out}">
+		<include name="**/*Test.class" />
+	</fileset>
+
+	<!--
+		Default web mode test cases
+	-->
+	<fileset id="default.web.tests" dir="${javac.junit.out}">
+		<include name="**/*Test.class" />
+	</fileset>
+
+	<!-- Platform shouldn't matter here, just picking one -->
+	<property.ensure name="gwt.dev.jar" location="${gwt.build.lib}/gwt-dev-linux.jar" />
+	<property.ensure name="gwt.user.jar" location="${gwt.build.lib}/gwt-user.jar" />
+
+	<target name="compile" description="Compile all class files">
+		<mkdir dir="${javac.out}" />
+		<gwt.javac>
+			<classpath>
+				<pathelement location="${gwt.tools.lib}/tomcat/servlet-api-2.4.jar" />
+				<pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
+				<pathelement location="${gwt.tools.lib}/jfreechart/jfreechart-1.0.3.jar" />
+				<pathelement location="${gwt.dev.jar}" />
+				<pathelement location="${gwt.user.jar}" />
+			</classpath>
+		</gwt.javac>
+	</target>
+
+	<target name="compile.tests" description="Compiles the test code for this project">
+		<mkdir dir="${javac.junit.out}" />
+		<gwt.javac srcdir="test" destdir="${javac.junit.out}">
+			<classpath>
+				<pathelement location="${javac.out}" />
+				<pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
+				<pathelement location="${gwt.dev.staging.jar}" />
+			</classpath>
+		</gwt.javac>
+	</target>
+
+	<target name="build" depends="compile" description="Build and package this project">
+		<mkdir dir="${gwt.build.lib}" />
+		<gwt.jar>
+			<fileset dir="src" excludes="**/package.html" />
+			<fileset dir="${javac.out}" />
+                        <zipfileset src="${gwt.tools.lib}/jfreechart/gnujaxp.jar"/>
+                        <zipfileset src="${gwt.tools.lib}/jfreechart/itext-1.4.6.jar"/>
+                        <zipfileset src="${gwt.tools.lib}/jfreechart/jcommon-1.0.6.jar"/>
+                        <zipfileset src="${gwt.tools.lib}/jfreechart/jfreechart-1.0.3.jar"/>
+		</gwt.jar>
+	</target>
+
+	<target name="checkstyle" description="Static analysis of source">
+		<gwt.checkstyle>
+			<fileset dir="src"/>
+		</gwt.checkstyle>
+	</target>
+
+	<target name="remoteweb-test" description="Run a remoteweb test at the given host and path">
+		<echo message="Performing remote browser testing at rmi://${gwt.remote.browser}" />
+		<gwt.junit test.args="-port ${gwt.junit.port} -out www -web -remoteweb rmi://${gwt.remote.browser}" test.out="${junit.out}/${gwt.remote.browser}" test.cases="default.web.tests" />
+	</target>
+
+	<target name="test" depends="compile, compile.tests" description="Run hosted-mode, web-mode and remoteweb tests for this project.">
+		<property.ensure name="distro.built" location="${gwt.dev.staging.jar}" message="GWT must be built before performing any tests.  This can be fixed by running ant in the ${gwt.root} directory." />
+
+		<!--
+			Run hosted and web mode tests for the platform on which this build
+			is executing
+		-->
+		<parallel threadcount="1">
+			<gwt.junit test.args="-port ${gwt.junit.port}" test.out="${junit.out}/${build.host.platform}-hosted-mode" test.cases="default.hosted.tests" />
+
+			<gwt.junit test.args="-port ${gwt.junit.port} -out www -web" test.out="${junit.out}/${build.host.platform}-web-mode" test.cases="default.web.tests" />
+
+			<!--
+				Run remote browser testing for the comma delimited list of remote browsers
+			-->
+			<foreach list="${gwt.remote.browsers}" delimiter="," parallel="true" maxThreads="1" target="remoteweb-test" param="gwt.remote.browser" />
+		</parallel>
+	</target>
+
+	<target name="clean" description="Cleans this project's intermediate and output files">
+		<delete dir="${project.build}" />
+		<delete file="${project.lib}" />
+	</target>
+</project>
+
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/ReportViewer.gwt.xml b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/ReportViewer.gwt.xml
new file mode 100644
index 0000000..75bca80
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/ReportViewer.gwt.xml
@@ -0,0 +1,15 @@
+<!--                                                                        -->
+<!-- Copyright 2007 Google Inc. All Rights Reserved.                        -->
+<!-- Deferred binding rules for browser selection.                          -->
+<!--                                                                        -->
+<module>
+  <inherits name="com.google.gwt.user.User"/>
+  <inherits name="com.google.gwt.http.HTTP"/>
+
+  <source path="client"/>
+
+  <entry-point class="com.google.gwt.junit.viewer.client.ReportViewer"/>
+
+  <servlet path='/test_reports' class='com.google.gwt.junit.viewer.server.ReportServerImpl'/>
+  <servlet path='/test_images' class='com.google.gwt.junit.viewer.server.ReportImageServer'/>
+</module>
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Benchmark.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Benchmark.java
new file mode 100644
index 0000000..7ff901c
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Benchmark.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2007 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.junit.viewer.client;
+
+import com.google.gwt.user.client.rpc.IsSerializable;
+
+import java.util.List;
+
+/**
+ * A data object for Benchmark.
+ */
+public class Benchmark implements IsSerializable {
+
+  private String className;
+
+  private String description;
+
+  private String name;
+
+  /**
+   * @gwt.typeArgs <com.google.gwt.junit.viewer.client.Result>
+   */
+  private List/*<Result>*/ results;
+
+  private String sourceCode;
+
+  public String getClassName() {
+    return className;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public List/*<Result>*/ getResults() {
+    return results;
+  }
+
+  public String getSourceCode() {
+    return sourceCode;
+  }
+
+  public void setClassName(String className) {
+    this.className = className;
+  }
+
+  public void setDescription(String description) {
+    this.description = description;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public void setResults(List results) {
+    this.results = results;
+  }
+
+  public void setSourceCode(String sourceCode) {
+    this.sourceCode = sourceCode;
+  }
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/BrowserInfo.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/BrowserInfo.java
new file mode 100644
index 0000000..f40b85c
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/BrowserInfo.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2007 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.junit.viewer.client;
+
+/**
+ * Provides information about a browser (vendor,version,operating system,etc...)
+ * based on user agent and other easily accessible information.
+ *
+ * This is not meant to be a "detect script" to implement browser workarounds,
+ * but rather a "pretty printer" for the browser information.
+ *
+ * This code is a derivation of Browser Detect  v2.1.6 documentation:
+ * http://www.dithered.com/javascript/browser_detect/index.html license:
+ * http://creativecommons.org/licenses/by/1.0/ code by Chris Nott
+ * (chris[at]dithered[dot]com)
+ *
+ * It has been transliterated from JavaScript to Java with additional changes
+ * along the way.
+ */
+public class BrowserInfo {
+
+  /**
+   * Retrieves a "pretty" version of the browser version information.
+   *
+   * @param userAgent - The HTTP user agent string.
+   * @return A pretty-printed version of the browser including the a) vendor b)
+   *         version c) and operating system
+   */
+  public static String getBrowser(String userAgent) {
+
+    userAgent = userAgent.toLowerCase();
+
+    // browser engine name
+    boolean isGecko = userAgent.indexOf("gecko") != -1
+        && userAgent.indexOf("safari") == -1;
+    boolean isAppleWebKit = userAgent.indexOf("applewebkit") != -1;
+
+    // browser name
+    boolean isKonqueror = userAgent.indexOf("konqueror") != -1;
+    boolean isSafari = userAgent.indexOf("safari") != - 1;
+    boolean isOmniweb = userAgent.indexOf("omniweb") != - 1;
+    boolean isOpera = userAgent.indexOf("opera") != -1;
+    boolean isIcab = userAgent.indexOf("icab") != -1;
+    boolean isAol = userAgent.indexOf("aol") != -1;
+    boolean isIE = userAgent.indexOf("msie") != -1 && !isOpera && (
+        userAgent.indexOf("webtv") == -1);
+    boolean isMozilla = isGecko && userAgent.indexOf("gecko/") + 14 == userAgent
+        .length();
+    boolean isFirefox = userAgent.indexOf("firefox/") != -1
+        || userAgent.indexOf("firebird/") != -1;
+    boolean isNS = isGecko ? userAgent.indexOf("netscape") != -1 :
+        userAgent.indexOf("mozilla") != -1 &&
+            ! isOpera &&
+            ! isSafari &&
+            userAgent.indexOf("spoofer") == -1 &&
+            userAgent.indexOf("compatible") == -1 &&
+            userAgent.indexOf("webtv") == -1 &&
+            userAgent.indexOf("hotjava") == -1;
+
+    // spoofing and compatible browsers
+    boolean isIECompatible = userAgent.indexOf("msie") != -1 && !isIE;
+    boolean isNSCompatible = userAgent.indexOf("mozilla") != -1 && !isNS
+        && !isMozilla;
+
+    // rendering engine versions
+    String geckoVersion = isGecko ? userAgent.substring(
+        userAgent.lastIndexOf("gecko/") + 6,
+        userAgent.lastIndexOf("gecko/") + 14) : "-1";
+    String equivalentMozilla = isGecko ? userAgent
+        .substring(userAgent.indexOf("rv:") + 3) : "-1";
+    String appleWebKitVersion = isAppleWebKit ? userAgent
+        .substring(userAgent.indexOf("applewebkit/") + 12) : "-1";
+
+    // float versionMinor = parseFloat(navigator.appVersion);
+    String versionMinor = "";
+
+    // correct version number
+    if (isGecko && !isMozilla) {
+      versionMinor = userAgent.substring(
+          userAgent.indexOf("/", userAgent.indexOf("gecko/") + 6) + 1);
+    } else if (isMozilla) {
+      versionMinor = userAgent.substring(userAgent.indexOf("rv:") + 3);
+    } else if (isIE) {
+      versionMinor = userAgent.substring(userAgent.indexOf("msie ") + 5);
+    } else if (isKonqueror) {
+      versionMinor = userAgent.substring(userAgent.indexOf("konqueror/") + 10);
+    } else if (isSafari) {
+      versionMinor = userAgent.substring(userAgent.lastIndexOf("safari/") + 7);
+    } else if (isOmniweb) {
+      versionMinor = userAgent.substring(userAgent.lastIndexOf("omniweb/") + 8);
+    } else if (isOpera) {
+      versionMinor = userAgent.substring(userAgent.indexOf("opera") + 6);
+    } else if (isIcab) {
+      versionMinor = userAgent.substring(userAgent.indexOf("icab") + 5);
+    }
+
+    String version = getVersion(versionMinor);
+
+    // dom support
+    // boolean isDOM1 = (document.getElementById);
+    // boolean isDOM2Event = (document.addEventListener && document.removeEventListener);
+
+    // css compatibility mode
+    // this.mode = document.compatMode ? document.compatMode : "BackCompat";
+
+    // platform
+    boolean isWin = userAgent.indexOf("win") != -1;
+    boolean isWin32 = isWin && userAgent.indexOf("95") != -1 ||
+        userAgent.indexOf("98") != -1 ||
+        userAgent.indexOf("nt") != -1 ||
+        userAgent.indexOf("win32") != -1 ||
+        userAgent.indexOf("32bit") != -1 ||
+        userAgent.indexOf("xp") != -1;
+
+    boolean isMac = userAgent.indexOf("mac") != -1;
+    boolean isUnix = userAgent.indexOf("unix") != -1 ||
+        userAgent.indexOf("sunos") != -1 ||
+        userAgent.indexOf("bsd") != -1 ||
+        userAgent.indexOf("x11") != -1;
+
+    boolean isLinux = userAgent.indexOf("linux") != -1;
+
+    // specific browser shortcuts
+    /*
+    this.isNS4x = (this.isNS && this.versionMajor == 4);
+    this.isNS40x = (this.isNS4x && this.versionMinor < 4.5);
+    this.isNS47x = (this.isNS4x && this.versionMinor >= 4.7);
+    this.isNS4up = (this.isNS && this.versionMinor >= 4);
+    this.isNS6x = (this.isNS && this.versionMajor == 6);
+    this.isNS6up = (this.isNS && this.versionMajor >= 6);
+    this.isNS7x = (this.isNS && this.versionMajor == 7);
+    this.isNS7up = (this.isNS && this.versionMajor >= 7);
+
+    this.isIE4x = (this.isIE && this.versionMajor == 4);
+    this.isIE4up = (this.isIE && this.versionMajor >= 4);
+    this.isIE5x = (this.isIE && this.versionMajor == 5);
+    this.isIE55 = (this.isIE && this.versionMinor == 5.5);
+    this.isIE5up = (this.isIE && this.versionMajor >= 5);
+    this.isIE6x = (this.isIE && this.versionMajor == 6);
+    this.isIE6up = (this.isIE && this.versionMajor >= 6);
+
+    this.isIE4xMac = (this.isIE4x && this.isMac);
+    */
+
+    String name = isGecko ? "Gecko" :
+        isAppleWebKit ? "Apple WebKit" :
+        isKonqueror ? "Konqueror" :
+        isSafari ? "Safari" :
+        isOpera ? "Opera" :
+        isIE ? "IE" :
+        isMozilla ? "Mozilla" :
+        isFirefox ? "Firefox" :
+        isNS ? "Netscape" : "";
+
+    name += " " + version + " on " +
+        (isWin ? "Windows" :
+         isMac ? "Mac" :
+         isUnix ? "Unix" :
+         isLinux ? "Linux" : "Unknown");
+
+    return name;
+  }
+
+  // Reads the version from a string which begins with a version number
+  // and contains additional character data
+  private static String getVersion(String versionPlusCruft) {
+    for (int index = 0; index < versionPlusCruft.length(); ++index) {
+      char c = versionPlusCruft.charAt(index);
+      if (c != '.' && ! Character.isDigit(c)) {
+        return versionPlusCruft.substring(0, index);
+      }
+    }
+    return versionPlusCruft;
+  }
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Category.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Category.java
new file mode 100644
index 0000000..627a05f
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Category.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2007 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.junit.viewer.client;
+
+import com.google.gwt.user.client.rpc.IsSerializable;
+
+import java.util.List;
+
+/**
+ * A data object for Category.
+ */
+public class Category implements IsSerializable {
+
+  /**
+   * @gwt.typeArgs <com.google.gwt.junit.viewer.client.Benchmark>
+   */
+  private List benchmarks;
+
+  private String description;
+
+  private String name;
+
+  public List getBenchmarks() {
+    return benchmarks;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setBenchmarks(List benchmarks) {
+    this.benchmarks = benchmarks;
+  }
+
+  public void setDescription(String description) {
+    this.description = description;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Report.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Report.java
new file mode 100644
index 0000000..6429191
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Report.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2007 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.junit.viewer.client;
+
+import com.google.gwt.user.client.rpc.IsSerializable;
+
+import java.util.List;
+import java.util.Date;
+
+/**
+ * A data object for Report.
+ */
+public class Report implements IsSerializable {
+
+  /**
+   * @gwt.typeArgs <com.google.gwt.junit.viewer.client.Category>
+   */
+  private List/*<Category>*/ categories;
+
+  private Date date;
+
+  private String dateString; // Temporary addition until we get better date
+
+  // formatting in GWT
+  private String gwtVersion;
+
+  private String id;
+
+  public List/*<Category>*/ getCategories() {
+    return categories;
+  }
+
+  public Date getDate() {
+    return date;
+  }
+
+  public String getDateString() {
+    return dateString;
+  }
+
+  public String getGwtVersion() {
+    return gwtVersion;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public ReportSummary getSummary() {
+    int numTests = 0;
+    boolean testsPassed = true;
+
+    for (int i = 0; i < categories.size(); ++i) {
+      Category c = (Category) categories.get(i);
+      List benchmarks = c.getBenchmarks();
+      numTests += benchmarks.size();
+    }
+
+    return new ReportSummary(id, date, dateString, numTests, testsPassed);
+  }
+
+  public void setCategories(List categories) {
+    this.categories = categories;
+  }
+
+  public void setDate(Date date) {
+    this.date = date;
+  }
+
+  public void setDateString(String dateString) {
+    this.dateString = dateString;
+  }
+
+  public void setGwtVersion(String gwtVersion) {
+    this.gwtVersion = gwtVersion;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+}
+
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/ReportServer.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/ReportServer.java
new file mode 100644
index 0000000..a50e2c7
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/ReportServer.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2007 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.junit.viewer.client;
+
+import com.google.gwt.user.client.rpc.RemoteService;
+
+import java.util.List;
+
+/**
+ * Provides Benchmark report summaries and details. This service must be running
+ * in order to view the reports via ReportViewer.
+ *
+ * @see com.google.gwt.junit.viewer.server.ReportServerImpl
+ * @see ReportViewer
+ */
+public interface ReportServer extends RemoteService {
+
+  /**
+   * Returns the full details of the specified report.
+   *
+   * @param reportId The id of the report. Originates from the ReportSummary.
+   * @return the matching Report, or null if the Report could not be found.
+   */
+  public Report getReport(String reportId);
+
+  /**
+   * Returns a list of summaries of all the Benchmark reports.
+   *
+   * @return a non-null list of ReportSummary
+   */
+  public List/*<ReportSummary>*/ getReportSummaries();
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/ReportServerAsync.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/ReportServerAsync.java
new file mode 100644
index 0000000..ccd4f1c
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/ReportServerAsync.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2007 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.junit.viewer.client;
+
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/**
+ * The asynchronous interface for ReportServer.
+ *
+ * @see ReportServer
+ */
+public interface ReportServerAsync {
+
+  public void getReport(String reportId, AsyncCallback callback);
+
+  public void getReportSummaries(AsyncCallback callback);
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/ReportSummary.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/ReportSummary.java
new file mode 100644
index 0000000..7e8d89e
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/ReportSummary.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2007 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.junit.viewer.client;
+
+import com.google.gwt.user.client.rpc.IsSerializable;
+
+import java.util.Date;
+
+/**
+ * A data object summarizing the results of a report.
+ */
+public class ReportSummary implements IsSerializable {
+
+  private boolean allTestsSucceeded;
+
+  private Date date;
+
+  // A temporary addition until we get better date formatting in GWT user
+  private String dateString;
+
+  private String id;
+
+  // in GWT
+  private int numTests;
+
+  public ReportSummary() {
+  }
+
+  public ReportSummary(String id, Date date, String dateString, int numTests,
+      boolean allTestsSucceeded) {
+    this.id = id;
+    this.date = date;
+    this.dateString = dateString;
+    this.numTests = numTests;
+    this.allTestsSucceeded = allTestsSucceeded;
+  }
+
+  public boolean allTestsSucceeded() {
+    return allTestsSucceeded;
+  }
+
+  public Date getDate() {
+    return date;
+  }
+
+  public String getDateString() {
+    return dateString;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public int getNumTests() {
+    return numTests;
+  }
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/ReportViewer.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/ReportViewer.java
new file mode 100644
index 0000000..33782c6
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/ReportViewer.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright 2007 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.junit.viewer.client;
+
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.CellPanel;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.HasHorizontalAlignment;
+import com.google.gwt.user.client.ui.HasVerticalAlignment;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.rpc.ServiceDefTarget;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.http.client.URL;
+
+import java.util.List;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * The application for viewing benchmark reports. In order for the ReportViewer
+ * to operate correctly, you must have both the {@link ReportServer} RPC and
+ * {@link com.google.gwt.junit.viewer.server.ReportImageServer} servlets up and
+ * running within a servlet container.
+ *
+ * <code>ReportViewer's</code> GWT XML module is configured to start these
+ * servlets by default. Just start <code>ReportViewer</code> in hosted mode, and
+ * GWT will run them within its own embedded servlet engine. For example,
+ *
+ * <pre>java -cp &lt;classpath&gt; com.google.gwt.dev.GWTShell -out
+ * ReportViewerShell/www
+ * com.google.gwt.junit.viewer.ReportViewer/ReportViewer.html</pre>
+ *
+ * You can configure the location where ReportServer reads the benchmark reports
+ * from by setting the system property named in
+ * {@link com.google.gwt.junit.client.Benchmark#REPORT_PATH}.
+ */
+public class ReportViewer implements EntryPoint {
+
+  private static class MutableBool {
+
+    boolean value;
+
+    MutableBool(boolean value) {
+      this.value = value;
+    }
+  }
+
+  private static final String baseUrl = GWT.getModuleBaseURL();
+
+  private static final String imageServer = baseUrl + "test_images/";
+
+  HTML detailsLabel;
+
+  Report report;
+
+  VerticalPanel reportPanel;
+
+  ReportServerAsync reportServer;
+
+  FlexTable reportTable;
+
+  HTML statusLabel;
+
+  List/*<ReportSummary>*/ summaries;
+
+  VerticalPanel summariesPanel;
+
+  FlexTable summariesTable;
+
+  CellPanel topPanel;
+
+  public void onModuleLoad() {
+
+    init();
+
+    // Asynchronously load the summaries
+    ServiceDefTarget target = (ServiceDefTarget) GWT.create(ReportServer.class);
+    target.setServiceEntryPoint(GWT.getModuleBaseURL() + "test_reports");
+    reportServer = (ReportServerAsync) target;
+
+    reportServer.getReportSummaries(new AsyncCallback() {
+      public void onFailure(Throwable caught) {
+        String msg = "<p>" + caught.toString() + "</p>" +
+                     "<p>Is your path to the reports correct?</p>";
+        statusLabel.setHTML(msg);
+      }
+
+      public void onSuccess(Object result) {
+        summaries = (List/*<ReportSummary>*/) result;
+        if (summaries != null) {
+          if (summaries.size() == 0) {
+            statusLabel.setText(
+                "There are no benchmark reports available in this folder.");
+          }
+          Collections.sort(summaries, new Comparator() {
+            public int compare(Object o1, Object o2) {
+              ReportSummary r1 = (ReportSummary) o1;
+              ReportSummary r2 = (ReportSummary) o2;
+              return r2.getDate().compareTo(r1.getDate()); // most recent first
+            }
+          });
+          displaySummaries();
+        }
+      }
+    });
+  }
+
+  private FlexTable createReportTable() {
+
+    FlexTable topTable = new FlexTable();
+
+    FlexTable tempReportTable = new FlexTable();
+    tempReportTable.setBorderWidth(1);
+    tempReportTable.setCellPadding(5);
+    tempReportTable.setWidget(0, 0, new Label("Date Created"));
+    tempReportTable.setWidget(0, 1, new Label("GWT Version"));
+
+    if (report == null) {
+      tempReportTable
+          .setWidget(1, 0, new Label("No currently selected report."));
+      tempReportTable.getFlexCellFormatter().setColSpan(1, 0, 3);
+      return tempReportTable;
+    }
+
+    detailsLabel.setHTML("<h3>" + report.getId() + " details </h3>");
+    tempReportTable.setWidget(1, 0, new Label(report.getDateString()));
+    tempReportTable.setWidget(1, 1, new Label(report.getGwtVersion()));
+
+    // topTable.setWidget( 0, 0, tempReportTable );
+    int currentRow = 1;
+
+    Collections.sort(report.getCategories(), new Comparator() {
+      public int compare(Object o1, Object o2) {
+        Category c1 = (Category) o1;
+        Category c2 = (Category) o2;
+        return c1.getName().compareTo(c2.getName());
+      }
+    }); // Should be done once in the RPC
+
+    for (int i = 0; i < report.getCategories().size(); ++i) {
+      Category c = (Category) report.getCategories().get(i);
+
+      if (!c.getName().equals("")) {
+        FlexTable categoryTable = new FlexTable();
+        categoryTable.setBorderWidth(0);
+        categoryTable.setCellPadding(5);
+        categoryTable.setText(0, 0, c.getName());
+        categoryTable.getFlexCellFormatter()
+            .setStyleName(0, 0, "benchmark-category");
+
+        categoryTable.setWidget(0, 1, new Label("Description"));
+        categoryTable.setWidget(1, 0, new Label(c.getName()));
+        categoryTable.setWidget(1, 1, new Label(c.getDescription()));
+
+        topTable.setWidget(currentRow++, 0, categoryTable);
+      }
+
+      Collections.sort(c.getBenchmarks(), new Comparator() {
+        public int compare(Object o1, Object o2) {
+          Benchmark b1 = (Benchmark) o1;
+          Benchmark b2 = (Benchmark) o2;
+          return b1.getName().compareTo(b2.getName());
+        }
+      }); // Should be done once in the RPC
+
+      for (int j = 0; j < c.getBenchmarks().size(); ++j) {
+        Benchmark benchmark = (Benchmark) c.getBenchmarks().get(j);
+
+        FlexTable benchmarkTable = new FlexTable();
+        benchmarkTable.setBorderWidth(0);
+        benchmarkTable.setCellPadding(5);
+        benchmarkTable.setText(0, 0, benchmark.getName());
+        // benchmarkTable.setText( 0, 1, benchmark.getDescription());
+        benchmarkTable.setWidget(1, 0,
+            new HTML("<pre>" + benchmark.getSourceCode() + "</pre>"));
+        benchmarkTable.getFlexCellFormatter()
+            .setStyleName(0, 0, "benchmark-name");
+        // benchmarkTable.getFlexCellFormatter().setStyleName( 0, 1, "benchmark-description" );
+        benchmarkTable.getFlexCellFormatter()
+            .setStyleName(1, 0, "benchmark-code");
+
+        // TODO(tobyr) Provide detailed benchmark information.
+        // Following commented code is a step in that direction. 
+/*
+        benchmarkTable.setWidget( 0, 1, new Label( "Description"));
+        benchmarkTable.setWidget( 0, 2, new Label( "Class Name"));
+        benchmarkTable.setWidget( 0, 3, new Label( "Source Code"));
+        benchmarkTable.setWidget( 1, 0, new Label( benchmark.getName()));
+        benchmarkTable.setWidget( 1, 1, new Label( benchmark.getDescription()));
+        benchmarkTable.setWidget( 1, 2, new Label( benchmark.getClassName()));
+        benchmarkTable.setWidget( 1, 3, new HTML( "<pre>" + benchmark.getSourceCode() + "</pre>"));
+*/
+        topTable.setWidget(currentRow++, 0, benchmarkTable);
+
+        FlexTable resultsTable = new FlexTable();
+        resultsTable.setBorderWidth(0);
+        resultsTable.setCellPadding(5);
+        FlexTable.FlexCellFormatter resultsFormatter = resultsTable
+            .getFlexCellFormatter();
+        topTable.setWidget(currentRow++, 0, resultsTable);
+
+        Collections.sort(benchmark.getResults(), new Comparator() {
+          public int compare(Object o1, Object o2) {
+            Result r1 = (Result) o1;
+            Result r2 = (Result) o2;
+            return r1.getAgent().compareTo(r2.getAgent());
+          }
+        }); // Should be done once in the RPC
+
+        final List trialsTables = new ArrayList();
+        final MutableBool isVisible = new MutableBool(false);
+
+        Button visibilityButton = new Button("Show Data", new ClickListener() {
+          public void onClick(Widget sender) {
+            isVisible.value = !isVisible.value;
+            for (int i = 0; i < trialsTables.size(); ++i) {
+              Widget w = (Widget) trialsTables.get(i);
+              w.setVisible(isVisible.value);
+            }
+            String name = isVisible.value ? "Hide Data" : "Show Data";
+            ((Button) sender).setText(name);
+          }
+        });
+
+        for (int k = 0; k < benchmark.getResults().size(); ++k) {
+          Result result = (Result) benchmark.getResults().get(k);
+
+          /*
+          resultsTable.setWidget( 0, 0, new Label( "Result Agent"));
+          resultsTable.setWidget( 0, 1, new Label( "Host"));
+          resultsTable.setWidget( 0, 2, new Label( "Graph"));
+          resultsTable.setWidget( 1, 0, new Label( result.getAgent()));
+          resultsTable.setWidget( 1, 1, new Label( result.getHost()));
+          */
+
+          resultsTable.setWidget(0, k, new Image(getImageUrl(report.getId(),
+              c.getName(), benchmark.getClassName(), benchmark.getName(),
+              result.getAgent())));
+
+          /*
+          FlexTable allTrialsTable = new FlexTable();
+          allTrialsTable.setBorderWidth(1);
+          allTrialsTable.setCellPadding(5);
+          FlexTable.CellFormatter allTrialsFormatter = allTrialsTable
+              .getFlexCellFormatter();
+          topTable.setWidget(currentRow++, 0, allTrialsTable);
+          allTrialsTable.setWidget(0, k, trialsTable);
+          allTrialsFormatter
+              .setAlignment(0, k, HasHorizontalAlignment.ALIGN_CENTER,
+                  HasVerticalAlignment.ALIGN_TOP);
+          */
+
+          resultsFormatter
+              .setAlignment(2, k, HasHorizontalAlignment.ALIGN_LEFT,
+                  HasVerticalAlignment.ALIGN_TOP);
+
+          // A table of straight data for all trials for an agent
+          FlexTable trialsTable = new FlexTable();
+          trialsTable.setVisible(false);
+          trialsTables.add(trialsTable);
+          trialsTable.setBorderWidth(1);
+          trialsTable.setCellPadding(5);
+
+          if (k == 0) {
+            resultsTable.setWidget(1, k, visibilityButton);
+            resultsFormatter.setColSpan(1, k, benchmark.getResults().size());
+            resultsFormatter
+                .setAlignment(1, k, HasHorizontalAlignment.ALIGN_LEFT,
+                    HasVerticalAlignment.ALIGN_MIDDLE);
+          }
+
+          resultsTable.setWidget(2, k, trialsTable);
+
+          List trials = result.getTrials();
+          int numTrials = trials.size();
+          int numVariables = ((Trial) trials.get(0)).getVariables().size();
+
+          Trial sampleTrial = (Trial) trials.get(0);
+          Map variables = sampleTrial.getVariables();
+          List variableNames = new ArrayList(variables.keySet());
+          Collections.sort(variableNames);
+
+          // Write out the variable column headers
+          for (int varIndex = 0; varIndex < numVariables; ++varIndex) {
+            String varName = (String) variableNames.get(varIndex);
+            trialsTable.setWidget(0, varIndex, new HTML(varName));
+          }
+
+          // Timing header
+          trialsTable.setWidget(0, numVariables, new HTML("Timing (ms)"));
+
+          // Write out all the trial data
+          for (int l = 0; l < numTrials; ++l) {
+            Trial trial = (Trial) trials.get(l);
+
+            // Write the variable values
+            for (int varIndex = 0; varIndex < numVariables; ++varIndex) {
+              String varName = (String) variableNames.get(varIndex);
+              String varValue = (String) trial.getVariables().get(varName);
+              trialsTable.setWidget(l + 1, varIndex, new HTML(varValue));
+            }
+
+            // Write out the timing data
+            String data = null;
+            if (trial.getException() != null) {
+              data = trial.getException();
+            } else {
+              data = trial.getRunTimeMillis() + "";
+            }
+            trialsTable.setWidget(l + 1, numVariables, new HTML(data));
+          }
+        }
+      }
+    }
+
+    return topTable;
+  }
+
+  private FlexTable createSummariesTable() {
+
+    FlexTable tempSummariesTable = new FlexTable();
+    tempSummariesTable.setCellPadding(5);
+    tempSummariesTable.setBorderWidth(1);
+    tempSummariesTable.setWidget(0, 0, new Label("Id"));
+    tempSummariesTable.setWidget(0, 1, new Label("Date Created"));
+    tempSummariesTable.setWidget(0, 2, new Label("Tests"));
+    // tempSummariesTable.setWidget( 0, 3, new Label( "Succeeded"));
+
+    if (summaries == null) {
+      tempSummariesTable.setWidget(1, 0, new Label("Fetching reports..."));
+      tempSummariesTable.getFlexCellFormatter().setColSpan(1, 0, 4);
+      return tempSummariesTable;
+    }
+
+    for (int i = 0; i < summaries.size(); ++i) {
+      ReportSummary summary = (ReportSummary) summaries.get(i);
+      int index = i + 1;
+      Label idLabel = new Label(summary.getId());
+      idLabel.addClickListener(new ClickListener() {
+        public void onClick(Widget w) {
+          getReportDetails(((Label) w).getText());
+        }
+      });
+      tempSummariesTable.setWidget(index, 0, idLabel);
+      tempSummariesTable
+          .setWidget(index, 1, new Label(summary.getDateString()));
+      tempSummariesTable
+          .setWidget(index, 2, new Label(summary.getNumTests() + ""));
+      // tempSummariesTable.setWidget( index, 3, new Label(summary.allTestsSucceeded() + ""));
+    }
+
+    return tempSummariesTable;
+  }
+
+  private void displayReport() {
+    FlexTable table = createReportTable();
+    reportPanel.remove(reportTable);
+    reportTable = table;
+    reportPanel.insert(reportTable, 1);
+  }
+
+//  private native String getDocumentLocation() /*-{
+//    return window.location;
+//  }-*/;
+
+  private void displaySummaries() {
+    FlexTable table = createSummariesTable();
+    summariesPanel.remove(summariesTable);
+    summariesTable = table;
+    summariesPanel.insert(summariesTable, 1);
+  }
+
+  private String encode(String str) {
+    if (str.equals("")) {
+      return str;
+    }
+    return URL.encodeComponent(str);
+  }
+
+  private String getImageUrl(String report, String category, String testClass,
+      String testMethod, String agent) {
+    return imageServer + encode(report) + "/" +
+        encode(category) + "/" +
+        encode(testClass) + "/" +
+        encode(testMethod) + "/" +
+        encode(agent);
+  }
+
+  /**
+   * Loads report details asynchronously for a given report.
+   *
+   * @param id the non-null id of the report
+   */
+  private void getReportDetails(String id) {
+    statusLabel.setText("Retrieving the report...");
+    reportServer.getReport(id, new AsyncCallback() {
+      public void onFailure(Throwable caught) {
+        statusLabel.setText(caught.toString());
+      }
+
+      public void onSuccess(Object result) {
+        report = (Report) result;
+        statusLabel.setText("Finished fetching report details.");
+        displayReport();
+      }
+    });
+  }
+
+  private void init() {
+    topPanel = new VerticalPanel();
+
+    summariesPanel = new VerticalPanel();
+    summariesPanel.add(new HTML("<h3>Benchmark Reports</h3>"));
+    summariesTable = createSummariesTable();
+    summariesPanel.add(summariesTable);
+
+    reportPanel = new VerticalPanel();
+    detailsLabel = new HTML("<h3>Report Details</h3>");
+    reportPanel.add(detailsLabel);
+    reportTable = createReportTable();
+    // reportPanel.add( reportTable );
+
+    topPanel.add(summariesPanel);
+    CellPanel spacerPanel = new HorizontalPanel();
+    spacerPanel.setSpacing(10);
+    spacerPanel.add(new Label());
+    topPanel.add(spacerPanel);
+    topPanel.add(reportPanel);
+    final RootPanel root = RootPanel.get();
+
+    root.add(topPanel);
+
+    statusLabel = new HTML("Select a report.");
+    root.add(statusLabel);
+  }
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Result.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Result.java
new file mode 100644
index 0000000..79453d5
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Result.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2007 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.junit.viewer.client;
+
+import com.google.gwt.user.client.rpc.IsSerializable;
+
+import java.util.List;
+
+/**
+ * A data object for Benchmark results.
+ */
+public class Result implements IsSerializable {
+
+  private String agent;
+
+  private String host;
+
+  private List trials;
+
+  public Result() {
+  }
+
+  public String getAgent() {
+    return agent;
+  }
+
+  public String getHost() {
+    return host;
+  }
+
+  public List getTrials() {
+    return trials;
+  }
+
+  public void setAgent(String agent) {
+    this.agent = agent;
+  }
+
+  public void setHost(String host) {
+    this.host = host;
+  }
+
+  public void setTrials(List trials) {
+    this.trials = trials;
+  }
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Trial.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Trial.java
new file mode 100644
index 0000000..b949db0
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/client/Trial.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2007 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.junit.viewer.client;
+
+import com.google.gwt.user.client.rpc.IsSerializable;
+
+import java.util.Map;
+import java.util.HashMap;
+
+/**
+ * A data object for Trial.
+ */
+public class Trial implements IsSerializable {
+
+  String exception;
+
+  double runTimeMillis;
+
+  Map/*<String,String>*/ variables;
+
+  public Trial() {
+    this.variables = new HashMap();
+  }
+
+  public String getException() {
+    return exception;
+  }
+
+  public double getRunTimeMillis() {
+    return runTimeMillis;
+  }
+
+  /**
+   * Returns the names and values of the variables used in the test. If there
+   * were no variables, the map is empty.
+   */
+  public Map getVariables() {
+    return variables;
+  }
+
+  public void setException(String exception) {
+    this.exception = exception;
+  }
+
+  public void setRunTimeMillis(double runTimeMillis) {
+    this.runTimeMillis = runTimeMillis;
+  }
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/public/ReportViewer.html b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/public/ReportViewer.html
new file mode 100644
index 0000000..0d43991
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/public/ReportViewer.html
@@ -0,0 +1,9 @@
+<html>
+        <head>
+                <meta name='gwt:module' content='com.google.gwt.junit.viewer.ReportViewer'>
+                <title>ReportViewer</title>
+        </head>
+        <body bgcolor="white">
+                <script language="javascript" src="gwt.js"></script>
+        </body>
+</html>
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/BenchmarkXml.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/BenchmarkXml.java
new file mode 100644
index 0000000..184e7ed
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/BenchmarkXml.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2007 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.junit.viewer.server;
+
+import com.google.gwt.junit.viewer.client.Benchmark;
+
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Converts an XML element to a Benchmark object.
+ *
+ */
+class BenchmarkXml {
+
+  public static Benchmark fromXml( Element element ) {
+    Benchmark benchmark = new Benchmark();
+    benchmark.setClassName(element.getAttribute( "class" ));
+    benchmark.setName(element.getAttribute( "name" ));
+    benchmark.setDescription(element.getAttribute( "description" ));
+
+    List children = ReportXml.getElementChildren( element, "result" );
+    benchmark.setResults( new ArrayList/*<Result>*/(children.size()));
+    for ( int i = 0; i < children.size(); ++i ) {
+      benchmark.getResults().add( ResultXml.fromXml( (Element) children.get( i )));
+    }
+
+    Element code = ReportXml.getElementChild( element, "source_code" );
+    if ( code != null ) {
+      benchmark.setSourceCode( ReportXml.getText(code) );
+    }
+
+    return benchmark;
+  }
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/CategoryXml.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/CategoryXml.java
new file mode 100644
index 0000000..a06bbea
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/CategoryXml.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2007 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.junit.viewer.server;
+
+import com.google.gwt.junit.viewer.client.Category;
+
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Converts an XML Element to a Category object.
+ *
+ */
+class CategoryXml {
+  public static Category fromXml( Element element ) {
+    Category category = new Category();
+    category.setName(element.getAttribute( "name" ));
+    category.setDescription(element.getAttribute( "description" ));
+
+    List/*<Element>*/ children = ReportXml.getElementChildren( element, "benchmark" );
+    category.setBenchmarks(new ArrayList/*<Benchmark>*/( children.size()));
+    for ( int i = 0; i < children.size(); ++i ) {
+      category.getBenchmarks().add( BenchmarkXml.fromXml( (Element) children.get( i )));
+    }
+
+    return category;
+  }
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ReportDatabase.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ReportDatabase.java
new file mode 100644
index 0000000..b91a7d6
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ReportDatabase.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2007 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.junit.viewer.server;
+
+import com.google.gwt.junit.viewer.client.Report;
+import com.google.gwt.junit.viewer.client.ReportSummary;
+import com.google.gwt.junit.client.Benchmark;
+import com.google.gwt.user.client.rpc.IsSerializable;
+
+import org.w3c.dom.Document;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.DocumentBuilder;
+
+/**
+ * Serves up benchmark reports created during JUnit execution.
+ *
+ * The benchmark reports are read from the path specified by the system property
+ * named <code>Benchmark.REPORT_PATH</code>. In the case the property is not set,
+ * they are read from the user's current working directory.
+ *
+ */
+public class ReportDatabase {
+
+  public static class BadPathException extends RuntimeException {
+    String path;
+    public BadPathException( String path ) {
+      super("The path " + path + " does not exist." );
+      this.path = path;
+    }
+    public String getPath() {
+      return path;
+    }
+  }
+
+  private static class ReportEntry {
+    private ReportSummary summary;
+    private Report report;
+    private long lastModified;
+    public ReportEntry( Report report, ReportSummary summary, long lastModified ) {
+      this.report = report;
+      this.summary = summary;
+      this.lastModified = lastModified;
+    }
+  }
+
+  private static class ReportFile {
+    File file;
+    long lastModified;
+    ReportFile( File f ) {
+      this.file = f;
+      this.lastModified = f.lastModified();
+    }
+  }
+
+  /**
+   * The amount of time to go between report updates.
+   *
+   */
+  private static final int UPDATE_DURATION_MILLIS = 30000;
+
+  private static ReportDatabase database = new ReportDatabase();
+
+  public static ReportDatabase getInstance() {
+    return database;
+  }
+
+  private static String getReportId( File f ) {
+    return f.getName();
+  }
+
+  /**
+   * A list of all reports by id.
+   */
+  private Map/*<String,ReportEntry>*/ reports = new HashMap/*<String,ReportEntry>*/();
+
+  /**
+   * The last time we updated our reports.
+   *
+   */
+  private long lastUpdateMillis = -1L;
+
+  /**
+   * Lock for updating from file system.
+   * (Guarantees a single update while not holding reportsLock open).
+   *
+   */
+  private Object updateLock = new Object();
+
+  /**
+   * Are we currently undergoing updating?
+   *
+   */
+  private boolean updating = false;
+
+  /**
+   * Lock for reports.
+   *
+   */
+  private Object reportsLock = new Object();
+
+  /**
+   * The path to read benchmark reports from.
+   *
+   */
+  private final String reportPath;
+
+  private ReportDatabase() throws BadPathException {
+    String path = System.getProperty( Benchmark.REPORT_PATH );
+    if ( path == null || path.trim().equals( "" ) ) {
+      path = System.getProperty( "user.dir" );
+    }
+    reportPath = path;
+
+    if (! new File(reportPath).exists()) {
+      throw new BadPathException(reportPath);
+    }
+  }
+
+  public Report getReport( String reportId ) {
+    synchronized ( reportsLock ) {
+      ReportEntry entry = (ReportEntry)reports.get( reportId );
+      return entry == null ? null : entry.report;
+    }
+  }
+
+  public List/*<ReportSummary>*/ getReportSummaries() {
+
+    /**
+     * There are probably ways to make this faster, but I've taken
+     * basic precautions to try to make this scale ok with multiple clients.
+     */
+
+    boolean update = false;
+
+    // See if we need to do an update
+    // Go ahead and let others continue reading, even if an update is required.
+    synchronized ( updateLock ) {
+      if ( ! updating ) {
+        long currentTime = System.currentTimeMillis();
+
+        if ( currentTime > lastUpdateMillis + UPDATE_DURATION_MILLIS ) {
+          update = updating = true;
+        }
+      }
+    }
+
+    if ( update ) {
+      updateReports();
+    }
+
+    synchronized ( reportsLock ) {
+      List/*<ReportSummary>*/ summaries = new ArrayList/*<ReportSummary>*/( reports.size() );
+      for ( Iterator it = reports.values().iterator(); it.hasNext(); ) {
+        ReportEntry entry = (ReportEntry) it.next();
+        summaries.add( entry.summary );
+      }
+      return summaries;
+    }
+  }
+
+  private void updateReports() {
+
+    File path = new File( reportPath );
+
+    File[] files = path.listFiles( new FilenameFilter() {
+      public boolean accept( File f, String name ) {
+        return name.startsWith("report-") && name.endsWith( ".xml" );
+      }
+    } );
+
+    Map filesToUpdate = new HashMap();
+    Map filesById = new HashMap();
+    for ( int i = 0; i < files.length; ++i ) {
+      File f = files[ i ];
+      filesById.put( getReportId( f ), new ReportFile( f ) );
+    }
+
+    // Lock temporarily so we can determine what needs updating
+    // (This could be a read-lock - not a general read-write lock,
+    // if we moved dead report removal outside of this critical section).
+    synchronized ( reportsLock ) {
+
+      // Add reports which need to be updated or are new
+      for ( int i = 0; i < files.length; ++i ) {
+        File file = files[ i ];
+        String reportId = getReportId( file );
+        ReportEntry entry = (ReportEntry) reports.get( reportId );
+        if ( entry == null || entry.lastModified < file.lastModified() ) {
+          filesToUpdate.put( reportId, null );
+        }
+      }
+
+      // Remove reports which no longer exist
+      for ( Iterator it = reports.keySet().iterator(); it.hasNext(); ) {
+        String id = (String)it.next();
+        if ( filesById.get(id) == null) {
+          it.remove();
+        }
+      }
+    }
+
+    try {
+      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+      factory.setIgnoringElementContentWhitespace( true );
+      factory.setIgnoringComments( true );
+      DocumentBuilder builder = factory.newDocumentBuilder();
+
+      for ( Iterator it = filesToUpdate.keySet().iterator(); it.hasNext(); ) {
+        String id = (String)it.next();
+        ReportFile reportFile = (ReportFile) filesById.get(id);
+        String filePath = reportFile.file.getAbsolutePath();
+        Document doc = builder.parse(filePath);
+        Report report = ReportXml.fromXml( doc.getDocumentElement() );
+        report.setId(id);
+        ReportSummary summary = report.getSummary();
+        long lastModified = new File(filePath).lastModified();
+        filesToUpdate.put(id, new ReportEntry( report, summary, lastModified));
+      }
+
+      // Update the reports
+      synchronized ( reportsLock ) {
+        for ( Iterator it = filesToUpdate.keySet().iterator(); it.hasNext(); ) {
+          String id = (String)it.next();
+          reports.put( id, filesToUpdate.get( id ));
+        }
+      }
+    } catch ( Exception e ) {
+      // Even if we got an error, we'll just try again on the next update
+      // This might happen if a report has only been partially written, for
+      // example.
+      e.printStackTrace();
+    }
+
+    synchronized ( updateLock ) {
+      updating = false;
+      lastUpdateMillis = System.currentTimeMillis();
+    }
+  }
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ReportImageServer.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ReportImageServer.java
new file mode 100644
index 0000000..e8752bc
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ReportImageServer.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright 2007 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.junit.viewer.server;
+
+import com.google.gwt.junit.viewer.client.Report;
+import com.google.gwt.junit.viewer.client.Category;
+import com.google.gwt.junit.viewer.client.Benchmark;
+import com.google.gwt.junit.viewer.client.Result;
+import com.google.gwt.junit.viewer.client.Trial;
+import com.google.gwt.junit.viewer.client.BrowserInfo;
+
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.encoders.EncoderUtil;
+import org.jfree.chart.encoders.ImageFormat;
+import org.jfree.chart.plot.PlotOrientation;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.InputStream;
+import java.io.ByteArrayInputStream;
+import java.net.URLDecoder;
+import java.awt.image.BufferedImage;
+import java.awt.Font;
+import java.util.List;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.TreeMap;
+import java.util.ArrayList;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.ServletException;
+
+/**
+ * Serves up report images for the ReportViewer application. Generates the
+ * charts/graphs which contain the benchmarking data for a report.
+ *
+ * <p>This servlet requires the name of the report file, the category, the
+ * benchmark class, the test method, and the browser agent.<p>
+ *
+ * <p>An Example URI:<pre>
+ * /com.google.gwt.junit.viewer.ReportViewer/test_images/
+ *   report-12345.xml/
+ *   RemoveCategory/
+ *   com.google.gwt.junit.client.ArrayListAndVectorBenchmark/
+ *   testArrayListRemoves/
+ *   Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.12) Gecko/20050920/
+ * </pre></p>
+ */
+public class ReportImageServer extends HttpServlet {
+
+  private static final String charset = "UTF-8";
+
+  private static void copy(InputStream in, OutputStream out)
+      throws IOException {
+    byte[] buf = new byte[512];
+
+    while (true) {
+      int bytesRead = in.read(buf);
+      if (bytesRead == -1) {
+        break;
+      }
+      out.write(buf, 0, bytesRead);
+    }
+  }
+
+  private JFreeChart createChart(String testName, Result result, String title) {
+
+    // Display the trial data - we might need meta information from the result
+    // that tells us how many total variables there are, what types they are of,
+    // etc....
+    List trials = result.getTrials();
+    Trial firstTrial = (Trial) trials.get(0);
+
+    // Pick the domain and series variables for our graph.
+    // Right now we only handle up to two "user" variables.
+    // We set the domain variable to the be the one containing the most unique values.
+    int numVariables = firstTrial.getVariables().size();
+
+    String domainVariable = null;
+    String seriesVariable = null;
+
+    Map/*<String,Set<String>>*/ variableValues = null;
+
+    if (numVariables == 1) {
+      domainVariable = (String) firstTrial.getVariables().keySet().iterator()
+          .next();
+    } else {
+      // TODO(tobyr): Do something smarter, like allow the user to specify which variables
+      // are domain and series, along with the variables which are held constant.
+
+      variableValues = new HashMap();
+
+      for (int i = 0; i < trials.size(); ++i) {
+        Trial trial = (Trial) trials.get(i);
+        Map variables = trial.getVariables();
+
+        for (Iterator it = variables.entrySet().iterator(); it.hasNext();) {
+          Map.Entry entry = (Map.Entry) it.next();
+          String variable = (String) entry.getKey();
+          String value = (String) entry.getValue();
+          Set set = (Set) variableValues.get(variable);
+          if (set == null) {
+            set = new TreeSet();
+            variableValues.put(variable, set);
+          }
+          set.add(value);
+        }
+      }
+
+      TreeMap numValuesMap = new TreeMap();
+
+      for (Iterator it = variableValues.entrySet().iterator(); it.hasNext();) {
+        Map.Entry entry = (Map.Entry) it.next();
+        String variable = (String) entry.getKey();
+        Set values = (Set) entry.getValue();
+        Integer numValues = new Integer(values.size());
+        List variables = (List) numValuesMap.get(numValues);
+        if (variables == null) {
+          variables = new ArrayList();
+          numValuesMap.put(numValues, variables);
+        }
+        variables.add(variable);
+      }
+
+      if (numValuesMap.values().size() > 0) {
+        domainVariable = (String) ((List) numValuesMap
+            .get(numValuesMap.lastKey())).get(0);
+        seriesVariable = (String) ((List) numValuesMap
+            .get(numValuesMap.firstKey())).get(0);
+      }
+    }
+
+    String valueTitle = "time (ms)"; // This axis is time across all charts.
+
+    if (numVariables == 0) {
+      // Show a bar graph, with a single centered simple bar
+      // 0 variables means there is only 1 trial
+      Trial trial = (Trial) trials.iterator().next();
+
+      DefaultCategoryDataset data = new DefaultCategoryDataset();
+      data.addValue(trial.getRunTimeMillis(), "result", "result");
+
+      return ChartFactory.createBarChart(title, testName, valueTitle,
+          data, PlotOrientation.VERTICAL, false, false, false);
+    } else if (numVariables == 1) {
+
+      // Show a line graph with only 1 series
+      // Or.... choose between a line graph and a bar graph depending upon whether the
+      // type of the domain is numeric.
+
+      XYSeriesCollection data = new XYSeriesCollection();
+
+      XYSeries series = new XYSeries(domainVariable);
+
+      for (Iterator it = trials.iterator(); it.hasNext();) {
+        Trial trial = (Trial) it.next();
+        if ( trial.getException() != null) {
+          continue;
+        }
+        double time = trial.getRunTimeMillis();
+        String domainValue = (String) trial.getVariables().get(domainVariable);
+        series.add(Double.parseDouble(domainValue), time);
+      }
+
+      data.addSeries(series);
+
+      return ChartFactory.createXYLineChart(title, domainVariable, valueTitle,
+          data, PlotOrientation.VERTICAL, false, false, false);
+    } else if (numVariables == 2) {
+      // Show a line graph with multiple series
+      XYSeriesCollection data = new XYSeriesCollection();
+
+      Set seriesValues = (Set) variableValues.get(seriesVariable);
+
+      for (Iterator it = seriesValues.iterator(); it.hasNext();) {
+        String seriesValue = (String) it.next();
+        XYSeries series = new XYSeries(seriesValue);
+
+        for (Iterator trialsIt = trials.iterator(); trialsIt.hasNext();) {
+          Trial trial = (Trial) trialsIt.next();
+          if ( trial.getException() != null) {
+            continue;
+          }
+          Map variables = trial.getVariables();
+          if (variables.get(seriesVariable).equals(seriesValue)) {
+            double time = trial.getRunTimeMillis();
+            String domainValue = (String) trial.getVariables()
+                .get(domainVariable);
+            series.add(Double.parseDouble(domainValue), time);
+          }
+        }
+        data.addSeries(series);
+      }
+
+      return ChartFactory.createXYLineChart(title, domainVariable, valueTitle,
+          data, PlotOrientation.VERTICAL, true, true, false);
+    }
+
+    return null;
+
+    // Sample JFreeChart code for creating certain charts:
+    // Leaving this around until we can handle multivariate charts in dimensions
+    // greater than two.
+
+    // Code for creating a category data set - probably better with a bar chart instead of line chart
+    /*
+    DefaultCategoryDataset data = new DefaultCategoryDataset();
+    String series = domainVariable;
+
+    for ( Iterator it = trials.iterator(); it.hasNext(); ) {
+      Trial trial = (Trial) it.next();
+      double time = trial.getRunTimeMillis();
+      String domainValue = (String) trial.getVariables().get( domainVariable );
+      data.addValue( time, series, domainValue );
+    }
+
+    String title = "";
+    String categoryTitle = domainVariable;
+    PlotOrientation orientation = PlotOrientation.VERTICAL;
+
+    chart = ChartFactory.createLineChart( title, categoryTitle, valueTitle, data, orientation, true, true, false );
+    */
+
+    /*
+    DefaultCategoryDataset data = new DefaultCategoryDataset();
+    String series1 = "firefox";
+    String series2 = "ie";
+
+    data.addValue( 1.0, series1, "1024");
+    data.addValue( 2.0, series1, "2048");
+    data.addValue( 4.0, series1, "4096");
+    data.addValue( 8.0, series1, "8192");
+
+    data.addValue( 2.0, series2, "1024");
+    data.addValue( 4.0, series2, "2048");
+    data.addValue( 8.0, series2, "4096");
+    data.addValue( 16.0, series2,"8192");
+
+    String title = "";
+    String categoryTitle = "size";
+    PlotOrientation orientation = PlotOrientation.VERTICAL;
+
+    chart = ChartFactory.createLineChart( title, categoryTitle, valueTitle, data, orientation, true, true, false );
+    */
+  }
+
+  public void doGet(HttpServletRequest request, HttpServletResponse response)
+      throws ServletException, IOException {
+    handleRequest(request, response);
+  }
+
+  public void doPost(HttpServletRequest request, HttpServletResponse response)
+      throws ServletException, IOException {
+    handleRequest(request, response);
+  }
+
+  private Benchmark getBenchmarkByName(List benchmarks, String name) {
+    for (Iterator it = benchmarks.iterator(); it.hasNext();) {
+      Benchmark benchmark = (Benchmark) it.next();
+      if (benchmark.getName().equals(name)) {
+        return benchmark;
+      }
+    }
+    return null;
+  }
+
+  private Category getCategoryByName(List categories, String categoryName) {
+    for (Iterator catIt = categories.iterator(); catIt.hasNext();) {
+      Category category = (Category) catIt.next();
+      if (category.getName().equals(categoryName)) {
+        return category;
+      }
+    }
+    return null;
+  }
+
+  private Result getResultsByAgent(List results, String agent) {
+    for (Iterator it = results.iterator(); it.hasNext();) {
+      Result result = (Result) it.next();
+      if (result.getAgent().equals(agent)) {
+        return result;
+      }
+    }
+    return null;
+  }
+
+  private void handleRequest(HttpServletRequest request,
+      HttpServletResponse response)
+      throws IOException, ServletException {
+
+    String uri = request.getRequestURI();
+    String requestString = uri.split("test_images/")[1];
+    String[] requestParams = requestString.split("/");
+
+    String reportName = URLDecoder.decode(requestParams[0], charset);
+    String categoryName = URLDecoder.decode(requestParams[1], charset);
+    String className = URLDecoder.decode(requestParams[2], charset);
+    String testName = URLDecoder.decode(requestParams[3], charset);
+    String agent = URLDecoder.decode(requestParams[4], charset);
+
+    ReportDatabase db = ReportDatabase.getInstance();
+    Report report = db.getReport(reportName);
+    List categories = report.getCategories();
+    Category category = getCategoryByName(categories, categoryName);
+    List benchmarks = category.getBenchmarks();
+    Benchmark benchmark = getBenchmarkByName(benchmarks, testName);
+    List results = benchmark.getResults();
+    Result result = getResultsByAgent(results, agent);
+
+    String title = BrowserInfo.getBrowser(agent);
+    JFreeChart chart = null;
+
+    try {
+      chart = createChart(testName, result, title);
+
+      if (chart == null) {
+        super.doGet(request, response);
+        return;
+      }
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+
+    chart.getTitle().setFont(Font.decode("Arial"));
+
+    // Try to fit all the graphs into a 1024 window, with a min of 240 and a max of 480
+    final int graphWidth = Math
+        .max(240, Math.min(480, (1024 - 10 * results.size()) / results.size()));
+    BufferedImage img = chart.createBufferedImage(graphWidth, 240);
+    byte[] image = EncoderUtil.encode(img, ImageFormat.PNG);
+
+    response.setContentType("image/png");
+
+    OutputStream output = response.getOutputStream();
+    copy(new ByteArrayInputStream(image), output);
+  }
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ReportServerImpl.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ReportServerImpl.java
new file mode 100644
index 0000000..f40ded6
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ReportServerImpl.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2007 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.junit.viewer.server;
+
+import com.google.gwt.user.server.rpc.RemoteServiceServlet;
+import com.google.gwt.junit.viewer.client.ReportServer;
+import com.google.gwt.junit.viewer.client.Report;
+
+import java.util.List;
+
+/**
+ * Implements the ReportServer RPC interface.
+ */
+public class ReportServerImpl extends RemoteServiceServlet
+    implements ReportServer {
+
+  public Report getReport(String reportId) {
+    return ReportDatabase.getInstance().getReport(reportId);
+  }
+
+  /**
+   * @gwt.typeArgs <com.google.gwt.junit.viewer.client.ReportSummary>
+   */
+  public List/*<ReportSummary>*/ getReportSummaries() {
+    return ReportDatabase.getInstance().getReportSummaries();
+  }
+}
\ No newline at end of file
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ReportXml.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ReportXml.java
new file mode 100644
index 0000000..6a3dee0
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ReportXml.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2007 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.junit.viewer.server;
+
+import com.google.gwt.junit.viewer.client.Report;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Node;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Date;
+
+/**
+ * Hydrates a Report from its XML representation.
+ */
+class ReportXml {
+
+  /**
+   * Hydrates a Report from its XML representation.
+   *
+   * @param element The XML element to hydrate from.
+   * @return a new report (with null id)
+   */
+  public static Report fromXml(Element element) {
+
+    Report report = new Report();
+    String dateString = element.getAttribute("date");
+
+    try {
+      DateFormat format = DateFormat.getDateTimeInstance();
+      Date d = format.parse(dateString);
+      report.setDate(d);
+      report.setDateString(format.format(d));
+    } catch (ParseException e) {
+      // let date remain null if it doesn't parse correctly
+    }
+
+    report.setGwtVersion(element.getAttribute("gwt_version"));
+
+    List/*<Element>*/ children = getElementChildren(element, "category");
+    report.setCategories(new ArrayList/*<Category>*/(children.size()));
+    for (int i = 0; i < children.size(); ++i) {
+      report.getCategories()
+          .add(CategoryXml.fromXml((Element) children.get(i)));
+    }
+
+    return report;
+  }
+
+  static Element getElementChild(Element e, String name) {
+    NodeList children = e.getElementsByTagName(name);
+    return children.getLength() == 0 ? null : (Element) children.item(0);
+  }
+
+  static List/*<Element>*/ getElementChildren(Element e, String name) {
+    NodeList children = e.getElementsByTagName(name);
+    int numElements = children.getLength();
+    List/*<Element>*/ elements = new ArrayList/*<Element>*/(numElements);
+    for (int i = 0; i < children.getLength(); ++i) {
+      Node n = children.item(i);
+      elements.add((Element) n);
+    }
+    return elements;
+  }
+
+  static String getText(Element e) {
+    Node n = e.getFirstChild();
+    return n == null ? null : n.getNodeValue();
+  }
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ResultXml.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ResultXml.java
new file mode 100644
index 0000000..cef6932
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/ResultXml.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2007 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.junit.viewer.server;
+
+import com.google.gwt.junit.viewer.client.Result;
+
+import org.w3c.dom.Element;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Hydrates a benchmark Result from an XML Element.
+ *
+ */
+public class ResultXml {
+  public static Result fromXml( Element element ) {
+    Result result = new Result();
+    result.setAgent(element.getAttribute( "agent" ));
+    result.setHost(element.getAttribute( "host" ));
+
+    List/*<Element>*/ children = ReportXml.getElementChildren( element, "trial" );
+
+    ArrayList trials = new ArrayList( children.size() );
+    result.setTrials(trials);
+
+    for ( int i = 0; i < children.size(); ++i ) {
+      trials.add( TrialXml.fromXml( (Element) children.get(i) ));
+    }
+
+    // TODO(tobyr) Put some type information in here for the variables
+
+    return result;
+  }
+}
diff --git a/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/TrialXml.java b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/TrialXml.java
new file mode 100644
index 0000000..530f8cd
--- /dev/null
+++ b/tools/benchmark-viewer/src/com/google/gwt/junit/viewer/server/TrialXml.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2007 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.junit.viewer.server;
+
+import com.google.gwt.junit.viewer.client.Trial;
+
+import org.w3c.dom.Element;
+
+import java.util.List;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Hydrates a benchmark Trial from an XML Element.
+ *
+ */
+class TrialXml {
+
+  public static Trial fromXml( Element element ) {
+    Trial trial = new Trial();
+
+    String timing = element.getAttribute( "timing" );
+
+    if ( timing != null ) {
+      trial.setRunTimeMillis(Double.parseDouble( timing ));
+    }
+
+    Element exception = ReportXml.getElementChild( element, "exception" );
+    if ( exception != null ) {
+      trial.setException( ReportXml.getText( exception ) );
+    }
+
+    List elements = ReportXml.getElementChildren( element, "variable" );
+
+    Map variables = trial.getVariables();
+
+    for ( Iterator it = elements.iterator(); it.hasNext(); ) {
+      Element e = (Element) it.next();
+      String name = e.getAttribute( "name" );
+      String value = e.getAttribute( "value" );
+      variables.put( name, value ) ;
+    }
+    
+    return trial;
+  }
+}
diff --git a/tools/build.xml b/tools/build.xml
new file mode 100755
index 0000000..f17e778
--- /dev/null
+++ b/tools/build.xml
@@ -0,0 +1,15 @@
+<project name="tools" default="build" basedir=".">
+	<property name="gwt.root" location=".." />
+	<property name="project.tail" value="tools" />
+	<import file="${gwt.root}/common.ant.xml" />
+
+        <!-- "build" is the default when subprojects are directly targetted -->
+        <property name="target" value="build" />
+      
+        <target name="build" depends="benchmark-viewer" description="Builds all targets"/>
+
+        <target name="benchmark-viewer" depends="" description="Run benchmark-viewer">
+                <gwt.ant dir="benchmark-viewer" />
+        </target>
+</project>
+
diff --git a/user/build.xml b/user/build.xml
index 7c2fa75..2957fd7 100755
--- a/user/build.xml
+++ b/user/build.xml
@@ -9,6 +9,11 @@
 	<fileset id="default.hosted.tests" dir="${javac.junit.out}" 
 			 includes="${gwt.junit.testcase.includes}">
 		<!--
+			Requires manual testing, because it generates intentional failures.
+		-->
+		<exclude name="com/google/gwt/junit/client/ParallelRemoteTest.class" />
+
+		<!--
 			Causes a security dialog to popup and subsequently blocks testing
 		-->
 		<exclude name="com/google/gwt/user/client/ui/FormPanelTest.class" />
@@ -95,6 +100,7 @@
 			<classpath>
 				<pathelement location="${gwt.tools.lib}/tomcat/servlet-api-2.4.jar" />
 				<pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
+				<pathelement location="${gwt.tools.lib}/jfreechart/jfreechart-1.0.3.jar" />
 				<pathelement location="${gwt.dev.jar}" />
 			</classpath>
 		</gwt.javac>
diff --git a/user/javadoc/com/google/gwt/examples/Benchmarks.gwt.xml b/user/javadoc/com/google/gwt/examples/Benchmarks.gwt.xml
new file mode 100644
index 0000000..b61f09d
--- /dev/null
+++ b/user/javadoc/com/google/gwt/examples/Benchmarks.gwt.xml
@@ -0,0 +1,3 @@
+<module>
+  <source path='benchmarks'/>
+</module>
diff --git a/user/javadoc/com/google/gwt/examples/benchmarks/AllocBenchmark.java b/user/javadoc/com/google/gwt/examples/benchmarks/AllocBenchmark.java
new file mode 100644
index 0000000..54e4c58
--- /dev/null
+++ b/user/javadoc/com/google/gwt/examples/benchmarks/AllocBenchmark.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2007 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.examples.benchmarks;
+
+import com.google.gwt.junit.client.Category;
+import com.google.gwt.junit.client.Benchmark;
+
+/**
+ * Provides profile statistics on allocation times for different kinds of
+ * objects.
+ *
+ * @gwt.benchmark.category com.google.gwt.user.client.ui.AllocBenchmark.AllocCategory
+ *
+ */
+public class AllocBenchmark extends Benchmark {
+
+  /**
+   * @gwt.benchmark.name Allocation Benchmarks
+   * @gwt.benchmark.description A series of benchmarks that tests the impact of
+   * different kinds of allocations.
+   *
+   */
+  class AllocCategory implements Category {
+  }
+
+  private static final int numAllocs = 1000;
+
+  public String getModuleName() {
+    return "com.google.gwt.examples.Benchmarks";
+  }
+
+  /**
+   * Allocates java.lang.Object in a for loop 1,000 times.
+   *
+   * The current version of the compiler lifts the declaration of obj outside
+   * of this loop and also does constant folding of numAllocs.
+   * Also, this loop allocs the GWT JS mirror for java.lang.Object
+   * <em>NOT</em> an empty JS object, for example.
+   *
+   */
+  public void testJavaObjectAlloc() {
+    for ( int i = 0; i < numAllocs; ++i ) {
+      Object obj = new Object();
+    }
+  }
+
+  /**
+   * Compares GWT mirror allocations of java.lang.Object to an empty JS object.
+   */
+  public native void testJsniObjectAlloc1() /*-{
+    for (var i = 0; i < @com.google.gwt.examples.benchmarks.AllocBenchmark::numAllocs; ++i ) {
+      var obj = {}; // An empty JS object alloc
+    }
+  }-*/;
+
+  /**
+   * Like version 1, but also folds the constant being used in the iteration.
+   */
+  public native void testJsniObjectAlloc2() /*-{
+    for (var i = 0; i < 1000; ++i ) {
+      var obj = {}; // An empty JS object alloc
+    }
+  }-*/;
+
+  /**
+   * Like version 2, but hoists the variable declaration from the loop.
+   */
+  public native void testJsniObjectAlloc3() /*-{
+    var obj;
+    for (var i = 0; i < 1000; ++i ) {
+      obj = {}; // An empty JS object alloc
+    }
+  }-*/;
+
+  /**
+   * Like version 3, but counts down (and in a slightly different range).
+   */
+  public native void testJsniObjectAlloc4() /*-{
+    var obj;
+    for (var i = 1000; i > 0; --i ) {
+      obj = {}; // An empty JS object alloc
+    }
+  }-*/;
+}
+
diff --git a/user/javadoc/com/google/gwt/examples/benchmarks/ArrayListAndVectorBenchmark.java b/user/javadoc/com/google/gwt/examples/benchmarks/ArrayListAndVectorBenchmark.java
new file mode 100644
index 0000000..9a34662
--- /dev/null
+++ b/user/javadoc/com/google/gwt/examples/benchmarks/ArrayListAndVectorBenchmark.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2007 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.examples.benchmarks;
+
+import com.google.gwt.junit.client.IntRange;
+import com.google.gwt.junit.client.Benchmark;
+import com.google.gwt.junit.client.Operator;
+import com.google.gwt.junit.client.Range;
+
+import java.util.Vector;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Iterator;
+
+/**
+ * Benchmarks common operations on both ArrayLists and Vectors.
+ * This test covers appends, inserts, and removes for various sizes
+ * and positions on both ArrayLists and Vectors.
+ *
+ */
+public class ArrayListAndVectorBenchmark extends Benchmark {
+
+  /**
+   * Many profiled widgets have position dependent insert/remove code.
+   * <code>Position</code> is a helper class meant to capture the positional
+   * information for these sorts of operations.
+   */
+  protected static class Position {
+
+    public static final Position BEGIN = new Position("at the beginning");
+    public static final Position END = new Position("at the end");
+    public static final Position NONE = new Position("no location specified");
+    public static final Position VARIED = new Position("in varied locations");
+
+    public static final Range positions = new Range() {
+      public Iterator iterator() {
+        return Arrays.asList( new Position[] {BEGIN, END, NONE, VARIED } ).iterator();
+      }
+    };
+
+    public static final Range positions2 = new Range() {
+      public Iterator iterator() {
+        return Arrays.asList( new Position[] {BEGIN, END, VARIED } ).iterator();
+      }
+    };
+
+    private String label;
+
+    /**
+     * Constructor for <code>Position</code>.
+     */
+    public Position(String label) {
+      this.label = label;
+    }
+
+    public String toString() {
+      return " " + label;
+    }
+  }
+
+  private static final int PRIME = 3001;
+
+  final IntRange insertRemoveRange = new IntRange(64, Integer.MAX_VALUE,
+      Operator.MULTIPLY, 2);
+
+  final IntRange baseRange = new IntRange(512, Integer.MAX_VALUE,
+      Operator.MULTIPLY, 2);
+
+  ArrayList list;
+  Vector vector;
+  int index = 0;
+
+  public String getModuleName() {
+    return "com.google.gwt.examples.Benchmarks";
+  }
+
+  /**
+   * Appends <code>size</code> items to an empty ArrayList.
+   * @gwt.benchmark.param size -limit = baseRange
+   */
+  public void testArrayListAdds( Integer size ) {
+    int num = size.intValue();
+    for (int i = 0; i < num; i++) {
+      list.add("hello");
+    }
+  }
+
+  // Required for JUnit
+  public void testArrayListAdds() {
+  }
+
+  /**
+   * Performs <code>size</code> gets on an ArrayList of size, <code>size</code>.
+   * @gwt.benchmark.param size -limit = baseRange
+   */
+  public void testArrayListGets( Integer size ) {
+    int num = size.intValue();
+    for (int i = 0; i < num; i++) {
+      list.get(i);
+    }
+  }
+
+  // Required for JUnit
+  public void testArrayListGets() {
+  }
+
+  /**
+   * Performs <code>size</code> inserts at position, <code>where</code>, on an
+   * empty ArrayList.
+   * @gwt.benchmark.param where = Position.positions
+   * @gwt.benchmark.param size -limit = insertRemoveRange
+   */
+  public void testArrayListInserts( Position where, Integer size ) {
+    insertIntoCollection(size, where, list);
+  }
+
+  // Required for JUnit
+  public void testArrayListInserts() {
+  }
+
+  /**
+   * Performs <code>size</code> removes at position, <code>where</code>, on an
+   * ArrayList of size, <code>size</code>.
+   * @gwt.benchmark.param where = Position.positions2
+   * @gwt.benchmark.param size -limit = insertRemoveRange
+   */
+  public void testArrayListRemoves(Position where, Integer size) {
+    removeFromCollection(size, where, list);
+  }
+
+  // Required for JUnit
+  public void testArrayListRemoves() {
+  }
+
+  /**
+   * Appends <code>size</code> items to an empty Vector.
+   * @gwt.benchmark.param size -limit = baseRange
+   */
+  public void testVectorAdds( Integer size ) {
+    int num = size.intValue();
+    for (int i = 0; i < num; i++) {
+      vector.add("hello");
+    }
+  }
+
+  // Required for JUnit
+  public void testVectorAdds() {
+  }
+
+  /**
+   * Performs <code>size</code> gets on a Vector of size, <code>size</code>.
+   * @gwt.benchmark.param size -limit = baseRange
+   */
+  public void testVectorGets( Integer size ) {
+    int num = size.intValue();
+    for (int i = 0; i < num; i++) {
+      vector.get(i);
+    }
+  }
+
+  // Required for JUnit
+  public void testVectorGets() {
+  }
+
+  /**
+   * Performs <code>size</code> inserts at position, <code>where</code>, on an
+   * empty Vector.
+   * @gwt.benchmark.param where = Position.positions
+   * @gwt.benchmark.param size -limit = insertRemoveRange
+   */
+  public void testVectorInserts(Position where, Integer size) {
+    insertIntoCollection( size, where, vector );
+  }
+
+  // Required for JUnit
+  public void testVectorInserts() {
+  }
+
+  /**
+   * Performs <code>size</code> removes at position, <code>where</code>, on a
+   * Vector of size, <code>size</code>.
+   * @gwt.benchmark.param where = Position.positions2
+   * @gwt.benchmark.param size -limit = insertRemoveRange
+   */
+  public void testVectorRemoves( Position where, Integer size ) {
+    removeFromCollection( size, where, vector );
+  }
+
+  // Required for JUnit
+  public void testVectorRemoves() {
+  }
+
+  void beginArrayListAdds( Integer size ) {
+    list = new ArrayList();
+  }
+
+  void beginArrayListGets( Integer size ) {
+    createArrayList( size );
+  }
+
+  void beginArrayListInserts(Position where, Integer size) {
+    list = new ArrayList();
+    index = 0;
+  }
+
+  void beginArrayListRemoves(Position where, Integer size) {
+    beginArrayListInserts(where, size);
+    testArrayListInserts(where, size);
+  }
+
+  void beginVectorAdds(Integer size) {
+    vector = new Vector();
+  }
+
+  void beginVectorGets( Integer size ) {
+    createVector( size );
+  }
+
+  void beginVectorInserts(Position where, Integer size) {
+    vector = new Vector(); index = 0;
+  }
+
+  void beginVectorRemoves(Position where, Integer size) {
+    beginVectorInserts(where,size); testVectorInserts(where,size);
+  }
+
+  private void createArrayList( Integer size ) {
+    beginArrayListAdds( size );
+    testArrayListAdds( size );
+  }
+
+  private void createVector( Integer size ) {
+    beginVectorAdds( size );
+    testVectorAdds( size );
+  }
+
+  private void insertIntoCollection(Integer size, Position where, List v) {
+    int num = size.intValue();
+    for (int i = 0; i < num; i++) {
+      if (where == Position.NONE ) {
+        v.add("hello");
+      } else if (where == Position.BEGIN) {
+        v.add(0, "hello");
+      } else if (where == Position.END) {
+        v.add(v.size(), "hello");
+      } else if (where == Position.VARIED) {
+        v.add(index, "hello");
+        index += PRIME;
+        index %= v.size();
+      }
+    }
+  }
+
+  private int removeFromCollection(Integer size, Position where, List v) {
+    int num = size.intValue();
+    for (int i = 0; i < num; i++) {
+      if (where == Position.NONE) {
+        throw new RuntimeException("cannot remove with no position");
+      } else if (where == Position.BEGIN) {
+        v.remove(0);
+      } else if (where == Position.END) {
+        v.remove(v.size() - 1);
+      } else if (where == Position.VARIED) {
+        v.remove(index);
+        index += PRIME;
+        int currentSize = v.size();
+        if ( currentSize > 0 ) {
+          index %= v.size();
+        }
+      }
+    }
+    return index;
+  }
+}
diff --git a/user/src/com/google/gwt/junit/JUnit.gwt.xml b/user/src/com/google/gwt/junit/JUnit.gwt.xml
index 12703be..4a22ebf 100644
--- a/user/src/com/google/gwt/junit/JUnit.gwt.xml
+++ b/user/src/com/google/gwt/junit/JUnit.gwt.xml
@@ -1,5 +1,5 @@
 <!--                                                                        -->
-<!-- Copyright 2006 Google Inc. All Rights Reserved.                        -->
+<!-- Copyright 2007 Google Inc. All Rights Reserved.                        -->
 <!-- Deferred binding rules for browser selection.                          -->
 <!--                                                                        -->
 <!-- Do not inherit this module directly.  Running GWTTestCase under JUnit  -->
@@ -15,6 +15,10 @@
     <when-type-assignable class="com.google.gwt.junit.client.GWTTestCase"/>
   </generate-with>
 
+  <generate-with class="com.google.gwt.junit.rebind.BenchmarkGenerator">
+    <when-type-assignable class="com.google.gwt.junit.client.Benchmark"/>
+  </generate-with>
+
   <servlet path='/junithost' class='com.google.gwt.junit.server.JUnitHostImpl'/>
 
 </module>
diff --git a/user/src/com/google/gwt/junit/JUnitMessageQueue.java b/user/src/com/google/gwt/junit/JUnitMessageQueue.java
index 8545b79..cbba383 100644
--- a/user/src/com/google/gwt/junit/JUnitMessageQueue.java
+++ b/user/src/com/google/gwt/junit/JUnitMessageQueue.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * Copyright 2007 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
@@ -15,136 +15,198 @@
  */
 package com.google.gwt.junit;
 
-import java.util.HashMap;
+import com.google.gwt.junit.client.TestResults;
+
+import java.util.List;
+import java.util.ArrayList;
 import java.util.Map;
+import java.util.HashMap;
 
 /**
- * A message queue to pass data between {@link JUnitShell} and
- * {@link com.google.gwt.junit.server.JUnitHostImpl} in a thread-safe manner.
- * 
- * <p>
- * The public methods are called by the servlet to find out what test to execute
- * next, and to report the results of the last test to run.
- * </p>
- * 
- * <p>
- * The protected methods are called by the shell to fetch test results and drive
- * the next test the client should run.
- * </p>
+ * A message queue to pass data between {@link JUnitShell} and {@link
+ * com.google.gwt.junit.server.JUnitHostImpl} in a thread-safe manner.
+ *
+ * <p> The public methods are called by the servlet to find out what test to
+ * execute next, and to report the results of the last test to run. </p>
+ *
+ * <p> The protected methods are called by the shell to fetch test results and
+ * drive the next test the client should run. </p>
  */
 public class JUnitMessageQueue {
 
   /**
-   * Maps the name of a test class to the method that should be run. Access must
-   * be synchronized.
+   * Tracks which test each client is requesting.
+   *
+   * Key = client-id (e.g. agent+host) Value = the index of the current
+   * requested test
    */
-  private final Map nameMap = new HashMap();
+  private Map/*<String,Integer>*/ clientTestRequests
+      = new HashMap/*<String,Integer>*/();
 
   /**
-   * Maps the name of a test class to the last results to be reported. Access
-   * must be synchronized.
+   * The index of the current test being executed.
    */
-  private final Map resultsMap = new HashMap();
+  private int currentTestIndex = -1;
 
   /**
-   * Only instantiatable within this package.
+   * The number of TestCase clients executing in parallel.
+   */
+  private int numClients = 1;
+
+  /**
+   * The lock used to synchronize access around testMethod, clientTestRequests,
+   * and currentTestIndex.
+   */
+  private Object readTestLock = new Object();
+
+  /**
+   * The lock used to synchronize access around testResults.
+   */
+  private Object resultsLock = new Object();
+
+  /**
+   * The name of the test method to execute. We don't need the class, because
+   * the remote TestCases already knows the class.
+   */
+  private String testMethod;
+
+  /**
+   * The results for the current test method.
+   */
+  private List/*<TestResults>*/ testResults = new ArrayList/*<TestResults>*/();
+
+  /**
+   * Creates a message queue with one client.
+   *
+   * @see JUnitMessageQueue#JUnitMessageQueue(int)
    */
   JUnitMessageQueue() {
   }
 
   /**
-   * Called by the servlet to query for for the next method to test.
-   * 
-   * @param testClassName The name of the test class.
-   * @param timeout How long to wait for an answer.
-   * @return The next test to run, or <code>null</code> if
-   *         <code>timeout</code> is exceeded or the next test does not match
-   *         <code>testClassName</code>.
+   * Only instantiatable within this package.
+   *
+   * @param numClients The number of parallel clients being served by this
+   * queue.
    */
-  public String getNextTestName(String testClassName, long timeout) {
-    synchronized (nameMap) {
+  JUnitMessageQueue(int numClients) {
+    this.numClients = numClients;
+  }
+
+  /**
+   * Called by the servlet to query for for the next method to test.
+   *
+   * @param testClassName The name of the test class.
+   * @param timeout       How long to wait for an answer.
+   * @return The next test to run, or <code>null</code> if <code>timeout</code>
+   *         is exceeded or the next test does not match <code>testClassName</code>.
+   */
+  public String getNextTestName(String clientId, String testClassName,
+      long timeout) {
+    synchronized (readTestLock) {
       long stopTime = System.currentTimeMillis() + timeout;
-      while (!nameMap.containsKey(testClassName)) {
+
+      while (!testIsAvailableFor(clientId)) {
         long timeToWait = stopTime - System.currentTimeMillis();
         if (timeToWait < 1) {
           return null;
         }
         try {
-          nameMap.wait(timeToWait);
+          readTestLock.wait(timeToWait);
         } catch (InterruptedException e) {
           // just abort
           return null;
         }
       }
 
-      return (String) nameMap.remove(testClassName);
+      bumpClientTestRequest(clientId);
+
+      return testMethod;
     }
   }
 
   /**
    * Called by the servlet to report the results of the last test to run.
-   * 
+   *
    * @param testClassName The name of the test class.
-   * @param t The exception thrown by the last test, or <code>null</code> if
-   *          the test completed without error.
+   * @param results       The result of running the test.
    */
-  public void reportResults(String testClassName, Throwable t) {
-    synchronized (resultsMap) {
-      resultsMap.put(testClassName, t);
+  public void reportResults(String testClassName, TestResults results) {
+
+    // TODO(tobyr): testClassName is not needed now
+    synchronized (resultsLock) {
+      testResults.add(results);
     }
   }
 
   /**
-   * Called by the shell to fetch the results of a completed test.
-   * 
+   * Fetches the results of a completed test.
+   *
    * @param testClassName The name of the test class.
-   * @return An exception thrown from a failed test, or <code>null</code> if
+   * @return An getException thrown from a failed test, or <code>null</code> if
    *         the test completed without error.
    */
-  Throwable getResult(String testClassName) {
-    synchronized (resultsMap) {
-      return (Throwable) resultsMap.remove(testClassName);
-    }
-  }
-
-  /**
-   * Called by the shell to see if the servlet has begun running the current
-   * test.
-   * 
-   * @param testClassName
-   * @return <code>true</code> if the servlet has not yet fetched the next
-   *         test name, otherwise <code>false</code>.
-   */
-  boolean hasNextTestName(String testClassName) {
-    synchronized (nameMap) {
-      return nameMap.containsKey(testClassName);
-    }
+  List/*<TestResults>*/ getResults(String testClassName) {
+    // TODO(tobyr): testClassName is not needed now
+    return (List/*<TestResult>*/) testResults;
   }
 
   /**
    * Called by the shell to see if the currently-running test has completed.
-   * 
+   *
    * @param testClassName The name of the test class.
    * @return If the test has completed, <code>true</code>, otherwise
    *         <code>false</code>.
    */
   boolean hasResult(String testClassName) {
-    synchronized (resultsMap) {
-      return resultsMap.containsKey(testClassName);
+
+    // TODO(tobyr): testClassName is not needed now
+    synchronized (resultsLock) {
+      return testResults.size() == numClients;
     }
   }
 
   /**
    * Called by the shell to set the name of the next method to run for this test
    * class.
-   * 
+   *
    * @param testClassName The name of the test class.
-   * @param testName The name of the method to run.
+   * @param testName      The name of the method to run.
    */
   void setNextTestName(String testClassName, String testName) {
-    synchronized (nameMap) {
-      nameMap.put(testClassName, testName);
-      nameMap.notifyAll();
+
+    // TODO(tobyr): testClassName is not needed now
+    synchronized (readTestLock) {
+      testMethod = testName;
+      ++currentTestIndex;
+      testResults = new ArrayList/*<TestResults>*/(numClients);
+      readTestLock.notifyAll();
     }
   }
+
+  /**
+   * Sets the number of clients that will be executing the JUnit tests in
+   * parallel.
+   *
+   * @param numClients must be > 0
+   */
+  void setNumClients(int numClients) {
+    this.numClients = numClients;
+  }
+
+  // This method requires that readTestLock is being held for the duration.
+  private void bumpClientTestRequest(String clientId) {
+    Integer index = (Integer) clientTestRequests.get(clientId);
+    clientTestRequests.put(clientId, new Integer(index.intValue() + 1));
+  }
+
+  // This method requires that readTestLock is being held for the duration.
+  private boolean testIsAvailableFor(String clientId) {
+    Integer index = (Integer) clientTestRequests.get(clientId);
+    if (index == null) {
+      index = new Integer(0);
+      clientTestRequests.put(clientId, index);
+    }
+    return index.intValue() == currentTestIndex;
+  }
 }
\ No newline at end of file
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index ace04af..b70c481 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * Copyright 2007 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
@@ -23,7 +23,11 @@
 import com.google.gwt.dev.shell.BrowserWidgetHost;
 import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
 import com.google.gwt.junit.client.TimeoutException;
+import com.google.gwt.junit.client.TestResults;
+import com.google.gwt.junit.client.Trial;
+import com.google.gwt.junit.client.Benchmark;
 import com.google.gwt.junit.remote.BrowserManager;
+import com.google.gwt.junit.benchmarks.BenchmarkReport;
 import com.google.gwt.util.tools.ArgHandlerFlag;
 import com.google.gwt.util.tools.ArgHandlerString;
 
@@ -33,41 +37,60 @@
 
 import java.rmi.Naming;
 import java.util.ArrayList;
+import java.util.List;
+import java.util.Date;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.io.File;
 
 /**
  * This class is responsible for hosting JUnit test case execution. There are
  * three main pieces to the JUnit system.
- * 
- * <ul>
- * <li>Test environment</li>
- * <li>Client classes</li>
- * <li>Server classes</li>
- * </ul>
- * 
- * <p>
- * The test environment consists of this class and the non-translatable version
- * of {@link com.google.gwt.junit.client.GWTTestCase}. These two classes
- * integrate directly into the real JUnit test process.
- * </p>
- * 
- * <p>
- * The client classes consist of the translatable version of
- * {@link com.google.gwt.junit.client.GWTTestCase}, translatable JUnit classes,
- * and the user's own {@link com.google.gwt.junit.client.GWTTestCase}-derived
- * class. The client communicates to the server via RPC.
- * </p>
- * 
- * <p>
- * The server consists of {@link com.google.gwt.junit.server.JUnitHostImpl}, an
- * RPC servlet which communicates back to the test environment through a
- * {@link JUnitMessageQueue}, thus closing the loop.
- * </p>
+ *
+ * <ul> <li>Test environment</li> <li>Client classes</li> <li>Server
+ * classes</li> </ul>
+ *
+ * <p> The test environment consists of this class and the non-translatable
+ * version of {@link com.google.gwt.junit.client.GWTTestCase}. These two classes
+ * integrate directly into the real JUnit test process. </p>
+ *
+ * <p> The client classes consist of the translatable version of {@link
+ * com.google.gwt.junit.client.GWTTestCase}, translatable JUnit classes, and the
+ * user's own {@link com.google.gwt.junit.client.GWTTestCase}-derived class. The
+ * client communicates to the server via RPC. </p>
+ *
+ * <p> The server consists of {@link com.google.gwt.junit.server.JUnitHostImpl},
+ * an RPC servlet which communicates back to the test environment through a
+ * {@link JUnitMessageQueue}, thus closing the loop. </p>
  */
 public class JUnitShell extends GWTShell {
 
   /**
+   * Executes shutdown logic for JUnitShell
+   *
+   * Sadly, there's no simple way to know when all unit tests have finished
+   * executing. So this class is registered as a VM shutdown hook so that work
+   * can be done at the end of testing - for example, writing out the reports.
+   */
+  private class Shutdown implements Runnable {
+
+    public void run() {
+      try {
+        String reportPath = System.getProperty(Benchmark.REPORT_PATH);
+        if (reportPath == null || reportPath.trim().equals("")) {
+          reportPath = System.getProperty("user.dir");
+        }
+        report.generate(reportPath + File.separator + "report-"
+            + new Date().getTime() + ".xml");
+      } catch (Exception e) {
+        // It really doesn't matter how we got here.
+        // Regardless of the failure, the VM is shutting down.
+        e.printStackTrace();
+      }
+    }
+  }
+
+  /**
    * This is a system property that, when set, emulates command line arguments.
    */
   private static final String PROP_GWT_ARGS = "gwt.args";
@@ -84,6 +107,10 @@
    */
   private static final int TEST_BEGIN_TIMEOUT_MILLIS = 30000;
 
+  // A larger value when debugging the unit test framework, so you
+  // don't get spurious timeouts.
+  // private static final int TEST_BEGIN_TIMEOUT_MILLIS = 200000;
+
   /**
    * Singleton object for hosting unit tests. All test case instances executed
    * by the TestRunner will use the single unitTestShell.
@@ -98,7 +125,7 @@
   /**
    * Called by {@link com.google.gwt.junit.server.JUnitHostImpl} to get an
    * interface into the test process.
-   * 
+   *
    * @return The {@link JUnitMessageQueue} interface that belongs to the
    *         singleton {@link JUnitShell}, or <code>null</code> if no such
    *         singleton exists.
@@ -111,9 +138,23 @@
   }
 
   /**
+   * Called by {@link com.google.gwt.junit.rebind.JUnitTestCaseStubGenerator} to
+   * add test meta data to the test report.
+   *
+   * @return The {@link BenchmarkReport} that belongs to the singleton {@link
+   *         JUnitShell}, or <code>null</code> if no such singleton exists.
+   */
+  public static BenchmarkReport getReport() {
+    if (unitTestShell == null) {
+      return null;
+    }
+    return unitTestShell.report;
+  }
+
+  /**
    * Entry point for {@link com.google.gwt.junit.client.GWTTestCase}. Gets or
-   * creates the singleton {@link JUnitShell} and invokes its
-   * {@link #runTestImpl(String, TestCase, TestResult)}.
+   * creates the singleton {@link JUnitShell} and invokes its {@link
+   * #runTestImpl(String, TestCase, TestResult)}.
    */
   public static void runTest(String moduleName, TestCase testCase,
       TestResult testResult) throws UnableToCompleteException {
@@ -121,7 +162,8 @@
   }
 
   /**
-   * Lazily initialize the singleton JUnitShell.
+   * Retrieves the JUnitShell. This should only be invoked during TestRunner
+   * execution of JUnit tests.
    */
   private static JUnitShell getUnitTestShell() {
     if (unitTestShell == null) {
@@ -130,10 +172,17 @@
       if (!shell.processArgs(args)) {
         throw new RuntimeException("Invalid shell arguments");
       }
+
+      shell.messageQueue = new JUnitMessageQueue(shell.numClients);
+
       if (!shell.startUp()) {
         throw new RuntimeException("Shell failed to start");
       }
+
+      shell.report = new BenchmarkReport(shell.getTopLogger());
       unitTestShell = shell;
+
+      Runtime.getRuntime().addShutdownHook(new Thread(shell. new Shutdown()));
     }
 
     return unitTestShell;
@@ -157,7 +206,19 @@
   /**
    * Portal to interact with the servlet.
    */
-  private JUnitMessageQueue messageQueue = new JUnitMessageQueue();
+  private JUnitMessageQueue messageQueue;
+
+  /**
+   * The number of test clients executing in parallel. With -remoteweb, users
+   * can specify a number of parallel test clients, but by default we only have
+   * 1.
+   */
+  private int numClients = 1;
+
+  /**
+   * The result of benchmark runs.
+   */
+  private BenchmarkReport report;
 
   /**
    * What type of test we're running; Local hosted, local web, or remote web.
@@ -168,7 +229,7 @@
    * The time at which the current test will fail if the client has not yet
    * started the test.
    */
-  private long testBeginTimout;
+  private long testBeginTimeout;
 
   /**
    * Class name of the current/last test case to run.
@@ -219,8 +280,13 @@
 
       public boolean setString(String str) {
         try {
-          BrowserManager browserManager = (BrowserManager) Naming.lookup(str);
-          runStyle = new RunStyleRemoteWeb(JUnitShell.this, browserManager);
+          String[] urls = str.split(",");
+          numClients = urls.length;
+          BrowserManager[] browserManagers = new BrowserManager[ numClients ];
+          for (int i = 0; i < numClients; ++i) {
+            browserManagers[i] = (BrowserManager) Naming.lookup(urls[i]);
+          }
+          runStyle = new RunStyleRemoteWeb(JUnitShell.this, browserManagers);
         } catch (Exception e) {
           System.err.println("Error connecting to browser manager at " + str);
           e.printStackTrace();
@@ -308,12 +374,14 @@
    * to complete.
    */
   protected boolean notDone() {
+    /*
     if (messageQueue.hasNextTestName(testCaseClassName)
-        && testBeginTimout < System.currentTimeMillis()) {
+        && testBeginTimeout < System.currentTimeMillis()) {
       throw new TimeoutException(
           "The browser did not contact the server within "
               + TEST_BEGIN_TIMEOUT_MILLIS + "ms.");
     }
+    */
 
     if (messageQueue.hasResult(testCaseClassName)) {
       return false;
@@ -356,18 +424,51 @@
     try {
       // Set a timeout period to automatically fail if the servlet hasn't been
       // contacted; something probably went wrong (the module failed to load?)
-      testBeginTimout = System.currentTimeMillis() + TEST_BEGIN_TIMEOUT_MILLIS;
+      // testBeginTimeout = System.currentTimeMillis() + TEST_BEGIN_TIMEOUT_MILLIS;
       pumpEventLoop();
     } catch (TimeoutException e) {
       testResult.addError(testCase, e);
       return;
     }
 
-    Throwable result = messageQueue.getResult(testCaseClassName);
-    if (result instanceof AssertionFailedError) {
-      testResult.addFailure(testCase, (AssertionFailedError) result);
-    } else if (result != null) {
-      testResult.addError(testCase, result);
+    List/*JUnitMessageQueue.TestResult*/ results = messageQueue
+        .getResults(testCaseClassName);
+
+    if (results == null) {
+      return;
+    }
+
+    boolean parallelTesting = numClients > 1;
+
+    for (int i = 0; i < results.size(); ++i) {
+      TestResults result = (TestResults) results.get(i);
+      Trial firstTrial = (Trial) result.getTrials().get(0);
+      Throwable exception = firstTrial.getException();
+
+      // In the case that we're running multiple clients at once, we need to
+      // let the user know the browser in which the failure happened
+      if (parallelTesting && exception != null) {
+        String msg = "Remote test failed at " + result.getHost() + " on " + result.getAgent();
+        if (exception instanceof AssertionFailedError) {
+          AssertionFailedError newException = new AssertionFailedError(msg + "\n" + exception.getMessage());
+          newException.setStackTrace(exception.getStackTrace());
+          exception = newException;
+        } else {
+          exception = new RuntimeException(msg, exception);
+        }
+      }
+
+      // A "successful" failure
+      if (exception instanceof AssertionFailedError) {
+        testResult.addFailure(testCase, (AssertionFailedError) exception);       
+      } else if (exception != null) {
+        // A real failure
+        testResult.addError(testCase, exception);
+      }
+
+      if (testCase instanceof Benchmark) {
+        report.addBenchmarkResults(testCase, result);
+      }
     }
   }
 
@@ -382,7 +483,8 @@
       // Match either a non-whitespace, non start of quoted string, or a
       // quoted string that can have embedded, escaped quoting characters
       //
-      Pattern pattern = Pattern.compile("[^\\s\"]+|\"[^\"\\\\]*(\\\\.[^\"\\\\]*)*\"");
+      Pattern pattern = Pattern
+          .compile("[^\\s\"]+|\"[^\"\\\\]*(\\\\.[^\"\\\\]*)*\"");
       Matcher matcher = pattern.matcher(args);
       while (matcher.find()) {
         argList.add(matcher.group());
diff --git a/user/src/com/google/gwt/junit/RunStyleRemoteWeb.java b/user/src/com/google/gwt/junit/RunStyleRemoteWeb.java
index 13c5d8c..b7f5505 100644
--- a/user/src/com/google/gwt/junit/RunStyleRemoteWeb.java
+++ b/user/src/com/google/gwt/junit/RunStyleRemoteWeb.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * Copyright 2007 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
@@ -31,34 +31,44 @@
   private static final int INITIAL_KEEPALIVE_MS = 5000;
   private static final int PING_KEEPALIVE_MS = 2000;
 
-  /**
-   * A remote browser manager.
-   */
-  private final BrowserManager browserManager;
+  // Larger values when debugging the unit test framework, so you
+  // don't get spurious timeouts.
+  // private static final int INITIAL_KEEPALIVE_MS = 500000;
+  // private static final int PING_KEEPALIVE_MS = 200000;
 
   /**
-   * A local reference to a remote browser process.
+   * Remote browser managers.
    */
-  private int remoteToken = 0;
+  private final BrowserManager[] browserManagers;
+
+  /**
+   * References to the remote browser processes.
+   */
+  private int[] remoteTokens;
 
   /**
    * The containing shell.
    */
   private final JUnitShell shell;
 
+  private boolean running = false;
+
   /**
    * @param shell the containing shell
    */
-  public RunStyleRemoteWeb(JUnitShell shell, BrowserManager browserManager) {
+  public RunStyleRemoteWeb(JUnitShell shell, BrowserManager[] browserManagers) {
     this.shell = shell;
-    this.browserManager = browserManager;
+    this.browserManagers = browserManagers;
+    this.remoteTokens = new int[ browserManagers.length ];
   }
 
   public void maybeLaunchModule(String moduleName, boolean forceLaunch)
       throws UnableToCompleteException {
-    if (forceLaunch || remoteToken == 0) {
+
+    if (forceLaunch || !running) {
       shell.compileForWebMode(moduleName);
       String localhost;
+
       try {
         localhost = InetAddress.getLocalHost().getHostAddress();
       } catch (UnknownHostException e) {
@@ -66,28 +76,40 @@
       }
       String url = "http://" + localhost + ":" + shell.getPort() + "/"
           + moduleName;
+
       try {
-        if (remoteToken > 0) {
-          browserManager.killBrowser(remoteToken);
-          remoteToken = 0;
+        for ( int i = 0; i < remoteTokens.length; ++i ) {
+          int remoteToken = remoteTokens[ i ];
+          BrowserManager mgr = browserManagers[ i ];
+          if ( remoteToken != 0 ) {
+            mgr.killBrowser(remoteToken);
+          }
+          remoteTokens[ i ] = mgr.launchNewBrowser(url, INITIAL_KEEPALIVE_MS);
         }
-        remoteToken = browserManager.launchNewBrowser(url, INITIAL_KEEPALIVE_MS);
       } catch (Exception e) {
         shell.getTopLogger().log(TreeLogger.ERROR,
             "Error launching remote browser", e);
         throw new UnableToCompleteException();
       }
+
+      running = true;
     }
   }
 
   public boolean wasInterrupted() {
-    if (remoteToken > 0) {
-      try {
-        browserManager.keepAlive(remoteToken, PING_KEEPALIVE_MS);
-      } catch (Exception e) {
-        shell.getTopLogger().log(TreeLogger.WARN,
-            "Unexpected exception keeping remote browser alive", e);
-        return true;
+    for ( int i = 0; i < remoteTokens.length; ++i ) {
+      int remoteToken = remoteTokens[ i ];
+      BrowserManager mgr = browserManagers[ i ];
+      if (remoteToken > 0) {
+        try {
+          mgr.keepAlive(remoteToken, PING_KEEPALIVE_MS);
+        } catch (Exception e) {
+          // TODO(tobyr): We're failing on the first exception, rather than
+          //  collecting them, but that's probably OK for now.
+          shell.getTopLogger().log(TreeLogger.WARN,
+              "Unexpected exception keeping remote browser alive", e);
+          return true;
+        }
       }
     }
     return false;
diff --git a/user/src/com/google/gwt/junit/benchmarks/BenchmarkReport.java b/user/src/com/google/gwt/junit/benchmarks/BenchmarkReport.java
new file mode 100644
index 0000000..ffbe2b9
--- /dev/null
+++ b/user/src/com/google/gwt/junit/benchmarks/BenchmarkReport.java
@@ -0,0 +1,605 @@
+/*
+ * Copyright 2007 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.junit.benchmarks;
+
+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.core.ext.TreeLogger;
+import com.google.gwt.junit.rebind.BenchmarkGenerator;
+import com.google.gwt.junit.client.TestResults;
+import com.google.gwt.junit.client.Trial;
+
+import junit.framework.TestCase;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
+import org.eclipse.jdt.internal.compiler.IProblemFactory;
+import org.eclipse.jdt.internal.compiler.SourceElementParser;
+import org.eclipse.jdt.internal.compiler.ISourceElementRequestor;
+import org.eclipse.jdt.internal.compiler.SourceElementRequestorAdapter;
+import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
+import org.eclipse.jdt.internal.compiler.batch.CompilationUnit;
+
+import java.io.IOException;
+import java.io.FileOutputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileReader;
+import java.io.BufferedReader;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+import java.text.DateFormat;
+import java.text.BreakIterator;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.dom.DOMSource;
+
+/**
+ * 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 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/*<JUnitMessageQueue.TestResult>*/ results;
+
+    private TestCase test;
+
+    BenchmarkXml(TestCase test,
+        List/*<JUnitMessageQueue.TestResult>*/ results) {
+      this.test = test;
+      this.results = results;
+      Map/*<String,MetaData>*/ methodMetaData
+          = (Map/*<String,MetaData>*/) testMetaData
+          .get(test.getClass().toString());
+      metaData = (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 (Iterator it = results.iterator(); it.hasNext();) {
+        TestResults result = (TestResults) it.next();
+        benchmark.appendChild(toElement(doc, result));
+      }
+
+      return benchmark;
+    }
+
+    private Element toElement(Document doc, TestResults result) {
+      Element resultElement = doc.createElement("result");
+      resultElement.setAttribute("host", result.getHost());
+      resultElement.setAttribute("agent", result.getAgent());
+
+      List trials = result.getTrials();
+
+      for (Iterator it = trials.iterator(); it.hasNext();) {
+        Trial trial = (Trial) it.next();
+        Element trialElement = toElement(doc, trial);
+        resultElement.appendChild(trialElement);
+      }
+
+      return resultElement;
+    }
+
+    private Element toElement(Document doc, Trial trial) {
+      Element trialElement = doc.createElement("trial");
+
+      Map variables = trial.getVariables();
+
+      for (Iterator it = variables.entrySet().iterator(); it.hasNext();) {
+        Map.Entry entry = (Map.Entry) it.next();
+        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()));
+
+      Throwable exception = trial.getException();
+
+      if (exception != null) {
+        Element exceptionElement = doc.createElement("exception");
+        exceptionElement.appendChild(doc.createTextNode(exception.toString()));
+        trialElement.appendChild(exceptionElement);
+      }
+
+      return trialElement;
+    }
+  }
+
+  /**
+   * Parses a .java source file to get the source code for methods.
+   *
+   * This Parser takes some shortcuts based on the fact that it's only being
+   * used to locate test methods for unit tests. (For example, only requiring a
+   * method name instead of a full type signature for lookup).
+   *
+   * TODO(tobyr) I think that I might be able to replace all this code with a
+   * call to the existing metadata interface. Check declEnd/declStart in
+   * JAbstractMethod.
+   */
+  private static class Parser {
+
+    static class MethodBody {
+
+      int declarationEnd;   // the character index of the end of the method
+
+      int declarationStart; // the character index of the start of the method
+
+      String source;
+    }
+
+    private MethodBody currentMethod; // Only used during the visitor
+
+    // But it's less painful
+    private Map/*<String,MethodBody>*/ methods
+        = new HashMap/*<String,MethodBody>*/();
+
+    // Contains the contents of the entire source file
+    private char[] sourceContents;
+
+    Parser(JClassType klass) throws IOException {
+
+      Map settings = new HashMap();
+      settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_4);
+      settings.put(CompilerOptions.OPTION_TargetPlatform,
+          CompilerOptions.VERSION_1_4);
+      settings.put(CompilerOptions.OPTION_DocCommentSupport,
+          CompilerOptions.ENABLED);
+      CompilerOptions options = new CompilerOptions(settings);
+
+      IProblemFactory problemFactory = new DefaultProblemFactory(
+          Locale.getDefault());
+
+      // Save off the bounds of any method that is a test method
+      ISourceElementRequestor requestor = new SourceElementRequestorAdapter() {
+        public void enterMethod(MethodInfo methodInfo) {
+          String name = new String(methodInfo.name);
+          if (name.startsWith("test")) {
+            currentMethod = new MethodBody();
+            currentMethod.declarationStart = methodInfo.declarationStart;
+            methods.put(name, currentMethod);
+          }
+        }
+
+        public void exitMethod(int declarationEnd, int defaultValueStart,
+            int defaultValueEnd) {
+          if (currentMethod != null) {
+            currentMethod.declarationEnd = declarationEnd;
+            currentMethod = null;
+          }
+        }
+      };
+
+      boolean reportLocalDeclarations = true;
+      boolean optimizeStringLiterals = true;
+
+      SourceElementParser parser = new SourceElementParser(requestor,
+          problemFactory, options, reportLocalDeclarations,
+          optimizeStringLiterals);
+
+      File sourceFile = findSourceFile(klass);
+      sourceContents = read(sourceFile);
+      CompilationUnit unit = new CompilationUnit(sourceContents,
+          sourceFile.getName(), null);
+
+      parser.parseCompilationUnit(unit, true);
+    }
+
+    /**
+     * Returns the source code for the method of the given name.
+     *
+     * @return null if the source code for the method can not be located
+     */
+    public String getMethod(JMethod method) {
+      /*
+      MethodBody methodBody = (MethodBody)methods.get( method.getName() );
+      if ( methodBody == null ) {
+        return null;
+      }
+      if ( methodBody.source == null ) {
+        methodBody.source = new String(sourceContents,
+        methodBody.declarationStart, methodBody.declarationEnd - methodBody.
+        declarationStart + 1);
+      }
+      return methodBody.source;
+      */
+      return new String(sourceContents, method.getDeclStart(),
+          method.getDeclEnd() - method.getDeclStart() + 1);
+    }
+  }
+
+  /**
+   * 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";
+
+    /**
+     * Locates or creates the category element by the specified name.
+     *
+     * @param doc The document to search
+     * @return The matching category element
+     */
+    private Element getCategoryElement(Document doc, Element report,
+        String name) {
+      Element e = (Element) categoryElementMap.get(name);
+
+      if (e != null) {
+        return e;
+      }
+
+      Element categoryElement = doc.createElement("category");
+      categoryElementMap.put(name, categoryElement);
+      CategoryImpl category = (CategoryImpl) testCategories.get(name);
+      categoryElement.setAttribute("name", category.getName());
+      categoryElement.setAttribute("description", category.getDescription());
+
+      report.appendChild(categoryElement);
+
+      return categoryElement;
+    }
+
+    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 (Iterator entryIt = testResults.entrySet().iterator();
+          entryIt.hasNext();) {
+        Map.Entry entry = (Map.Entry) entryIt.next();
+        TestCase test = (TestCase) entry.getKey();
+        List/*<JUnitMessageQueue.TestResult>*/ results
+            = (List/*<JUnitMessageQueue.TestResult>*/) entry.getValue();
+        BenchmarkXml xml = new BenchmarkXml(test, results);
+        Element categoryElement = getCategoryElement(doc, report,
+            xml.metaData.getCategory().getClassName());
+        categoryElement.appendChild(xml.toElement(doc));
+      }
+
+      return report;
+    }
+  }
+
+  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 klass) {
+    final char separator = File.separator.charAt(0);
+    String filePath = klass.getPackage().getName().replace('.', separator) +
+        separator + klass.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 static char[] read(File f) throws IOException {
+    // TODO(tobyr) Can be done oh so much faster by just reading directly into
+    // a char[]
+
+    BufferedReader reader = new BufferedReader(new FileReader(f));
+    StringBuffer source = new StringBuffer((int) f.length());
+
+    while (true) {
+      String line = reader.readLine();
+      if (line == null) {
+        break;
+      }
+      source.append(line);
+      source.append("\n");
+    }
+
+    char[] buf = new char[ source.length() ];
+    source.getChars(0, buf.length, buf, 0);
+
+    return buf;
+  }
+
+  private Map/*<String,Map<CategoryImpl>*/ testCategories
+      = new HashMap/*<String,CategoryImpl>*/();
+
+  private Map/*<String,Map<String,MetaData>>*/ testMetaData
+      = new HashMap/*<String,Map<String,MetaData>>*/();
+
+  private Map/*<TestCase,List<JUnitMessageQueue.TestResult>>*/ testResults
+      = new HashMap/*<TestCase,JUnitMessageQueue.List<TestResult>>*/();
+
+  private TypeOracle typeOracle;
+
+  private TreeLogger logger;
+
+  public BenchmarkReport( TreeLogger logger ) {
+    this.logger = logger;
+  }
+
+  /**
+   * Adds the Benchmark to the report. All of the metadata about the benchmark
+   * (category, name, description, etc...) is recorded from the TypeOracle.
+   */
+  public void addBenchmark(JClassType benchmarkClass, TypeOracle typeOracle) {
+
+    this.typeOracle = typeOracle;
+    String categoryType = getSimpleMetaData(benchmarkClass,
+        GWT_BENCHMARK_CATEGORY);
+
+    Map 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
+        = (Map/*<String,MetaData>*/) testMetaData
+        .get(benchmarkClass.toString());
+    if (metaDataMap == null) {
+      metaDataMap = new HashMap/*<String,MetaData>*/();
+      testMetaData.put(benchmarkClass.toString(), metaDataMap);
+    }
+
+    Parser parser = null;
+
+    try {
+      parser = new Parser(benchmarkClass);
+    } catch (IOException e) {
+      // if we fail to create the parser for some reason, we'll have to just
+      // deal with a null parser.
+      logger.log(TreeLogger.WARN,
+          "Unable to parse the code for " + benchmarkClass, e);
+    }
+
+    // Add all of the benchmark methods
+    for (int i = 0; i < testMethods.size(); ++i) {
+      JMethod method = (JMethod) testMethods.get(i);
+      String methodName = method.getName();
+      String methodCategoryType = getSimpleMetaData(method,
+          GWT_BENCHMARK_CATEGORY);
+      if (methodCategoryType == null) {
+        methodCategoryType = categoryType;
+      }
+      CategoryImpl methodCategory = getCategory(methodCategoryType);
+      String sourceCode = parser == null ? null : parser.getMethod(method);
+      StringBuffer summary = new StringBuffer();
+      StringBuffer comment = new StringBuffer();
+      getComment(sourceCode, summary, comment);
+
+      MetaData metaData = new MetaData(benchmarkClass.toString(), methodName,
+          sourceCode, methodCategory, methodName, summary.toString());
+      metaDataMap.put(methodName, metaData);
+    }
+  }
+
+  public void addBenchmarkResults(TestCase test, TestResults results) {
+    List/*<TestResults>*/ currentResults = (List/*<TestResults>*/) testResults
+        .get(test);
+    if (currentResults == null) {
+      currentResults = new ArrayList/*<TestResults>*/();
+      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 IOException If anything goes wrong writing to outputPath
+   */
+  public void generate(String outputPath)
+      throws IOException, TransformerException, ParserConfigurationException {
+
+    // 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));
+
+    // 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 CategoryImpl getCategory(String name) {
+    CategoryImpl c = (CategoryImpl) testCategories.get(name);
+
+    if (c != null) {
+      return c;
+    }
+
+    String categoryName = "";
+    String categoryDescription = "";
+
+    if (name != null) {
+      JClassType categoryType = typeOracle.findType(name);
+
+      if (categoryType != null) {
+        categoryName = getSimpleMetaData(categoryType, GWT_BENCHMARK_NAME);
+        categoryDescription = getSimpleMetaData(categoryType,
+            GWT_BENCHMARK_DESCRIPTION);
+      }
+    }
+
+    c = new CategoryImpl(name, categoryName, categoryDescription);
+    testCategories.put(name, c);
+    return c;
+  }
+
+  /**
+   * 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>.
+   */
+  private void getComment(String 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);
+
+    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);
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/junit/benchmarks/CategoryImpl.java b/user/src/com/google/gwt/junit/benchmarks/CategoryImpl.java
new file mode 100644
index 0000000..6086768
--- /dev/null
+++ b/user/src/com/google/gwt/junit/benchmarks/CategoryImpl.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2007 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.junit.benchmarks;
+
+/**
+ * Benchmark Category information. Part of the overall MetaData for a
+ * Benchmark. It is the backing store for com.google.gwt.junit.client.Category.
+ */
+class CategoryImpl {
+
+  private String className;
+  private String description;
+  private String name;
+
+  public CategoryImpl( String className, String name, String description ) {
+    this.className = className;
+    this.name = name;
+    this.description = description;
+  }
+
+  public String getClassName() {
+    return className;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+  
+  public String getName() {
+    return name;
+  }
+}
diff --git a/user/src/com/google/gwt/junit/benchmarks/MetaData.java b/user/src/com/google/gwt/junit/benchmarks/MetaData.java
new file mode 100644
index 0000000..646e292
--- /dev/null
+++ b/user/src/com/google/gwt/junit/benchmarks/MetaData.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2007 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.junit.benchmarks;
+
+/**
+ * The benchmark metadata for a single benchmark method.
+ */
+class MetaData {
+
+  private CategoryImpl category;
+
+  private String className;
+
+  private String methodName;
+
+  private String sourceCode;
+
+  private String testDescription;
+
+  private String testName;
+
+  public MetaData(String className, String methodName, String sourceCode,
+      CategoryImpl category, String testName, String testDescription) {
+    this.className = className;
+    this.methodName = methodName;
+    this.sourceCode = sourceCode;
+    this.category = category;
+    this.testName = testName;
+    this.testDescription = testDescription;
+  }
+
+  public boolean equals(Object obj) {
+    if (! (obj instanceof MetaData)) {
+      return false;
+    }
+
+    MetaData md = (MetaData) obj;
+
+    return md.className.equals(className) && md.methodName.equals(methodName);
+  }
+
+  public CategoryImpl getCategory() {
+    return category;
+  }
+
+  public String getClassName() {
+    return className;
+  }
+
+  public String getMethodName() {
+    return methodName;
+  }
+
+  public String getSourceCode() {
+    return sourceCode;
+  }
+
+  public String getTestDescription() {
+    return testDescription;
+  }
+
+  public String getTestName() {
+    return testName;
+  }
+
+  public int hashCode() {
+    int result;
+    result = (className != null ? className.hashCode() : 0);
+    result = 29 * result + (methodName != null ? methodName.hashCode() : 0);
+    return result;
+  }
+}
+
diff --git a/user/src/com/google/gwt/junit/client/Benchmark.java b/user/src/com/google/gwt/junit/client/Benchmark.java
new file mode 100644
index 0000000..174bfcc
--- /dev/null
+++ b/user/src/com/google/gwt/junit/client/Benchmark.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2007 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.junit.client;
+
+/**
+ * A type of {@link com.google.gwt.junit.client.GWTTestCase} which specifically
+ * records performance results. {@link com.google.gwt.junit.client.Benchmark}s
+ * have additional functionality above and beyond GWT's JUnit support for
+ * standard <code>TestCases</code>.
+ *
+ * <ul>
+ * <li>In a single <code>JUnit</code> run, the results of all executed
+ * benchmarks are collected and stored in an XML report viewable with the
+ * <code>benchmarkViewer</code>.</li>
+ *
+ * <li>GWT automatically removes jitter from your benchmark methods by running
+ * them for a minimum period of time (150ms). GWT also optionally limits your
+ * benchmark execution to a maximum period of time (1000ms).</li>
+ *
+ * <li>GWT supports "begin" and "end" test methods that separate setup and
+ * teardown costs from the actual work being benchmarked. Simply name your
+ * functions "begin[TestMethodName]" and "end[TestMethodName]" and they will
+ * be executed before and after every execution of your test method. The
+ * timings of these setup methods are not included in the test results.</li>
+ *
+ * <li>GWT supports test methods that have parameters. GWT will execute each
+ * benchmark method multiple times in order to exhaustively test all the possible
+ * combinations of parameter values. For each parameter that your test method
+ * accepts, it should document it with the annotation,
+ * <code>&#64;gwt.benchmark.param</code>.
+ *
+ * <p>The syntax for gwt.benchmark.param is
+ * <code>&lt;param name&gt; = &lt;Iterable&gt;</code>. For example,
+ *
+ * <pre>
+ * &#64;gwt.benchmark.param where = java.util.Arrays.asList(
+ *   new Position[] { Position.BEGIN, Position.END, Position.VARIED } )
+ * &#64;gwt.benchmark.param size -limit = insertRemoveRange
+ * public void testArrayListRemoves(Position where, Integer size) { ... }
+ * </pre></p>
+ *
+ * <p>In this example, the annotated function is executed with all the possible
+ * permutations of <code>Position = (BEGIN, END, and VARIED)</code> and
+ * <code>insertRemoveRange = IntRange( 64, Integer.MAX_VALUE, "*", 2 )</code>.
+ * </p>
+ *
+ * <p>This particular example also demonstrates how GWT can automatically limit
+ * the number of executions of your test. Your final parameter (in this example,
+ * size) can optionally be decorated with -limit to indicate to GWT that
+ * it should stop executing additional permutations of the test when the
+ * execution time becomes too long (over 1000ms). So, in this example,
+ * for each value of <code>Position</code>, <code>testArrayListRemoves</code>
+ * will be executed for increasing values of <code>size</code> (beginning with
+ * 64 and increasing in steps of 2), until either it reaches
+ * <code>Integer.MAX_VALUE</code> or the execution time for the last
+ * permutation is > 1000ms.</p>
+ * </li>
+ * </ul>
+ *
+ * <p>{@link Benchmark}s support the following annotations on each test method
+ * in order to decorate each test with additional information useful for
+ * reporting.</p>
+ *
+ * <ul>
+ * <li>&#64;gwt.benchmark.category - The class name of the {@link Category} the
+ * benchmark belongs to. This property may also be set at the
+ * {@link com.google.gwt.junit.client.Benchmark} class level.</li>
+ * </ul>
+ *
+ * <h2>Examples of benchmarking in action</h2>
+ *
+ * <h3>A simple benchmark example</h3>
+ * {@link com.google.gwt.examples.benchmarks.AllocBenchmark} is a simple example
+ * of a basic benchmark that doesn't take advantage of most of benchmarking's
+ * advanced features.
+ *
+ * {@example com.google.gwt.examples.benchmarks.AllocBenchmark}
+ *
+ * <h3>An advanced benchmark example</h3>
+ * {@link com.google.gwt.examples.benchmarks.ArrayListAndVectorBenchmark} is a more
+ * sophisticated example of benchmarking. It demonstrates the use of "begin"
+ * and "end" test methods, parameterized test methods, and automatic
+ * test execution limits.
+ *
+ * {@example com.google.gwt.examples.benchmarks.ArrayListAndVectorBenchmark}
+ */
+public abstract class Benchmark extends GWTTestCase {
+
+  /**
+   * The name of the system property that specifies the location
+   * where benchmark reports are both written to and read from.
+   * Its value is <code>com.google.gwt.junit.reportPath</code>.
+   *
+   * If this system property is not set, the path defaults to the user's
+   * current working directory.
+   */
+  public static final String REPORT_PATH = "com.google.gwt.junit.reportPath";
+}
diff --git a/user/src/com/google/gwt/junit/client/Category.java b/user/src/com/google/gwt/junit/client/Category.java
new file mode 100644
index 0000000..6ef1147
--- /dev/null
+++ b/user/src/com/google/gwt/junit/client/Category.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2007 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.junit.client;
+
+/**
+ * A benchmark category. {@link com.google.gwt.junit.client.Benchmark}s which
+ * use the GWT annotation, <code>@gwt.benchmark.category</code>, must set it to
+ * a class which implements this interface.
+ *
+ * <p>The following GWT annotations can be set on a <code>Category</code>:
+ *
+ * <ul>
+ *   <li><code>@gwt.benchmark.name</code> The name of the <code>Category</code>
+ * </li>
+ *  <li><code>@gwt.benchmark.description</code> The description of the
+ * <code>Category</code></li>
+ * </ul>
+ * </p>
+ * 
+ */
+public interface Category {
+}
diff --git a/user/src/com/google/gwt/junit/client/GWTTestCase.java b/user/src/com/google/gwt/junit/client/GWTTestCase.java
index ceb7c83..69574b2 100644
--- a/user/src/com/google/gwt/junit/client/GWTTestCase.java
+++ b/user/src/com/google/gwt/junit/client/GWTTestCase.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * Copyright 2007 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
@@ -43,7 +43,7 @@
 
   /**
    * Add a checkpoint message to the current test. If this test fails, all
-   * checkpoint messages will be appended to the exception description. This can
+   * checkpoint messages will be appended to the getException description. This can
    * be useful in web mode for determining how far test execution progressed
    * before a failure occurs.
    * 
@@ -64,7 +64,7 @@
    * pin down where exceptions are originating.
    * 
    * @return <code>true</code> for normal JUnit behavior, or
-   *         <code>false</code> to disable normal JUnit exception reporting
+   *         <code>false</code> to disable normal JUnit getException reporting
    */
   public boolean catchExceptions() {
     return true;
@@ -123,8 +123,8 @@
    * <ol>
    * <li> If {@link #finishTest()} is called before the delay period expires,
    * the test will succeed.</li>
-   * <li> If any exception escapes from an event handler during the delay
-   * period, the test will error with the thrown exception.</li>
+   * <li> If any getException escapes from an event handler during the delay
+   * period, the test will error with the thrown getException.</li>
    * <li> If the delay period expires and neither of the above has happened, the
    * test will error with a {@link TimeoutException}. </li>
    * </ol>
@@ -171,6 +171,17 @@
   }
 
   /**
+   * Returns the overall test results for this unit test.
+   *
+   * These TestResults are more comprehensive than JUnit's default test results,
+   * and are automatically collected by GWT's testing infrastructure.
+   */
+  protected final TestResults getTestResults() {
+    // implemented in the translatable version of this class
+    return null;
+  }
+
+  /**
    * Runs the test via the {@link JUnitShell} environment.
    */
   protected final void runTest() throws Throwable {
diff --git a/user/src/com/google/gwt/junit/client/IntRange.java b/user/src/com/google/gwt/junit/client/IntRange.java
new file mode 100644
index 0000000..f2e47f3
--- /dev/null
+++ b/user/src/com/google/gwt/junit/client/IntRange.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2007 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.junit.client;
+
+import java.util.Iterator;
+
+/**
+ * A {@link com.google.gwt.junit.client.Range} that iterates over a start and
+ * end value by a stepping function. Typically used by benchmarks to supply a
+ * range of values over an integral parameter, such as size or length.
+ *
+ */
+public class IntRange implements Range {
+
+  /**
+   * Implementation of the Iterator.
+   *
+   */
+  private static class IntRangeIterator extends RangeIterator {
+
+    int end;
+
+    Operator operator;
+
+    int start;
+
+    int step;
+
+    int value;
+
+    IntRangeIterator(IntRange r) {
+      this.value = this.start = r.start;
+      this.end = r.end;
+      this.operator = r.operator;
+      if (operator == null) {
+        throw new IllegalArgumentException("operator must be \"*\" or \"+\"");
+      }
+      this.step = r.step;
+    }
+
+    public boolean hasNext() {
+      return value <= end;
+    }
+
+    public Object next() {
+      int currentValue = value;
+      value = step();
+      return new Integer(currentValue);
+    }
+
+    public int step() {
+      if (operator == Operator.MULTIPLY) {
+        return value * step;
+      } else {
+        return value + step;
+      }
+    }
+  }
+
+  int end;
+
+  Operator operator;
+
+  int start;
+
+  int step;
+
+  /**
+   * Creates a new range that produces Iterators which begin at
+   * <code>start</code>, end at <code>end</code> and increment by the
+   * stepping function described by <code>operator</code> and
+   * <code>step</code>.
+   *
+   * @param start Initial starting value, inclusive.
+   * @param end Ending value, inclusive.
+   * @param operator The function used to step.
+   * @param step The amount to step by, for each iteration.
+   */
+  public IntRange(int start, int end, Operator operator, int step) {
+    this.start = start;
+    this.end = end;
+    this.operator = operator;
+    this.step = step;
+    if (step <= 0) {
+      throw new IllegalArgumentException("step must be > 0");
+    }
+  }
+
+  public Iterator iterator() {
+    return new IntRangeIterator(this);
+  }
+}
diff --git a/user/src/com/google/gwt/junit/client/Operator.java b/user/src/com/google/gwt/junit/client/Operator.java
new file mode 100644
index 0000000..f773a0a
--- /dev/null
+++ b/user/src/com/google/gwt/junit/client/Operator.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2007 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.junit.client;
+
+/**
+ * A mathematical operator used in {@link com.google.gwt.junit.client.Range}s
+ * to indicate the stepping function.
+ */
+public final class Operator {
+
+  /**
+   * The standard multiplication operator.
+   */
+  public static Operator MULTIPLY = new Operator( "*" );
+
+  /**
+   * The standard addition operator.
+   */
+  public static Operator ADD = new Operator( "+" );
+
+  private String value;
+
+  private Operator(String value) {
+    this.value = value;
+  }
+
+  /**
+   * Returns the textual representation of the <code>Operator</code>.
+   *
+   * @return a non-null {@link String}
+   */
+  public String toString() {
+    return value;
+  }
+}
diff --git a/user/src/com/google/gwt/junit/client/Range.java b/user/src/com/google/gwt/junit/client/Range.java
new file mode 100644
index 0000000..dd1eb98
--- /dev/null
+++ b/user/src/com/google/gwt/junit/client/Range.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2007 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.junit.client;
+
+import java.util.Iterator;
+
+/**
+ * A range of values for a Benchmark parameter.
+ *
+ * A Range produces an Iterator that contains all of the values that a Benchmark
+ * parameter should be tested over.
+ *
+ * Range is unlikely to provide any extra semantics above what you would get
+ * with java.util.Iterable, but it was introduced before GWT's JDK 1.5 support.
+ *
+ */
+public interface Range {
+  Iterator iterator();
+}
diff --git a/user/src/com/google/gwt/junit/client/RangeIterator.java b/user/src/com/google/gwt/junit/client/RangeIterator.java
new file mode 100644
index 0000000..8c37eec
--- /dev/null
+++ b/user/src/com/google/gwt/junit/client/RangeIterator.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2007 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.junit.client;
+
+import java.util.Iterator;
+
+/**
+ * A base class useful for implementing Iterators for Ranges.
+ *
+ */
+public abstract class RangeIterator implements Iterator {
+  public void remove() {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/user/src/com/google/gwt/junit/client/TestResults.java b/user/src/com/google/gwt/junit/client/TestResults.java
new file mode 100644
index 0000000..85d74c1
--- /dev/null
+++ b/user/src/com/google/gwt/junit/client/TestResults.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2007 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.junit.client;
+
+import com.google.gwt.user.client.rpc.IsSerializable;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Encapsulates the results of the execution of a single benchmark. A TestResult
+ * is constructed transparently within a benchmark and reported back to the
+ * JUnit RPC server, JUnitHost. It's then shared (via JUnitMessageQueue) with
+ * JUnitShell and aggregated in BenchmarkReport with other TestResults.
+ *
+ * @skip
+ * @see com.google.gwt.junit.client.impl.JUnitHost
+ * @see com.google.gwt.junit.JUnitMessageQueue
+ * @see com.google.gwt.junit.JUnitShell
+ * @see com.google.gwt.junit.benchmarks.BenchmarkReport
+ */
+public class TestResults implements IsSerializable {
+
+  // Computed at the server, via http header
+  String agent;
+
+  String host;
+
+  /**
+   * The URL of the document on the browser (document.location). This is used to
+   * locate the *cache.html document containing the generated JavaScript for the
+   * test. In the case of hosted mode, this points (uselessly) to the nocache
+   * file, because there is no generated JavaScript.
+   *
+   * Apparently, we can't get this value on the server-side because of the goofy
+   * way HTTP_REFERER is set by different browser implementations of
+   * XMLHttpRequest.
+   */
+  String sourceRef;
+
+  /**
+   * @gwt.typeArgs <com.google.gwt.junit.client.Trial>
+   */
+  List/*<Trial>*/ trials;
+
+  public TestResults() {
+    trials = new ArrayList();
+  }
+
+  public String getAgent() {
+    return agent;
+  }
+
+  public String getHost() {
+    return host;
+  }
+
+  public String getSourceRef() {
+    return sourceRef;
+  }
+
+  public List getTrials() {
+    return trials;
+  }
+
+  public void setAgent(String agent) {
+    this.agent = agent;
+  }
+
+  public void setHost(String host) {
+    this.host = host;
+  }
+
+  public void setSourceRef(String sourceRef) {
+    this.sourceRef = sourceRef;
+  }
+
+  public void setTrials(List trials) {
+    this.trials = trials;
+  }
+
+  public String toString() {
+    return "trials: " + trials + ", sourceRef: " + sourceRef + ", agent: "
+        + agent + ", host: " + host;
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/junit/client/Trial.java b/user/src/com/google/gwt/junit/client/Trial.java
new file mode 100644
index 0000000..6e8b71a
--- /dev/null
+++ b/user/src/com/google/gwt/junit/client/Trial.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2007 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.junit.client;
+
+import com.google.gwt.junit.client.impl.ExceptionWrapper;
+import com.google.gwt.user.client.rpc.IsSerializable;
+
+import java.util.Map;
+import java.util.HashMap;
+
+/**
+ * The result of a single trial-run of a single benchmark method. Each Trial
+ * contains the results of running a benchmark method with one set of
+ * values for its parameters. TestResults for a method will contain Trials
+ * for all permutations of the parameter values. For test methods without
+ * parameters, there is only 1 trial result.
+ *
+ * @skip
+ */
+public class Trial implements IsSerializable {
+
+  ExceptionWrapper exceptionWrapper;
+
+  double runTimeMillis;
+
+  // Deserialized from exceptionWrapper on the server-side
+  transient Throwable exception;
+
+  /**
+   * @gwt.typeArgs <java.lang.String,java.lang.String>
+   */
+  Map/*<String,String>*/ variables;
+
+  /**
+   * Creates a new Trial.
+   *
+   * @param runTimeMillis    The amount of time spent executing the test
+   * @param exceptionWrapper The wrapped getException thrown by the the last
+   *                         test, or <code>null</code> if the last test
+   *                         completed successfully.
+   */
+  public Trial(Map/*<String,String>*/ variables, double runTimeMillis,
+      ExceptionWrapper exceptionWrapper) {
+    this.variables = variables;
+    this.runTimeMillis = runTimeMillis;
+    this.exceptionWrapper = exceptionWrapper;
+  }
+
+  public Trial() {
+    this.variables = new HashMap();
+  }
+
+  public Throwable getException() {
+    return exception;
+  }
+
+  public ExceptionWrapper getExceptionWrapper() {
+    return exceptionWrapper;
+  }
+
+  public double getRunTimeMillis() {
+    return runTimeMillis;
+  }
+
+  /**
+   * Returns the names and values of the variables used in the test. If there
+   * were no variables, the map is empty.
+   */
+  public Map getVariables() {
+    return variables;
+  }
+
+  public void setException(Throwable exception) {
+    this.exception = exception;
+  }
+
+  public void setExceptionWrapper(ExceptionWrapper exceptionWrapper) {
+    this.exceptionWrapper = exceptionWrapper;
+  }
+
+  public void setRunTimeMillis(double runTimeMillis) {
+    this.runTimeMillis = runTimeMillis;
+  }
+
+  public String toString() {
+    return "variables: " + variables + ", exceptionWrapper: " + exceptionWrapper
+        + ", runTimeMillis: " + runTimeMillis;
+  }
+}
diff --git a/user/src/com/google/gwt/junit/client/impl/JUnitHost.java b/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
index ba040c2..4e76afe 100644
--- a/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
+++ b/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * Copyright 2007 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
@@ -16,6 +16,7 @@
 package com.google.gwt.junit.client.impl;
 
 import com.google.gwt.user.client.rpc.RemoteService;
+import com.google.gwt.junit.client.TestResults;
 
 /**
  * An interface for {@link com.google.gwt.junit.client.GWTTestCase} to communicate with the test process
@@ -36,9 +37,8 @@
    * run.
    * 
    * @param testClassName The class name of the calling test case.
-   * @param ew The wrapped exception thrown by the the last test, or
-   *          <code>null</code> if the last test completed successfully.
+   * @param results The results of executing the test
    * @return the name of the next method to run.
    */
-  String reportResultsAndGetNextMethod(String testClassName, ExceptionWrapper ew);
+  String reportResultsAndGetNextMethod(String testClassName, TestResults results);
 }
diff --git a/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java b/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
index dee9c01..41fb319 100644
--- a/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
+++ b/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * Copyright 2007 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
@@ -16,6 +16,7 @@
 package com.google.gwt.junit.client.impl;
 
 import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.junit.client.TestResults;
 
 /**
  * The asynchronous version of {@link JUnitHost}.
@@ -24,24 +25,21 @@
 
   /**
    * Gets the name of next method to run.
-   * 
+   *
    * @param testClassName The class name of the calling test case.
-   * @param callBack The object that will receive the name of the next method to
-   *          run.
+   * @param callBack      The object that will receive the name of the next
+   *                      method to run.
    */
   void getFirstMethod(String testClassName, AsyncCallback callBack);
 
   /**
    * Reports results for the last method run and gets the name of next method to
    * run.
-   * 
+   *
    * @param testClassName The class name of the calling test case.
-   * @param ew The wrapped exception thrown by the the last test, or
-   *          <code>null</code> if the last test completed successfully.
-   * @param callBack The object that will receive the name of the next method to
-   *          run.
+   * @param results       The results of the test.
+   * @param callBack      The object that will receive the name of the next
+   *                      method to run.
    */
-  void reportResultsAndGetNextMethod(String testClassName, ExceptionWrapper ew,
-      AsyncCallback callBack);
-
+  void reportResultsAndGetNextMethod(String testClassName, TestResults results, AsyncCallback callBack);
 }
diff --git a/user/src/com/google/gwt/junit/client/impl/PermutationIterator.java b/user/src/com/google/gwt/junit/client/impl/PermutationIterator.java
new file mode 100644
index 0000000..fce580c
--- /dev/null
+++ b/user/src/com/google/gwt/junit/client/impl/PermutationIterator.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2007 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.junit.client.impl;
+
+import com.google.gwt.junit.client.Range;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Iterates over all the possible permutations available in a list of
+ * {@link com.google.gwt.junit.client.Range}s.
+ *
+ * <p>The simplest way to iterate over the permutations of multiple iterators
+ * is in a nested for loop. The PermutationIterator turns that for loop inside
+ * out into a single iterator, which enables you to access each permutation
+ * in a piecemeal fashion.</p>
+ *
+ */
+public class PermutationIterator implements Iterator {
+
+  /**
+   * A single permutation of all the iterators. Contains the current value
+   * of each iterator for the permutation.
+   *
+   */
+  public static class Permutation {
+    private List values;
+    public Permutation( List values ) {
+      this.values = new ArrayList( values );
+    }
+    public List getValues() {
+      return values;
+    }
+    public String toString() {
+      return values.toString();
+    }
+  }
+
+  private static class ListRange implements Range {
+    private List list;
+    public ListRange( List list ) {
+      this.list = list;
+    }
+    public Iterator iterator() {
+      return list.iterator();
+    }
+  }
+  public static void main( String[] args ) {
+    List ranges = Arrays.asList(
+      new Range[] {
+        new ListRange( Arrays.asList( new String[] {"a", "b", "c" } ) ),
+        new ListRange( Arrays.asList( new String[] {"1", "2", "3" } ) ),
+        new ListRange( Arrays.asList( new String[] {"alpha", "beta", "gamma", "delta" } ) ),
+      }
+    );
+
+    System.out.println("Testing normal iteration.");
+    for ( Iterator it = new PermutationIterator(ranges); it.hasNext(); ) {
+      Permutation p = (Permutation) it.next();
+      System.out.println(p);
+    }
+
+    System.out.println("\nTesting skipping iteration.");
+
+    Iterator skipIterator = Arrays.asList( new String[] {"alpha", "beta", "gamma", "delta" } ).iterator();
+    boolean skipped = true;
+    String skipValue = null;
+    for ( PermutationIterator it = new PermutationIterator(ranges); it.hasNext(); ) {
+      Permutation p = (Permutation) it.next();
+
+      if ( skipped ) {
+        if ( skipIterator.hasNext() ) {
+          skipValue = (String) skipIterator.next();
+          skipped = false;
+        }
+      }
+
+      System.out.println(p);
+
+      String value = (String) p.getValues().get(p.getValues().size() - 1);
+
+      if ( value.equals(skipValue) ) {
+        it.skipCurrentRange();
+        skipped = true;
+      }
+    }
+  }
+  private boolean firstRun = true;
+  private List iterators;
+  private boolean maybeHaveMore = true;
+  private List ranges;
+
+  private boolean rangeSkipped = false;
+
+  private List values;
+
+  /**
+   * Constructs a new PermutationIterator that provides the values for each
+   * possible permutation of <code>ranges</code>.
+   *
+   * @param ranges non-null. Each {@link com.google.gwt.junit.client.Range}
+   * must have at least one element. ranges.size() must be > 1
+   *
+   * TODO(tobyr) Consider if empty Ranges ever make sense in the context of
+   * permutations.
+   *
+   */
+  public PermutationIterator( List ranges ) {
+    this.ranges = ranges;
+
+    iterators = new ArrayList();
+
+    for ( int i = 0; i < ranges.size(); ++i ) {
+      Range r = ( Range ) ranges.get( i );
+      iterators.add( r.iterator() );
+    }
+
+    values = new ArrayList();
+  }
+
+  /**
+   * Returns a new <code>Permutation</code> containing the values of the next
+   * permutation.
+   *
+   * @return a non-null <code>Permutation</code>
+   */
+  public boolean hasNext() {
+
+    if ( ! maybeHaveMore ) {
+      return false;
+    }
+
+    // Walk the iterators from bottom to top checking to see if any still have
+    // any available values
+
+    for ( int currentIterator = iterators.size() - 1; currentIterator >= 0; --currentIterator ) {
+      Iterator it = (Iterator) iterators.get( currentIterator );
+      if ( it.hasNext() ) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  public Object next() {
+    assert hasNext() : "No more available permutations in this iterator.";
+
+    if ( firstRun ) {
+
+      // Initialize all of our iterators and values on the first run
+      for ( int i = 0; i < iterators.size(); ++i ) {
+        Iterator it = ( Iterator ) iterators.get( i );
+        values.add( it.next() );
+      }
+      firstRun = false;
+      return new Permutation( values );
+    }
+
+    if ( rangeSkipped ) {
+      rangeSkipped = false;
+      return new Permutation( values );
+    }
+
+    // Walk through the iterators from bottom to top, finding the first one
+    // which has a value available. Increment it, reset all of the subsequent
+    // iterators, and then return the current permutation.
+    for ( int currentIteratorIndex = iterators.size() - 1; currentIteratorIndex >= 0; --currentIteratorIndex ) {
+      Iterator it = (Iterator) iterators.get( currentIteratorIndex );
+      if ( it.hasNext() ) {
+        values.set( currentIteratorIndex, it.next() );
+        for ( int i = currentIteratorIndex + 1; i < iterators.size(); ++i ) {
+          Range resetRange = (Range) ranges.get( i );
+          Iterator resetIterator = resetRange.iterator();
+          iterators.set(i, resetIterator);
+          values.set( i, resetIterator.next() );
+        }
+
+        return new Permutation( values );
+      }
+    }
+
+    throw new AssertionError( "Assertion failed - Couldn't find a non-empty iterator." );
+  }
+
+  public void remove() {
+    throw new UnsupportedOperationException();
+  }
+
+  /**
+   * Skips the remaining set of values in the bottom 
+   * {@link com.google.gwt.junit.client.Range}. This method affects the results
+   * of both hasNext() and next().
+   *
+   */
+  public void skipCurrentRange() {
+
+    rangeSkipped = true;
+
+    for ( int currentIteratorIndex = iterators.size() - 2; currentIteratorIndex >= 0; --currentIteratorIndex ) {
+      Iterator it = (Iterator) iterators.get( currentIteratorIndex );
+      if ( it.hasNext() ) {
+        values.set( currentIteratorIndex, it.next() );
+        for ( int i = currentIteratorIndex + 1; i < iterators.size(); ++i ) {
+          Range resetRange = (Range) ranges.get( i );
+          Iterator resetIterator = resetRange.iterator();
+          iterators.set( i, resetIterator );
+          values.set( i, resetIterator.next() );
+        }
+        return;
+      }
+    }
+
+    maybeHaveMore = false;
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/junit/rebind/BenchmarkGenerator.java b/user/src/com/google/gwt/junit/rebind/BenchmarkGenerator.java
new file mode 100644
index 0000000..9fec6c1
--- /dev/null
+++ b/user/src/com/google/gwt/junit/rebind/BenchmarkGenerator.java
@@ -0,0 +1,589 @@
+/*
+ * Copyright 2007 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.junit.rebind;
+
+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.JMethod;
+import com.google.gwt.core.ext.typeinfo.JParameter;
+import com.google.gwt.junit.JUnitShell;
+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.user.rebind.SourceWriter;
+
+import java.util.Map;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * 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 MutableBoolean {
+    boolean value;
+  }
+
+  private static final String BEGIN_PREFIX = "begin";
+
+  private static final String BENCHMARK_PARAM_META = "gwt.benchmark.param";
+
+  private static final String EMPTY_FUNC = "__emptyFunc";
+
+  private static final String END_PREFIX = "end";
+
+  private static final String ESCAPE_LOOP = "__escapeLoop";
+
+  /**
+   * Returns all the zero-argument JUnit test methods that do not have
+   * overloads.
+   *
+   * @return Map<String,JMethod>
+   */
+  public static Map getNotOverloadedTestMethods(JClassType requestedClass) {
+    Map methods = getAllMethods(requestedClass, new MethodFilter() {
+      public boolean accept(JMethod method) {
+        return isJUnitTestMethod(method, true);
+      }
+    });
+
+    for (Iterator it = methods.entrySet().iterator(); it.hasNext();) {
+      Map.Entry entry = (Map.Entry) it.next();
+      List methodOverloads = (List) entry.getValue();
+      if (methodOverloads.size() > 1) {
+        it.remove();
+        continue;
+      }
+      entry.setValue(methodOverloads.get(0));
+    }
+
+    return methods;
+  }
+
+  /**
+   * 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 getParameterizedTestMethods(JClassType requestedClass,
+      TreeLogger logger) {
+
+    Map testMethods = getAllMethods(requestedClass, new MethodFilter() {
+      public boolean accept(JMethod method) {
+        return isJUnitTestMethod(method, true);
+      }
+    });
+
+    // Remove all non-overloaded test methods
+    for (Iterator it = testMethods.entrySet().iterator(); it.hasNext();) {
+
+      Map.Entry entry = (Map.Entry) it.next();
+      String name = (String) entry.getKey();
+      List methods = (List) entry.getValue();
+
+      if (methods.size() > 2) {
+        String msg = requestedClass + "." + name
+            + " has more than one overloaded version.\n" +
+            "It will not be included in the test case execution.";
+        logger.log(TreeLogger.WARN, msg, null);
+        it.remove();
+        continue;
+      }
+
+      if (methods.size() == 1) {
+        JMethod method = (JMethod) 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.\n" +
+              "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.
+        it.remove();
+        continue;
+      }
+
+      JMethod method1 = (JMethod) methods.get(0);
+      JMethod method2 = (JMethod) 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.\n" +
+            "It will not be included in the test case execution.";
+        logger.log(TreeLogger.WARN, msg, null);
+        it.remove();
+        continue;
+      }
+
+      entry.setValue(overloadedMethod);
+    }
+
+    return testMethods;
+  }
+
+  private static JMethod getBeginMethod(JClassType type, String name) {
+    StringBuffer methodName = new StringBuffer(name);
+    methodName.replace(0, "test".length(), BEGIN_PREFIX);
+    return getMethod(type, methodName.toString());
+  }
+
+  private static JMethod getEndMethod(JClassType type, String name) {
+    StringBuffer methodName = new StringBuffer(name);
+    methodName.replace(0, "test".length(), END_PREFIX);
+    return getMethod(type, methodName.toString());
+  }
+
+  private static JMethod getMethod(JClassType type, MethodFilter filter) {
+    Map map = getAllMethods(type, filter);
+    Set entrySet = map.entrySet();
+    if (entrySet.size() == 0) {
+      return null;
+    }
+    List methods = (List) ((Map.Entry) entrySet.iterator().next()).getValue();
+    return (JMethod) 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);
+      }
+    });
+  }
+
+  public void writeSource() throws UnableToCompleteException {
+    super.writeSource();
+
+    // Needed for benchmarking the overhead of the function call to the
+    // benchmark
+    generateEmptyFunc(getSourceWriter());
+
+    implementZeroArgTestMethods();
+    implementParameterizedTestMethods();
+    JUnitShell.getReport().addBenchmark(getRequestedClass(), getTypeOracle());
+  }
+
+  /**
+   * 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,
+      boolean generateEscape, Statements recordCode, Statements breakCode) {
+    Statements benchmarkCode = new StatementsList();
+    List benchStatements = benchmarkCode.getStatements();
+
+    ForLoop loop = new ForLoop("int numLoops = 1", "true", "");
+    benchStatements.add(loop);
+    List 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 (generateEscape) {
+      loopStatements.add(new Statement(
+          "if ( numLoops == 1 && duration > 1000 ) {\n" +
+            breakCode.toString() + "\n" +
+          "}\n\n"
+      ));
+    }
+
+    loopStatements.add(new Statement("break"));
+
+    return benchmarkCode;
+  }
+
+  /**
+   * Generates code that executes <code>statements</code> for all possible
+   * values of <code>params</code>. Exports a label named ESCAPE_LOOP that
+   * points to the the "inner loop" that should be escaped to for a limited
+   * variable.
+   *
+   * @return the generated code
+   */
+  private Statements executeForAllValues(JParameter[] methodParams, Map params,
+      Statements statements) {
+    Statements root = new StatementsList();
+    Statements currentContext = root;
+
+    // Profile the setup and teardown costs for this test method
+    // but only if 1 of them exists.
+    for (int i = 0; i < methodParams.length; ++i) {
+      JParameter methodParam = methodParams[i];
+      String paramName = methodParam.getName();
+      String paramValue = (String) params.get(paramName);
+
+      String iteratorName = "it_" + paramName;
+      String initializer = "java.util.Iterator " + iteratorName + " = "
+          + paramValue + ".iterator()";
+      ForLoop loop = new ForLoop(initializer, iteratorName + ".hasNext()", "");
+      if (i == methodParams.length - 1) {
+        loop.setLabel(ESCAPE_LOOP);
+      }
+      currentContext.getStatements().add(loop);
+      String typeName = methodParam.getType().getQualifiedSourceName();
+      loop.getStatements().add(new Statement(typeName + " " + paramName + " = ("
+          + typeName + ") " + iteratorName + ".next()"));
+      currentContext = loop;
+    }
+
+    currentContext.getStatements().add(statements);
+
+    return root;
+  }
+
+  private Statements genBenchTarget(JMethod beginMethod, JMethod endMethod,
+      List paramNames, Statements test) {
+    Statements statements = new StatementsList();
+    List 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;
+  }
+
+  /**
+   * 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 squirrely 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 microbenchmark itself.
+   */
+  private void generateEmptyFunc(SourceWriter writer) {
+    writer.println("private native void " + EMPTY_FUNC + "() /*-{");
+    writer.println("}-*/;");
+    writer.println();
+  }
+
+  private Map/*<String,String>*/ getParamMetaData(JMethod method,
+      MutableBoolean isBounded) throws UnableToCompleteException {
+    Map/*<String,String>*/ params = new HashMap/*<String,String>*/();
+
+    String[][] allValues = method.getMetaData(BENCHMARK_PARAM_META);
+
+    if (allValues == null) {
+      return params;
+    }
+
+    for (int i = 0; i < allValues.length; ++i) {
+      String[] values = allValues[i];
+      StringBuffer result = new StringBuffer();
+      for (int j = 0; j < values.length; ++j) {
+        result.append(values[j]);
+        result.append(" ");
+      }
+      String expr = result.toString();
+      String[] lhsAndRhs = expr.split("=");
+      String paramName = lhsAndRhs[0].trim();
+      String[] nameExprs = paramName.split(" ");
+      if (nameExprs.length > 1 && nameExprs[1].equals("-limit")) {
+        paramName = nameExprs[0];
+        // Make sure this is the last parameter
+        JParameter[] parameters = method.getParameters();
+        if (! parameters[parameters.length - 1].getName().equals(paramName)) {
+          JClassType cls = method.getEnclosingType();
+          String msg = "Error at " + cls + "." + method.getName() + "\n" +
+              "Only the last parameter of a method can be marked with the -limit flag.";
+          logger.log(TreeLogger.ERROR, msg, null);
+          throw new UnableToCompleteException();
+        }
+
+        isBounded.value = true;
+      }
+      String paramValue = lhsAndRhs[1].trim();
+      params.put(paramName, paramValue);
+    }
+
+    return params;
+  }
+
+  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 (Iterator it = parameterizedMethods.entrySet().iterator();
+        it.hasNext();) {
+      Map.Entry entry = (Map.Entry) it.next();
+      String name = (String) entry.getKey();
+      JMethod method = (JMethod) entry.getValue();
+      JMethod beginMethod = getBeginMethod(type, name);
+      JMethod endMethod = getEndMethod(type, name);
+
+      sw.println("public void " + name + "() {");
+      sw.indent();
+      sw.println("  delayTestFinish( 2000 );");
+      sw.println();
+
+      MutableBoolean isBounded = new MutableBoolean();
+      Map params = getParamMetaData(method, isBounded);
+      validateParams(method, params);
+
+      JParameter[] methodParams = method.getParameters();
+      List paramNames = new ArrayList(methodParams.length);
+      for (int i = 0; i < methodParams.length; ++i) {
+        paramNames.add(methodParams[i].getName());
+      }
+
+      List paramValues = new ArrayList(methodParams.length);
+      for (int i = 0; i < methodParams.length; ++i) {
+        paramValues.add(params.get(methodParams[i].getName()));
+      }
+
+      sw.print( "final java.util.List ranges = java.util.Arrays.asList( new com.google.gwt.junit.client.Range[] { " );
+
+      for (int i = 0; i < paramNames.size(); ++i) {
+        String paramName = (String) paramNames.get(i);
+        sw.print( (String) params.get(paramName) );
+        if (i != paramNames.size() - 1) {
+          sw.print( ",");
+        } else {
+          sw.println( "} );" );
+        }
+        sw.print( " " );
+      }
+
+      sw.println(
+          "final com.google.gwt.junit.client.impl.PermutationIterator permutationIt = new com.google.gwt.junit.client.impl.PermutationIterator( ranges );\n" +
+          "com.google.gwt.user.client.DeferredCommand.addCommand( new com.google.gwt.user.client.IncrementalCommand() {\n" +
+          "  public boolean execute() {\n" +
+          "    delayTestFinish( 10000 );\n" +
+          "    if ( permutationIt.hasNext() ) {\n" +
+          "      com.google.gwt.junit.client.impl.PermutationIterator.Permutation permutation = (com.google.gwt.junit.client.impl.PermutationIterator.Permutation) permutationIt.next();\n"
+      );
+
+      for (int i = 0; i < methodParams.length; ++i) {
+        JParameter methodParam = methodParams[i];
+        String typeName = methodParam.getType().getQualifiedSourceName();
+        String paramName = (String) 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(
+          "com.google.gwt.junit.client.TestResults results = getTestResults();\n" +
+          "com.google.gwt.junit.client.Trial trial = new com.google.gwt.junit.client.Trial();\n" +
+          "trial.setRunTimeMillis( " + testTimingName + " - " + setupTimingName + " );\n" +
+          "java.util.Map variables = trial.getVariables();\n");
+
+      for (int i = 0; i < paramNames.size(); ++i) {
+        String paramName = (String) paramNames.get(i);
+        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, false, null, breakCode);
+      testBench = benchmark(testBench, testTimingName, isBounded.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" +
+          "    finishTest();\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() {
+    Map zeroArgMethods = getNotOverloadedTestMethods(getRequestedClass());
+    SourceWriter sw = getSourceWriter();
+    JClassType type = getRequestedClass();
+
+    for (Iterator it = zeroArgMethods.entrySet().iterator(); it.hasNext();) {
+      Map.Entry entry = (Map.Entry) it.next();
+      String name = (String) entry.getKey();
+      JMethod method = (JMethod) entry.getValue();
+      JMethod beginMethod = getBeginMethod(type, name);
+      JMethod endMethod = getEndMethod(type, name);
+
+      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.EMPTY_LIST,
+          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.EMPTY_LIST, testStatements);
+
+      String recordResultsCode =
+          "com.google.gwt.junit.client.TestResults results = getTestResults();\n"  +
+          "com.google.gwt.junit.client.Trial trial = new com.google.gwt.junit.client.Trial();\n"  +
+          "trial.setRunTimeMillis( " + testTimingName + " - " + setupTimingName + " );\n" +
+          "results.getTrials().add( trial )";
+
+      Statements breakCode = new Statement( "  break " + ESCAPE_LOOP );
+
+      setupBench = benchmark(setupBench, setupTimingName, false, null, breakCode);
+      testBench = benchmark(testBench, testTimingName, true,
+          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 params)
+      throws UnableToCompleteException {
+    JParameter[] methodParams = method.getParameters();
+    for (int i = 0; i < methodParams.length; ++i) {
+      JParameter methodParam = methodParams[i];
+      String paramName = methodParam.getName();
+      String paramValue = (String) 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();
+      }
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/junit/rebind/JUnitTestCaseStubGenerator.java b/user/src/com/google/gwt/junit/rebind/JUnitTestCaseStubGenerator.java
index 68a1b9b..2f71036 100644
--- a/user/src/com/google/gwt/junit/rebind/JUnitTestCaseStubGenerator.java
+++ b/user/src/com/google/gwt/junit/rebind/JUnitTestCaseStubGenerator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * Copyright 2007 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
@@ -23,21 +23,143 @@
 import com.google.gwt.core.ext.typeinfo.JMethod;
 import com.google.gwt.core.ext.typeinfo.NotFoundException;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
-import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.core.ext.typeinfo.JParameter;
 import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
 import com.google.gwt.user.rebind.SourceWriter;
 
 import java.io.PrintWriter;
-import java.util.HashSet;
+import java.util.Map;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.HashMap;
 
 /**
  * This class generates a stub class for classes that derive from GWTTestCase.
  * This stub class provides the necessary bridge between our Hosted or Hybrid
  * mode classes and the JUnit system.
+ *
  */
 public class JUnitTestCaseStubGenerator extends Generator {
 
-  private static final String GWT_TESTCASE_CLASS_NAME = GWTTestCase.class.getName();
+  interface MethodFilter {
+    public boolean accept( JMethod method );
+  }
+
+  private static final String GWT_TESTCASE_CLASS_NAME = "com.google.gwt.junit.client.GWTTestCase";
+
+  /**
+   * Returns the method names for the set of methods that are strictly JUnit
+   * test methods (have no arguments).
+   *
+   * @param requestedClass
+   */
+  public static String[] getTestMethodNames(JClassType requestedClass) {
+    return (String[]) getAllMethods( requestedClass, new MethodFilter() {
+      public boolean accept(JMethod method) {
+        return isJUnitTestMethod(method,false);
+      }
+    } ).keySet().toArray( new String[] {} );
+  }
+
+  /**
+   * Like JClassType.getMethod( String name ), except:
+   *
+   *  <li>it accepts a filter</li>
+   *  <li>it searches the inheritance hierarchy (includes subclasses)</li>
+   *
+   * For methods which are overriden, only the most derived implementations are included.
+   *
+   * @param type The type to search. Must not be null
+   * @return Map<String.List<JMethod>> The set of matching methods. Will not be null.
+   */
+  static Map getAllMethods( JClassType type, MethodFilter filter ) {
+    Map methods = new HashMap/*<String,List<JMethod>>*/();
+    JClassType cls = type;
+
+    while (cls != null) {
+      JMethod[] clsDeclMethods = cls.getMethods();
+
+      // For every method, include it iff our filter accepts it
+      // and we don't already have a matching method
+      for (int i = 0, n = clsDeclMethods.length; i < n; ++i) {
+
+        JMethod declMethod = clsDeclMethods[i];
+
+        if ( ! filter.accept(declMethod) ) {
+          continue;
+        }
+
+        List list = (List)methods.get(declMethod.getName());
+
+        if (list == null) {
+          list = new ArrayList();
+          methods.put(declMethod.getName(),list);
+          list.add(declMethod);
+          continue;
+        }
+
+        JParameter[] declParams = declMethod.getParameters();
+
+        for (int j = 0; j < list.size(); ++j) {
+          JMethod method = (JMethod)list.get(j);
+          JParameter[] parameters = method.getParameters();
+          if ( ! equals( declParams, parameters )) {
+            list.add(declMethod );
+          }
+        }
+      }
+      cls = cls.getSuperclass();
+    }
+
+    return methods;
+  }
+
+  /**
+   * Returns true if the method is considered to be a valid JUnit test method.
+   * The criteria are that the method's name begin with "test", have public
+   * access, and not be static. You must choose to include or exclude methods
+   * which have arguments.
+   *
+   */
+  static boolean isJUnitTestMethod(JMethod method, boolean acceptArgs) {
+    if (!method.getName().startsWith("test")) {
+      return false;
+    }
+
+    if (!method.isPublic() || method.isStatic()) {
+      return false;
+    }
+
+    return acceptArgs || method.getParameters().length == 0 && ! acceptArgs;
+  }
+
+  /**
+   * Returns true iff the two sets of parameters are of the same lengths and types.
+   *
+   * @param params1 must not be null
+   * @param params2 must not be null
+   */
+  private static boolean equals( JParameter[] params1, JParameter[] params2 ) {
+    if ( params1.length != params2.length ) {
+      return false;
+    }
+    for ( int i = 0; i < params1.length; ++i ) {
+      if ( params1[ i ].getType() != params2[ i ].getType() ) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  String qualifiedStubClassName;
+  String simpleStubClassName;
+  String typeName;
+  TreeLogger logger;
+  String packageName;
+
+  private JClassType requestedClass;
+  private SourceWriter sourceWriter;
+  private TypeOracle typeOracle;
 
   /**
    * Create a new type that statisfies the rebind request.
@@ -45,40 +167,62 @@
   public String generate(TreeLogger logger, GeneratorContext context,
       String typeName) throws UnableToCompleteException {
 
-    TypeOracle typeOracle = context.getTypeOracle();
+    if ( ! init( logger, context, typeName ) ) {
+      return qualifiedStubClassName;
+    }
+
+    writeSource();
+    sourceWriter.commit( logger );
+
+    return qualifiedStubClassName;
+  }
+
+  public JClassType getRequestedClass() {
+    return requestedClass;
+  }
+
+  public SourceWriter getSourceWriter() {
+    return sourceWriter;
+  }
+
+  public TypeOracle getTypeOracle() {
+    return typeOracle;
+  }
+
+  boolean init(TreeLogger logger, GeneratorContext context,String typeName) throws
+      UnableToCompleteException {
+
+    this.typeName = typeName;
+    this.logger = logger;
+    typeOracle = context.getTypeOracle();
     assert typeOracle != null;
 
-    JClassType requestedClass;
     try {
       requestedClass = typeOracle.getType(typeName);
     } catch (NotFoundException e) {
       logger.log(TreeLogger.ERROR, "Could not find type '" + typeName
-        + "'; please see the log, as this usually indicates a previous error ",
-        e);
+          + "'; please see the log, as this usually indicates a previous error ",
+          e);
       throw new UnableToCompleteException();
     }
 
     // Get the stub class name, and see if its source file exists.
     //
-    String simpleStubClassName = getSimpleStubClassName(requestedClass);
+    simpleStubClassName = getSimpleStubClassName(requestedClass);
+    packageName = requestedClass.getPackage().getName();
+    qualifiedStubClassName = packageName + "." + simpleStubClassName;
 
-    String packageName = requestedClass.getPackage().getName();
-    String qualifiedStubClassName = packageName + "." + simpleStubClassName;
+    sourceWriter = getSourceWriter(logger, context, packageName,
+        simpleStubClassName, requestedClass.getQualifiedSourceName());
 
-    SourceWriter sw = getSourceWriter(logger, context, packageName,
-      simpleStubClassName, requestedClass.getQualifiedSourceName());
-    if (sw == null) {
-      return qualifiedStubClassName;
-    }
+    return sourceWriter != null;
+  }
 
+  void writeSource() throws UnableToCompleteException {
     String[] testMethods = getTestMethodNames(requestedClass);
-    writeGetNewTestCase(simpleStubClassName, sw);
-    writeDoRunTestMethod(testMethods, sw);
-    writeGetTestName(typeName, sw);
-
-    sw.commit(logger);
-
-    return qualifiedStubClassName;
+    writeGetNewTestCase(simpleStubClassName, sourceWriter);
+    writeDoRunTestMethod(testMethods, sourceWriter);
+    writeGetTestName(typeName, sourceWriter);
   }
 
   /**
@@ -87,7 +231,7 @@
   private String getSimpleStubClassName(JClassType baseClass) {
     return "__" + baseClass.getSimpleSourceName() + "_unitTestImpl";
   }
- 
+
   private SourceWriter getSourceWriter(TreeLogger logger, GeneratorContext ctx,
       String packageName, String className, String superclassName) {
 
@@ -104,70 +248,6 @@
     return composerFactory.createSourceWriter(ctx, printWriter);
   }
 
-  /**
-   * Given a class return all methods that are considered JUnit test methods up
-   * to but not including the declared methods of the class named
-   * GWT_TESTCASE_CLASS_NAME.
-   */
-  private String[] getTestMethodNames(JClassType requestedClass) {
-    HashSet testMethodNames = new HashSet();
-    JClassType cls = requestedClass;
-
-    while (true) {
-      // We do not consider methods in the GWT superclass or above
-      //
-      if (isGWTTestCaseClass(cls)) {
-        break;
-      }
-
-      JMethod[] clsDeclMethods = cls.getMethods();
-      for (int i = 0, n = clsDeclMethods.length; i < n; ++i) {
-        JMethod declMethod = clsDeclMethods[i];
-
-        // Skip methods that are not JUnit test methods.
-        //
-        if (!isJUnitTestMethod(declMethod)) {
-          continue;
-        }
-
-        if (testMethodNames.contains(declMethod.getName())) {
-          continue;
-        }
-
-        testMethodNames.add(declMethod.getName());
-      }
-
-      cls = cls.getSuperclass();
-    }
-
-    return (String[]) testMethodNames.toArray(new String[testMethodNames.size()]);
-  }
-
-  /**
-   * Returns true if the class is the special GWT Test Case derived class.
-   */
-  private boolean isGWTTestCaseClass(JClassType cls) {
-    return cls.getQualifiedSourceName().equalsIgnoreCase(
-      GWT_TESTCASE_CLASS_NAME);
-  }
-
-  /**
-   * Returns true if the method is considered to be a valid JUnit test method.
-   * The criteria are that the method's name begin with "test", have public
-   * access, and not be static.
-   */
-  private boolean isJUnitTestMethod(JMethod method) {
-    if (!method.getName().startsWith("test")) {
-      return false;
-    }
-
-    if (!method.isPublic() || method.isStatic()) {
-      return false;
-    }
-
-    return true;
-  }
-
   private void writeDoRunTestMethod(String[] testMethodNames, SourceWriter sw) {
     sw.println();
     sw.println("protected final void doRunTest(String name) throws Throwable {");
diff --git a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
index 67eb6e8..e62a581 100644
--- a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
+++ b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * Copyright 2007 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
@@ -20,41 +20,50 @@
 import com.google.gwt.junit.client.impl.ExceptionWrapper;
 import com.google.gwt.junit.client.impl.JUnitHost;
 import com.google.gwt.junit.client.impl.StackTraceWrapper;
+import com.google.gwt.junit.client.TestResults;
+import com.google.gwt.junit.client.Trial;
 import com.google.gwt.user.client.rpc.InvocationException;
 import com.google.gwt.user.server.rpc.RemoteServiceServlet;
 
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
 
 /**
- * An RPC servlet that serves as a proxy to GWTUnitTestShell. Enables
+ * An RPC servlet that serves as a proxy to JUnitTestShell. Enables
  * communication between the unit test code running in a browser and the real
  * test process.
  */
 public class JUnitHostImpl extends RemoteServiceServlet implements JUnitHost {
 
   /**
+   * A maximum timeout to wait for the test system to respond with the next
+   * test. Practically speaking, the test system should respond nearly instantly
+   * if there are furthur tests to run.
+   */
+  private static final int TIME_TO_WAIT_FOR_TESTNAME = 300000;
+
+  // DEBUG timeout
+  // TODO(tobyr) Make this configurable
+  // private static final int TIME_TO_WAIT_FOR_TESTNAME = 500000;
+
+  /**
    * A hook into GWTUnitTestShell, the underlying unit test process.
    */
   private static JUnitMessageQueue sHost = null;
 
   /**
-   * A maximum timeout to wait for the test system to respond with the next
-   * test. Practically speaking, the test system should respond nearly instantly
-   * if there are furthur tests to run.
-   */
-  private static final int TIME_TO_WAIT_FOR_TESTNAME = 5000;
-
-  /**
-   * Tries to grab the GWTUnitTestShell sHost environment to communicate with the
-   * real test process.
+   * Tries to grab the GWTUnitTestShell sHost environment to communicate with
+   * the real test process.
    */
   private static synchronized JUnitMessageQueue getHost() {
     if (sHost == null) {
       sHost = JUnitShell.getMessageQueue();
       if (sHost == null) {
         throw new InvocationException(
-          "Unable to find JUnitShell; is this servlet running under GWTTestCase?");
+            "Unable to find JUnitShell; is this servlet running under GWTTestCase?");
       }
     }
     return sHost;
@@ -72,14 +81,27 @@
   }
 
   public String getFirstMethod(String testClassName) {
-    return getHost().getNextTestName(testClassName, TIME_TO_WAIT_FOR_TESTNAME);
+    return getHost().getNextTestName(getClientId(), testClassName,
+        TIME_TO_WAIT_FOR_TESTNAME);
   }
 
   public String reportResultsAndGetNextMethod(String testClassName,
-      ExceptionWrapper ew) {
+      TestResults results) {
     JUnitMessageQueue host = getHost();
-    host.reportResults(testClassName, deserialize(ew));
-    return host.getNextTestName(testClassName, TIME_TO_WAIT_FOR_TESTNAME);
+    HttpServletRequest request = getThreadLocalRequest();
+    String agent = request.getHeader("User-Agent");
+    results.setAgent(agent);
+    String machine = request.getRemoteHost();
+    results.setHost(machine);
+    List trials = results.getTrials();
+    for (int i = 0; i < trials.size(); ++i) {
+      Trial trial = (Trial) trials.get(i);
+      ExceptionWrapper ew = trial.getExceptionWrapper();
+      trial.setException(deserialize(ew));
+    }
+    host.reportResults(testClassName, results);
+    return host.getNextTestName(getClientId(), testClassName,
+        TIME_TO_WAIT_FOR_TESTNAME);
   }
 
   /**
@@ -97,14 +119,14 @@
       try {
         // try ExType(String, Throwable)
         Constructor ctor = exClass.getDeclaredConstructor(new Class[]{
-          String.class, Throwable.class});
+            String.class, Throwable.class});
         ctor.setAccessible(true);
         ex = (Throwable) ctor.newInstance(new Object[]{ew.message, cause});
       } catch (Throwable e) {
         // try ExType(String)
         try {
           Constructor ctor = exClass
-            .getDeclaredConstructor(new Class[]{String.class});
+              .getDeclaredConstructor(new Class[]{String.class});
           ctor.setAccessible(true);
           ex = (Throwable) ctor.newInstance(new Object[]{ew.message});
           ex.initCause(cause);
@@ -112,7 +134,7 @@
           // try ExType(Throwable)
           try {
             Constructor ctor = exClass
-              .getDeclaredConstructor(new Class[]{Throwable.class});
+                .getDeclaredConstructor(new Class[]{Throwable.class});
             ctor.setAccessible(true);
             ex = (Throwable) ctor.newInstance(new Object[]{cause});
             setField(exClass, "detailMessage", ex, ew.message);
@@ -126,8 +148,8 @@
               setField(exClass, "detailMessage", ex, ew.message);
             } catch (Throwable e4) {
               // we're out of options
-              this.log("Failed to deserialize exception of type '"
-                + ew.typeName + "'; no available constructor", e4);
+              this.log("Failed to deserialize getException of type '"
+                  + ew.typeName + "'; no available constructor", e4);
 
               // fall through
             }
@@ -136,8 +158,9 @@
       }
 
     } catch (Throwable e) {
-      this.log("Failed to deserialize exception of type '" + ew.typeName + "'",
-        e);
+      this.log(
+          "Failed to deserialize getException of type '" + ew.typeName + "'",
+          e);
     }
 
     if (ex == null) {
@@ -154,13 +177,14 @@
   private StackTraceElement deserialize(StackTraceWrapper stw) {
     StackTraceElement ste = null;
     Object[] args = new Object[]{
-      stw.className, stw.methodName, stw.fileName, new Integer(stw.lineNumber)};
+        stw.className, stw.methodName, stw.fileName,
+        new Integer(stw.lineNumber)};
     try {
       try {
         // Try the 4-arg ctor (JRE 1.5)
         Constructor ctor = StackTraceElement.class
-          .getDeclaredConstructor(new Class[]{
-            String.class, String.class, String.class, int.class});
+            .getDeclaredConstructor(new Class[]{
+                String.class, String.class, String.class, int.class});
         ctor.setAccessible(true);
         ste = (StackTraceElement) ctor.newInstance(args);
       } catch (NoSuchMethodException e) {
@@ -191,4 +215,13 @@
     return result;
   }
 
+  /**
+   * Returns a "client id" for the current request.
+   */
+  private String getClientId() {
+    HttpServletRequest request = getThreadLocalRequest();
+    String agent = request.getHeader("User-Agent");
+    String machine = request.getRemoteHost();
+    return machine + " / " + agent;
+  }
 }
diff --git a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/GWTTestCase.java b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/GWTTestCase.java
index adee751..4e22764 100644
--- a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/GWTTestCase.java
+++ b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/GWTTestCase.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * Copyright 2007 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
@@ -76,4 +76,7 @@
     impl.finishTest();
   }
 
+  protected final TestResults getTestResults() {
+    return impl.getTestResults();
+  }
 }
diff --git a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTTestCaseImpl.java b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTTestCaseImpl.java
index d727217..164fd5f 100644
--- a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTTestCaseImpl.java
+++ b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTTestCaseImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * Copyright 2007 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
@@ -22,6 +22,9 @@
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.rpc.ServiceDefTarget;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.junit.client.TestResults;
+import com.google.gwt.junit.client.Trial;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -173,6 +176,17 @@
   private KillTimer timer;
 
   /**
+   * The time the test began execution;
+   */
+  private long testBeginMillis;
+
+  /**
+   * Collective test results.
+   *
+   */
+  private TestResults results = new TestResults();
+
+  /**
    * Constructs a new GWTTestCaseImpl that is paired one-to-one with a
    * {@link GWTTestCase}.
    * 
@@ -288,28 +302,69 @@
    * @param ex The results of this test.
    */
   private void reportResultsAndRunNextMethod(Throwable ex) {
-    testIsFinished = true;
-
-    resetAsyncState();
+    List trials = results.getTrials();
 
     if (serverless) {
       // That's it, we're done
       return;
     }
+    
+    // TODO(tobyr) - Consider making this logic polymorphic which will remove
+    //               instanceof test
+    //
+    // If this is not a benchmark, we have to create a fake trial run
+    if ( ! (outer instanceof com.google.gwt.junit.client.Benchmark) ) {
+      Trial trial = new Trial();
+      long testDurationMillis = System.currentTimeMillis() - testBeginMillis;
+      trial.setRunTimeMillis( testDurationMillis );
 
-    ExceptionWrapper ew = null;
-    if (ex != null) {
-      ew = new ExceptionWrapper(ex);
-      if (checkPoints != null) {
-        for (int i = 0, c = checkPoints.size(); i < c; ++i) {
-          ew.message += "\n" + checkPoints.get(i);
+      if (ex != null) {
+        ExceptionWrapper ew = new ExceptionWrapper(ex);
+        if (checkPoints != null) {
+          for (int i = 0, c = checkPoints.size(); i < c; ++i) {
+            ew.message += "\n" + checkPoints.get(i);
+          }
         }
+        trial.setExceptionWrapper( ew );
+      }
+
+      trials.add( trial );
+    }
+    // If this was a benchmark, we need to handle exceptions specially
+    else {
+      // If an exception occurred, it happened without the trial being recorded
+      // We, unfortunately, don't know the trial parameters at this point.
+      // We should consider putting the exception handling code directly into
+      // the generated Benchmark subclasses.
+      if (ex != null) {
+        ExceptionWrapper ew = new ExceptionWrapper(ex);
+        if (checkPoints != null) {
+          for (int i = 0, c = checkPoints.size(); i < c; ++i) {
+            ew.message += "\n" + checkPoints.get(i);
+          }
+        }
+        Trial trial = new Trial();
+        trial.setExceptionWrapper( ew );
+        trials.add( trial );
       }
     }
-    junitHost.reportResultsAndGetNextMethod(outer.getTestName(), ew,
-        junitHostListener);
+
+    results.setSourceRef( getDocumentLocation() );
+    testIsFinished = true;
+    resetAsyncState();
+    String testName = outer.getTestName();
+    junitHost.reportResultsAndGetNextMethod(testName, results, junitHostListener);
   }
 
+  public TestResults getTestResults() {
+    return results;
+  }
+
+  private native String getDocumentLocation() /*-{
+    return $doc.location.toString();
+  }-*/;
+
+
   /**
    * Cleans up any asynchronous mode state.
    */
@@ -334,6 +389,11 @@
    */
   private void runTest() {
     Throwable caught = null;
+
+    testBeginMillis = System.currentTimeMillis();
+    results = new TestResults();
+
+
     if (shouldCatchExceptions()) {
       // Make sure no exceptions escape
       GWT.setUncaughtExceptionHandler(this);
diff --git a/user/test/com/google/gwt/junit/client/ParallelRemoteTest.java b/user/test/com/google/gwt/junit/client/ParallelRemoteTest.java
new file mode 100644
index 0000000..05c29a1
--- /dev/null
+++ b/user/test/com/google/gwt/junit/client/ParallelRemoteTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2007 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.junit.client;
+
+/**
+ * This class tests the -remoteweb parallel execution features in GWT's JUnit
+ * support. This test should not be part of the automatically run test suite,
+ * because it intentionally generates failures at different browser clients.
+ *
+ * What we're looking for in the output of this test is that the failures
+ * additionally contain the host and browser at which the test failed.
+ *
+ * To run this test correctly, you should be using the -remoteweb option
+ * with at least three different clients.
+ *
+ */
+public class ParallelRemoteTest extends GWTTestCase {
+
+  public String getModuleName() {
+    return "com.google.gwt.junit.JUnit";
+  }
+
+  public void testAssertFailsOnNotIE() {
+    String agent = getAgent().toLowerCase();
+    if ( agent.indexOf( "msie") == -1 ) {
+      fail( "Browser is not IE." );
+    }
+  }
+
+  public void testAssertFailsOnNotSafari() {
+    String agent = getAgent().toLowerCase();
+    if ( agent.indexOf( "safari") == -1 ) {
+      fail( "Browser is not Safari." );
+    }
+  }
+
+  public void testExceptionFailsOnNotIE() {
+    String agent = getAgent().toLowerCase();
+    if ( agent.indexOf( "msie") == -1 ) {
+      throw new RuntimeException( "Browser is not IE." );
+    }
+  }
+
+  public void testExceptionFailsOnNotSafari() {
+    String agent = getAgent().toLowerCase();
+    if ( agent.indexOf( "safari") == -1 ) {
+      throw new RuntimeException( "Browser is not Safari." );
+    }
+  }
+
+  private native String getAgent() /*-{
+    return navigator.userAgent.toString();
+  }-*/;
+}
diff --git a/user/test/com/google/gwt/user/client/ui/ArrayListAndVectorProfile.java b/user/test/com/google/gwt/user/client/ui/ArrayListAndVectorProfile.java
deleted file mode 100644
index c3b328b..0000000
--- a/user/test/com/google/gwt/user/client/ui/ArrayListAndVectorProfile.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2007 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.user.client.ui;
-
-import java.util.ArrayList;
-import java.util.Vector;
-
-/**
- * Profile class.
- */
-public class ArrayListAndVectorProfile extends WidgetProfile {
-
-  private static final int UPPER_BOUND = 40000;
-  private static final int LOWER_BOUND = 1024;
-
-  public void testTiming() throws Exception {
-
-    for (int i = LOWER_BOUND; i < UPPER_BOUND; i = i * 2) {
-      testTiming(i);
-    }
-
-    throw new Exception("Finished profiling");
-  }
-
-  public void testTiming(int i) {
-    arrayListTiming(i);
-    vectorTiming(i);
-  }
-
-  public void vectorTiming(int num) {
-    resetTimer();
-    Vector v = new Vector();
-    for (int i = 0; i < num; i++) {
-      v.add("hello");
-    }
-    timing("vector | add(" + num + ")");
-    resetTimer();
-    for (int k = 0; k < num; k++) {
-      v.get(k);
-    }
-
-    timing("vector | get(" + num  + ")");
-  }
-
-  public void arrayListTiming(int num) {
-    resetTimer();
-    ArrayList v = new ArrayList();
-    for (int i = 0; i < num; i++) {
-      v.add("hello");
-    }
-    timing("arrayList | add(" + num + ")");
-    resetTimer();
-    for (int k = 0; k < num; k++) {
-      v.get(k);
-    }
-
-    timing("arrayList | get(" + num + ")");
-  }
-}