tool: add -json flag to |speed|

Add a flag to speed.cc to generate machine-readable benchmark results.

Change-Id: I24a4324c5195b15494dc6d9471aa91c27b9f881d
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/37865
Reviewed-by: Adam Langley <alangley@gmail.com>
Reviewed-by: Zola Bridges <zbrid@google.com>
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/tool/speed.cc b/tool/speed.cc
index 160d90f..68073a9 100644
--- a/tool/speed.cc
+++ b/tool/speed.cc
@@ -13,9 +13,9 @@
  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
 
 #include <algorithm>
-#include <string>
 #include <functional>
 #include <memory>
+#include <string>
 #include <vector>
 
 #include <assert.h>
@@ -54,6 +54,8 @@
 
 #include "../third_party/sike/sike.h"
 
+// g_print_json is true if printed output is JSON formatted.
+static bool g_print_json = false;
 
 // TimeResults represents the results of benchmarking a function.
 struct TimeResults {
@@ -62,20 +64,54 @@
   // us is the number of microseconds that elapsed in the time period.
   unsigned us;
 
-  void Print(const std::string &description) {
-    printf("Did %u %s operations in %uus (%.1f ops/sec)\n", num_calls,
-           description.c_str(), us,
-           (static_cast<double>(num_calls) / us) * 1000000);
+  void Print(const std::string &description) const {
+    if (g_print_json) {
+      PrintJSON(description);
+    } else {
+      printf("Did %u %s operations in %uus (%.1f ops/sec)\n", num_calls,
+             description.c_str(), us,
+             (static_cast<double>(num_calls) / us) * 1000000);
+    }
   }
 
-  void PrintWithBytes(const std::string &description, size_t bytes_per_call) {
-    printf("Did %u %s operations in %uus (%.1f ops/sec): %.1f MB/s\n",
-           num_calls, description.c_str(), us,
-           (static_cast<double>(num_calls) / us) * 1000000,
-           static_cast<double>(bytes_per_call * num_calls) / us);
+  void PrintWithBytes(const std::string &description,
+                      size_t bytes_per_call) const {
+    if (g_print_json) {
+      PrintJSON(description, bytes_per_call);
+    } else {
+      printf("Did %u %s operations in %uus (%.1f ops/sec): %.1f MB/s\n",
+             num_calls, description.c_str(), us,
+             (static_cast<double>(num_calls) / us) * 1000000,
+             static_cast<double>(bytes_per_call * num_calls) / us);
+    }
   }
+
+ private:
+  void PrintJSON(const std::string &description,
+                 size_t bytes_per_call = 0) const {
+    if (first_json_printed) {
+      puts(",");
+    }
+
+    printf("{\"description\": \"%s\", \"numCalls\": %u, \"microseconds\": %u",
+           description.c_str(), num_calls, us);
+
+    if (bytes_per_call > 0) {
+      printf(", \"bytesPerCall\": %zu", bytes_per_call);
+    }
+
+    printf("}");
+    first_json_printed = true;
+  }
+
+  // first_json_printed is true if |g_print_json| is true and the first item in
+  // the JSON results has been printed already. This is used to handle the
+  // commas between each item in the result list.
+  static bool first_json_printed;
 };
 
+bool TimeResults::first_json_printed = false;
+
 #if defined(OPENSSL_WINDOWS)
 static uint64_t time_now() { return GetTickCount64() * 1000; }
 #elif defined(OPENSSL_APPLE)
@@ -274,24 +310,29 @@
     }
 
     std::sort(durations.begin(), durations.end());
-    printf("Did %u RSA %d key-gen operations in %uus (%.1f ops/sec)\n",
-           num_calls, size, us,
-           (static_cast<double>(num_calls) / us) * 1000000);
+    const std::string description =
+        std::string("RSA ") + std::to_string(size) + std::string(" key-gen");
+    const TimeResults results = {num_calls, us};
+    results.Print(description);
     const size_t n = durations.size();
     assert(n > 0);
 
-    // |min| and |max| must be stored in temporary variables to avoid an MSVC
-    // bug on x86. There, size_t is a typedef for unsigned, but MSVC's printf
-    // warning tries to retain the distinction and suggest %zu for size_t
-    // instead of %u. It gets confused if std::vector<unsigned> and
-    // std::vector<size_t> are both instantiated. Being typedefs, the two
-    // instantiations are identical, which somehow breaks the size_t vs unsigned
-    // metadata.
-    unsigned min = durations[0];
-    unsigned median = n & 1 ? durations[n / 2]
-                            : (durations[n / 2 - 1] + durations[n / 2]) / 2;
-    unsigned max = durations[n - 1];
-    printf("  min: %uus, median: %uus, max: %uus\n", min, median, max);
+    // Distribution information is useful, but doesn't fit into the standard
+    // format used by |g_print_json|.
+    if (!g_print_json) {
+      // |min| and |max| must be stored in temporary variables to avoid an MSVC
+      // bug on x86. There, size_t is a typedef for unsigned, but MSVC's printf
+      // warning tries to retain the distinction and suggest %zu for size_t
+      // instead of %u. It gets confused if std::vector<unsigned> and
+      // std::vector<size_t> are both instantiated. Being typedefs, the two
+      // instantiations are identical, which somehow breaks the size_t vs
+      // unsigned metadata.
+      unsigned min = durations[0];
+      unsigned median = n & 1 ? durations[n / 2]
+                              : (durations[n / 2 - 1] + durations[n / 2]) / 2;
+      unsigned max = durations[n - 1];
+      printf("  min: %uus, median: %uus, max: %uus\n", min, median, max);
+    }
   }
 
   return true;
@@ -979,6 +1020,16 @@
         "16,256,1350,8192,16384)",
     },
     {
+        "-json",
+        kBooleanArgument,
+        "If this flag is set, speed will print the output of each benchmark in "
+        "JSON format as follows: \"{\"description\": "
+        "\"descriptionOfOperation\", \"numCalls\": 1234, "
+        "\"timeInMicroseconds\": 1234567, \"bytesPerCall\": 1234}\". When "
+        "there is no information about the bytes per call for an  operation, "
+        "the JSON field for bytesPerCall will be omitted.",
+    },
+    {
         "",
         kOptionalArgument,
         "",
@@ -997,6 +1048,10 @@
     selected = args_map["-filter"];
   }
 
+  if (args_map.count("-json") != 0) {
+    g_print_json = true;
+  }
+
   if (args_map.count("-timeout") != 0) {
     g_timeout_seconds = atoi(args_map["-timeout"].c_str());
   }
@@ -1036,6 +1091,9 @@
   // knowledge in them and construct a couple of the AD bytes internally.
   static const size_t kLegacyADLen = kTLSADLen - 2;
 
+  if (g_print_json) {
+    puts("[");
+  }
   if (!SpeedRSA(selected) ||
       !SpeedAEAD(EVP_aead_aes_128_gcm(), "AES-128-GCM", kTLSADLen, selected) ||
       !SpeedAEAD(EVP_aead_aes_256_gcm(), "AES-256-GCM", kTLSADLen, selected) ||
@@ -1077,6 +1135,9 @@
       !SpeedHRSS(selected)) {
     return false;
   }
+  if (g_print_json) {
+    puts("\n]");
+  }
 
   return true;
 }