[debugger] Add a perf testing target.

Adds a framework and test target for adding zxdb perf tests.

This framework is very simple. It can run on both the host and the
target, unlike the existing //zircon/syste,/ulib/perftest which is quite
tied to the Zircon system. The test primitives are based on those in
Chrome.

Adds an empty module load perf test. This is to validate that the output
has the correct format but currently tests nothing.

Change-Id: Ib1517314d4f43f68d4ffc06575f048d524dc9b88
diff --git a/src/developer/debug/zxdb/BUILD.gn b/src/developer/debug/zxdb/BUILD.gn
index c2b2db4..a6248e7 100644
--- a/src/developer/debug/zxdb/BUILD.gn
+++ b/src/developer/debug/zxdb/BUILD.gn
@@ -51,6 +51,19 @@
       "//third_party/googletest:gtest_main",
     ]
   }
+
+  test("zxdb_perftests") {
+    sources = [
+      "zxdb_perftests.cc",
+    ]
+
+    deps = [
+      "//src/developer/debug/zxdb/common:perf_test",
+      "//src/developer/debug/zxdb/symbols:perf_tests",
+      "//third_party/googletest:gtest",
+      "//zircon/public/lib/cmdline",
+    ]
+  }
 }
 
 install_host_tools("zxdb_host") {
diff --git a/src/developer/debug/zxdb/common/BUILD.gn b/src/developer/debug/zxdb/common/BUILD.gn
index 95def7a1..a74b30f 100644
--- a/src/developer/debug/zxdb/common/BUILD.gn
+++ b/src/developer/debug/zxdb/common/BUILD.gn
@@ -51,3 +51,22 @@
     "//third_party/googletest:gtest",
   ]
 }
+
+# Simple support library for writing perf tests. This is a much simpler variant
+# of //zircon/system/ulib/perftest that will run on the host.
+source_set("perf_test") {
+  testonly = true
+
+  sources = [
+    "perf_test.cc",
+    "perf_test.h",
+  ]
+
+  public_deps = [
+    "//third_party/googletest:gtest",
+  ]
+
+  deps = [
+    "//src/lib/fxl",
+  ]
+}
diff --git a/src/developer/debug/zxdb/common/perf_test.cc b/src/developer/debug/zxdb/common/perf_test.cc
new file mode 100644
index 0000000..75f8992
--- /dev/null
+++ b/src/developer/debug/zxdb/common/perf_test.cc
@@ -0,0 +1,90 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/developer/debug/zxdb/common/perf_test.h"
+
+#include <stdio.h>
+
+#include "src/lib/fxl/logging.h"
+
+namespace zxdb {
+
+namespace {
+
+FILE* perf_log_file = nullptr;
+bool written_perf_line = false;
+
+}  // namespace
+
+bool InitPerfLog(const std::string& log_file) {
+  if (perf_log_file) {
+    // Trying to initialize twice.
+    FXL_NOTREACHED();
+    return false;
+  }
+
+  if (!(perf_log_file = fopen(log_file.c_str(), "w")))
+    return false;
+
+  fprintf(perf_log_file, "{\n");
+  return true;
+}
+
+void FinalizePerfLog() {
+  if (!perf_log_file) {
+    // Trying to cleanup without initializing.
+    FXL_NOTREACHED();
+    return;
+  }
+  fprintf(perf_log_file, "\n}\n");
+  fclose(perf_log_file);
+  perf_log_file = nullptr;
+}
+
+void LogPerfResult(const char* test_suite_name, const char* test_name,
+                   double value, const char* units) {
+  if (!perf_log_file) {
+    FXL_NOTREACHED();
+    return;
+  }
+
+  // Format: //zircon/system/ulib/perftest/performance-results-schema.json
+  // Example line:
+  //  {"label":"Vmo/CloneWrite/10000kbytes.close",
+  //   "test_suite":"fuchsia.zircon_benchmarks",
+  //   "unit":"nanoseconds",
+  //   "values":[2346.961749]}
+
+  // Add trailing comma for previous item when necessary.
+  if (written_perf_line)
+    fprintf(perf_log_file, ",\n");
+  written_perf_line = true;
+
+  fprintf(perf_log_file,
+          R"({"label":"%s", "test_suite":"%s", "unit":"%s", "values":[%g]})",
+          test_name, test_suite_name, units, value);
+  fflush(stdout);
+}
+
+PerfTimeLogger::PerfTimeLogger(const char* test_suite_name,
+                               const char* test_name)
+    : test_suite_name_(test_suite_name), test_name_(test_name) {
+  timer_.Start();
+}
+
+PerfTimeLogger::~PerfTimeLogger() {
+  if (!logged_)
+    Done();
+}
+
+void PerfTimeLogger::Done() {
+  // Use a floating-point millisecond value because it is more
+  // intuitive than microseconds and we want more precision than
+  // integer milliseconds
+  LogPerfResult(test_suite_name_.c_str(), test_name_.c_str(),
+                timer_.Elapsed().ToMillisecondsF(), "ms");
+  logged_ = true;
+}
+
+}  // namespace zxdb
diff --git a/src/developer/debug/zxdb/common/perf_test.h b/src/developer/debug/zxdb/common/perf_test.h
new file mode 100644
index 0000000..d9cba35
--- /dev/null
+++ b/src/developer/debug/zxdb/common/perf_test.h
@@ -0,0 +1,49 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SRC_DEVELOPER_DEBUG_ZXDB_COMMON_PERF_TEST_H_
+#define SRC_DEVELOPER_DEBUG_ZXDB_COMMON_PERF_TEST_H_
+
+#include <string>
+
+#include "src/lib/fxl/macros.h"
+#include "src/lib/fxl/time/stopwatch.h"
+
+namespace zxdb {
+
+// Initializes and finalizes the perf log. These functions should be called at
+// the beginning and end (respectively) of running all the performance tests.
+// The init function returns true on success.
+bool InitPerfLog(const std::string& log_path);
+void FinalizePerfLog();
+
+// Writes to the perf result log the given 'value' resulting from the named
+// 'test'. The units are to aid in reading the log by people.
+void LogPerfResult(const char* test_suite_name, const char* test_name,
+                   double value, const char* units);
+
+// Automates calling LogPerfResult for the common case where you want
+// to measure the time that something took. Call Done() when the test
+// is complete if you do extra work after the test or there are stack
+// objects with potentially expensive constructors. Otherwise, this
+// class with automatically log on destruction.
+class PerfTimeLogger {
+ public:
+  PerfTimeLogger(const char* test_suite_name, const char* test_name);
+  ~PerfTimeLogger();
+
+  void Done();
+
+ private:
+  bool logged_ = false;
+  std::string test_suite_name_;
+  std::string test_name_;
+  fxl::Stopwatch timer_;
+
+  FXL_DISALLOW_COPY_AND_ASSIGN(PerfTimeLogger);
+};
+
+}  // namespace zxdb
+
+#endif  // SRC_DEVELOPER_DEBUG_ZXDB_COMMON_PERF_TEST_H_
diff --git a/src/developer/debug/zxdb/symbols/BUILD.gn b/src/developer/debug/zxdb/symbols/BUILD.gn
index eb8f1d6..54166a1 100644
--- a/src/developer/debug/zxdb/symbols/BUILD.gn
+++ b/src/developer/debug/zxdb/symbols/BUILD.gn
@@ -238,3 +238,16 @@
     test_runtime_deps = get_target_outputs(":copy_test_so")
   }
 }
+
+source_set("perf_tests") {
+  testonly = true
+
+  sources = [
+    "module_symbol_index_perftest.cc",
+  ]
+
+  deps = [
+    ":symbols",
+    "//src/developer/debug/zxdb/common:perf_test",
+  ]
+}
diff --git a/src/developer/debug/zxdb/symbols/module_symbol_index_perftest.cc b/src/developer/debug/zxdb/symbols/module_symbol_index_perftest.cc
new file mode 100644
index 0000000..67d1fa1
--- /dev/null
+++ b/src/developer/debug/zxdb/symbols/module_symbol_index_perftest.cc
@@ -0,0 +1,17 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "src/developer/debug/zxdb/symbols/module_symbol_index.h"
+
+#include "gtest/gtest.h"
+#include "src/developer/debug/zxdb/common/perf_test.h"
+
+namespace zxdb {
+
+TEST(ModuleLoad, Perf) {
+  PerfTimeLogger logger("zxdb", "ModuleLoad");
+  // TODO(brettw) write this.
+}
+
+}  // namespace zxdb
diff --git a/src/developer/debug/zxdb/zxdb_perftests.cc b/src/developer/debug/zxdb/zxdb_perftests.cc
new file mode 100644
index 0000000..ae58575
--- /dev/null
+++ b/src/developer/debug/zxdb/zxdb_perftests.cc
@@ -0,0 +1,61 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <cmdline/args_parser.h>
+
+#include "gtest/gtest.h"
+#include "src/developer/debug/zxdb/common/perf_test.h"
+
+namespace {
+
+struct CommandLineOptions {
+  std::string out_file;
+};
+
+}  // namespace
+
+int main(int argc, char **argv) {
+  testing::InitGoogleTest(&argc, argv);
+
+  // Set up for command line parsing.
+  cmdline::ArgsParser<CommandLineOptions> parser;
+  parser.AddSwitch("out", 0,
+                   "--out\n  [required] JSON file to write perf stats to.",
+                   &CommandLineOptions::out_file);
+
+  bool requested_help = false;
+  parser.AddGeneralSwitch("help", 0, "--help\n   Print help",
+                          [&requested_help]() { requested_help = true; });
+
+  // Parse the command line.
+  CommandLineOptions options;
+  std::vector<std::string> params;
+  if (auto status = parser.Parse(argc, const_cast<const char **>(argv),
+                                 &options, &params);
+      status.has_error()) {
+    fprintf(stderr, "Error: %s\n", status.error_message().c_str());
+    return 1;
+  }
+  if (requested_help) {
+    // Gtest will have printed its own help in response to --help, and this
+    // gets appended.
+    fprintf(stderr, "\n\nPerf test options:\n\n%s\n", parser.GetHelp().c_str());
+    return 0;
+  }
+
+  // Initialize the perf test system.
+  if (options.out_file.empty()) {
+    fprintf(stderr, "Parameter --out=<output_file> is required.\n");
+    return 1;
+  }
+  if (!zxdb::InitPerfLog(options.out_file)) {
+    fprintf(stderr, "Failed to initialize perf log.\n");
+    return 1;
+  }
+
+  int result = RUN_ALL_TESTS();
+
+  zxdb::FinalizePerfLog();
+  return result;
+}