Add benchmark graphing utility

This is a small utility that converts from JMH JSON output to a plotly
HTML file. This allows one to visualize the benchmark data easily.
diff --git a/benchmark-graphs/build.gradle b/benchmark-graphs/build.gradle
new file mode 100644
index 0000000..9f5ee15
--- /dev/null
+++ b/benchmark-graphs/build.gradle
@@ -0,0 +1,8 @@
+apply plugin: 'application'
+
+dependencies {
+    compile 'com.bazaarvoice.jolt:jolt-core:0.1.0'
+    compile 'com.bazaarvoice.jolt:json-utils:0.1.0'
+}
+
+mainClassName = 'org.conscrypt.graphgen.Main'
diff --git a/benchmark-graphs/src/main/java/org/conscrypt/graphgen/Main.java b/benchmark-graphs/src/main/java/org/conscrypt/graphgen/Main.java
new file mode 100644
index 0000000..db8e21a
--- /dev/null
+++ b/benchmark-graphs/src/main/java/org/conscrypt/graphgen/Main.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * 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 org.conscrypt.graphgen;
+
+import static java.nio.file.FileVisitResult.CONTINUE;
+
+import com.bazaarvoice.jolt.Chainr;
+import com.bazaarvoice.jolt.JsonUtils;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility to convert from the JMH JSON output to an HTML file.
+ */
+public class Main {
+
+  public static final String JSON_TEMPLATES = "/json/templates/";
+  public static final String HTML_TEMPLATES = "/html/";
+
+  public static void main(String[] args) throws IOException, URISyntaxException {
+    if (args.length != 3) {
+      System.err.println("Usage: graphgen [template] [input.json] [output.html]");
+      listAllResources(System.err);
+      System.exit(1);
+    }
+
+    try (InputStream spec = Main.class.getResourceAsStream(JSON_TEMPLATES + args[0]);
+        InputStream jmhIn = new BufferedInputStream(new FileInputStream(args[1]));
+        OutputStream output = new BufferedOutputStream(new FileOutputStream(args[2]))) {
+      writeHtml(output, "header.html");
+      convertJmhJsonData(spec, jmhIn, output);
+      writeHtml(output, "footer.html");
+    }
+  }
+
+  private static void writeHtml(OutputStream out, String name) throws IOException {
+    InputStream header = Main.class.getResourceAsStream(HTML_TEMPLATES + name);
+    byte[] buffer = new byte[4096];
+    int numRead;
+    while ((numRead = header.read(buffer)) != -1) {
+      out.write(buffer, 0, numRead);
+    }
+  }
+
+  /**
+   * Load the JSON template data and convert it.
+   */
+  private static void convertJmhJsonData(InputStream specIn, InputStream jmhIn, OutputStream out) throws IOException {
+    List<?> chainrConfig = JsonUtils.jsonToList(specIn);
+    Chainr chainr = Chainr.fromSpec(chainrConfig);
+    List<Object> input = JsonUtils.jsonToList(jmhIn);
+    Object jsonOutput = chainr.transform(input);
+    out.write(JsonUtils.toJsonString(jsonOutput).getBytes(StandardCharsets.UTF_8));
+  }
+
+  /**
+   * Lists all the JSON templates in the Classpath.
+   */
+  private static void listAllResources(PrintStream err) throws IOException, URISyntaxException {
+    URI uri = Main.class.getResource(JSON_TEMPLATES).toURI();
+
+    final Path templatesPath;
+    if (uri.getScheme().equals("jar")) {
+      FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap());
+      templatesPath = fs.getPath(JSON_TEMPLATES);
+    } else {
+      templatesPath = Paths.get(uri);
+    }
+
+    err.println("Possible templates:");
+    PrintFileNames pfn = new PrintFileNames("  ", err);
+    Files.walkFileTree(templatesPath, pfn);
+  }
+
+  private static class PrintFileNames extends SimpleFileVisitor<Path> {
+    private final String prefix;
+    private final PrintStream out;
+
+    public PrintFileNames(String prefix, PrintStream out) {
+      this.prefix = prefix;
+      this.out = out;
+    }
+
+    @Override
+    public FileVisitResult visitFile(Path path, BasicFileAttributes basicFileAttributes)
+        throws IOException {
+      out.println(prefix + path.getFileName());
+      return CONTINUE;
+    }
+  }
+}
diff --git a/benchmark-graphs/src/main/resources/html/footer.html b/benchmark-graphs/src/main/resources/html/footer.html
new file mode 100644
index 0000000..4f96fd3
--- /dev/null
+++ b/benchmark-graphs/src/main/resources/html/footer.html
@@ -0,0 +1,33 @@
+;
+
+var d3 = Plotly.d3;
+
+var WIDTH_IN_PERCENT_OF_PARENT = 60,
+    HEIGHT_IN_PERCENT_OF_PARENT = 80;
+
+var gd3 = d3.select('body')
+    .append('div')
+    .style({
+        width: WIDTH_IN_PERCENT_OF_PARENT + '%',
+        'margin-left': (100 - WIDTH_IN_PERCENT_OF_PARENT) / 2 + '%',
+
+        height: HEIGHT_IN_PERCENT_OF_PARENT + 'vh',
+        'margin-top': (100 - HEIGHT_IN_PERCENT_OF_PARENT) / 2 + 'vh'
+    });
+
+var gd = gd3.node();
+
+Plotly.plot(gd, plotlydata['data'], plotlydata['layout']);
+
+window.onresize = function() {
+  Plotly.Plots.resize(gd);
+};
+
+};
+
+window.onload = onload;
+</script>
+</head>
+<body>
+</body>
+</html>
\ No newline at end of file
diff --git a/benchmark-graphs/src/main/resources/html/header.html b/benchmark-graphs/src/main/resources/html/header.html
new file mode 100644
index 0000000..fa1b573
--- /dev/null
+++ b/benchmark-graphs/src/main/resources/html/header.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+  <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
+  <script>
+var onload = function() {
+
+var plotlydata =
\ No newline at end of file
diff --git a/benchmark-graphs/src/main/resources/json/templates/SslEngineBenchmark.json b/benchmark-graphs/src/main/resources/json/templates/SslEngineBenchmark.json
new file mode 100644
index 0000000..5888356
--- /dev/null
+++ b/benchmark-graphs/src/main/resources/json/templates/SslEngineBenchmark.json
@@ -0,0 +1,80 @@
+[
+  {
+    "operation": "modify-overwrite-beta",
+    "spec": {
+      "*": {
+        "key-name": "=concat(@(1,params.sslProvider),' ',@(1,params.bufferType))"
+      }
+    }
+  },
+  {
+    "operation": "shift",
+    "spec": {
+      // pivot the data by benchmark name
+      "*": {
+        "key-name": {
+          "*": { // match any value of name
+            // go up the tree 3 levels
+            // grab the whole array element and
+            // write it to the output in an array
+            // aka benchmark1[] or benchmark2[]
+            "@(3,[&2])": "&.[]"
+          }
+        }
+      }
+    }
+  },
+  {
+    "operation": "shift",
+    "spec": {
+      // now group the relevant data
+      "*": { // benchmark 1 or 2
+        "$": "&.name",
+        "*": { // array of benchmark1 or 2
+          "params": {
+            "messageSize": "&3.x[]"
+          },
+          "primaryMetric": {
+            "score": "&3.y[]",
+            "scoreError": "&3.error_y.array[]"
+          }
+        }
+      }
+    }
+  },
+  {
+    // now convert from a map to a top level list
+    "operation": "shift",
+    "spec": {
+      // benchmark1 or benchmark2
+      "*": "data.[#1]"
+    }
+  },
+  {
+    // add graph default stuff
+    "operation": "modify-default-beta",
+    "spec": {
+      "data": {
+        "*": {
+          "mode": "lines+markers",
+          "type": "scatter"
+        }
+      },
+      "layout": {
+        "autosize": true,
+        "yaxis": {
+          "type": "linear",
+          "autorange": true,
+          "title": "messages/sec"
+        },
+        "title": "SslEngineBenchmark.sendMessage",
+        "showlegend": true,
+        "xaxis": {
+          "title": "size of message",
+          "type": "category",
+          "autorange": true
+        }
+      }
+    }
+  }
+]
diff --git a/settings.gradle b/settings.gradle
index e24ab2b..894bf75 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -9,6 +9,7 @@
 include ":conscrypt-android-platform"
 include ":conscrypt-libcore-stub"
 include ":conscrypt-api-doclet"
+include ":conscrypt-benchmark-graphs"
 
 project(':conscrypt-constants').projectDir = "$rootDir/constants" as File
 project(':conscrypt-openjdk').projectDir = "$rootDir/openjdk" as File
@@ -20,3 +21,4 @@
 project(':conscrypt-libcore-stub').projectDir = "$rootDir/libcore-stub" as File
 project(':conscrypt-android-platform').projectDir = "$rootDir/platform" as File
 project(':conscrypt-api-doclet').projectDir = "$rootDir/api-doclet" as File
+project(':conscrypt-benchmark-graphs').projectDir = "$rootDir/benchmark-graphs" as File