Add simple timing report streamer

This makes examining timing reports in the live system & logs much
easier.

Change-Id: I661ffbd7476ef0c6e2d7a00ff654e6c40ee9f811
Signed-off-by: James Kuszmaul <james.kuszmaul@bluerivertech.com>
diff --git a/aos/events/BUILD b/aos/events/BUILD
index 1a8d644..278934b 100644
--- a/aos/events/BUILD
+++ b/aos/events/BUILD
@@ -461,3 +461,30 @@
         "@com_github_google_glog//:glog",
     ],
 )
+
+cc_library(
+    name = "timing_report_dump_lib",
+    srcs = ["timing_report_dump_lib.cc"],
+    hdrs = ["timing_report_dump_lib.h"],
+    deps = ["//aos/events:event_loop"],
+)
+
+cc_binary(
+    name = "timing_report_dump",
+    srcs = ["timing_report_dump.cc"],
+    deps = [
+        ":timing_report_dump_lib",
+        "//aos:init",
+        "//aos/events/logging:log_reader",
+    ],
+)
+
+cc_binary(
+    name = "aos_timing_report_streamer",
+    srcs = ["aos_timing_report_streamer.cc"],
+    deps = [
+        ":timing_report_dump_lib",
+        "//aos:init",
+        "//aos/events:shm_event_loop",
+    ],
+)
diff --git a/aos/events/aos_timing_report_streamer.cc b/aos/events/aos_timing_report_streamer.cc
new file mode 100644
index 0000000..ee5efde
--- /dev/null
+++ b/aos/events/aos_timing_report_streamer.cc
@@ -0,0 +1,38 @@
+#include "aos/configuration.h"
+#include "aos/events/shm_event_loop.h"
+#include "aos/events/timing_report_dump_lib.h"
+#include "aos/init.h"
+#include "aos/json_to_flatbuffer.h"
+#include "gflags/gflags.h"
+
+DEFINE_string(config, "/app/aos_config.json", "The path to the config to use.");
+DEFINE_string(application, "",
+              "Application filter to use. Empty for no filter.");
+DEFINE_bool(stream, true, "Stream out all the timing reports that we receive.");
+DEFINE_bool(accumulate, true,
+            "Display accumulation of all timing reports that we've seen when "
+            "the process is terminated.");
+
+namespace aos {
+int Main() {
+  aos::FlatbufferVector<aos::Configuration> config(
+      aos::configuration::ReadConfig(FLAGS_config));
+  ShmEventLoop event_loop(&config.message());
+  TimingReportDump dumper(&event_loop,
+                          FLAGS_accumulate
+                              ? TimingReportDump::AccumulateStatistics::kYes
+                              : TimingReportDump::AccumulateStatistics::kNo,
+                          FLAGS_stream ? TimingReportDump::StreamResults::kYes
+                                       : TimingReportDump::StreamResults::kNo);
+  if (!FLAGS_application.empty()) {
+    dumper.ApplicationFilter(FLAGS_application);
+  }
+  event_loop.Run();
+  return EXIT_SUCCESS;
+}
+}  // namespace aos
+
+int main(int argc, char *argv[]) {
+  aos::InitGoogle(&argc, &argv);
+  return aos::Main();
+}
diff --git a/aos/events/timing_report_dump.cc b/aos/events/timing_report_dump.cc
new file mode 100644
index 0000000..0e27b33
--- /dev/null
+++ b/aos/events/timing_report_dump.cc
@@ -0,0 +1,59 @@
+#include "aos/events/timing_report_dump_lib.h"
+
+#include "aos/configuration.h"
+#include "aos/init.h"
+#include "aos/json_to_flatbuffer.h"
+#include "aos/events/logging/log_reader.h"
+#include "gflags/gflags.h"
+#include "glog/logging.h"
+
+DEFINE_string(application, "",
+              "Application filter to use. Empty for no filter.");
+DEFINE_bool(stream, false, "Stream out all the timing reports in the log.");
+DEFINE_bool(accumulate, true,
+            "Display accumulation of all timing reports at end of log.");
+
+namespace aos {
+struct DumperState {
+  std::unique_ptr<EventLoop> event_loop;
+  std::unique_ptr<TimingReportDump> dumper;
+};
+int Main(int argc, char *argv[]) {
+  if (argc < 2) {
+    LOG(ERROR) << "Expected at least 1 logfile as an argument";
+    return 1;
+  }
+  aos::logger::LogReader reader(
+      aos::logger::SortParts(aos::logger::FindLogs(argc, argv)));
+  reader.Register();
+  {
+    std::vector<DumperState> dumpers;
+    for (const aos::Node *node : aos::configuration::GetNodes(
+             reader.event_loop_factory()->configuration())) {
+      std::unique_ptr<aos::EventLoop> event_loop =
+          reader.event_loop_factory()->MakeEventLoop("timing_reports", node);
+      event_loop->SkipTimingReport();
+      event_loop->SkipAosLog();
+      std::unique_ptr<TimingReportDump> dumper =
+          std::make_unique<TimingReportDump>(
+              event_loop.get(),
+              FLAGS_accumulate ? TimingReportDump::AccumulateStatistics::kYes
+                               : TimingReportDump::AccumulateStatistics::kNo,
+              FLAGS_stream ? TimingReportDump::StreamResults::kYes
+                           : TimingReportDump::StreamResults::kNo);
+      if (!FLAGS_application.empty()) {
+        dumper->ApplicationFilter(FLAGS_application);
+      }
+      dumpers.push_back({std::move(event_loop), std::move(dumper)});
+    }
+    reader.event_loop_factory()->Run();
+  }
+  reader.Deregister();
+  return EXIT_SUCCESS;
+}
+}  // namespace aos
+
+int main(int argc, char *argv[]) {
+  aos::InitGoogle(&argc, &argv);
+  return aos::Main(argc, argv);
+}
diff --git a/aos/events/timing_report_dump_lib.cc b/aos/events/timing_report_dump_lib.cc
new file mode 100644
index 0000000..bd44ff6
--- /dev/null
+++ b/aos/events/timing_report_dump_lib.cc
@@ -0,0 +1,377 @@
+#include "aos/events/timing_report_dump_lib.h"
+
+#include <iomanip>
+#include <iostream>
+
+namespace aos {
+TimingReportDump::TimingReportDump(aos::EventLoop *event_loop,
+                                   AccumulateStatistics accumulate,
+                                   StreamResults stream)
+    : event_loop_(event_loop), accumulate_(accumulate), stream_(stream) {
+  // We watch on the timing report channel, so we can't send timing reports.
+  event_loop_->SkipTimingReport();
+  event_loop_->MakeWatcher("/aos", [this](const timing::Report &report) {
+    HandleTimingReport(report);
+  });
+}
+
+namespace {
+std::ostream &operator<<(std::ostream &os, const timing::Statistic &stats) {
+  // Use default width for numbers (in case any previous things set a different
+  // width in the output stream).
+  const auto num_width = std::setw(0);
+  os << std::setfill(' ') << num_width << stats.average() << " [" << num_width
+     << stats.min() << ", " << num_width << stats.max() << "] std "
+     << num_width << stats.standard_deviation();
+  return os;
+}
+
+// Generates a table of the specified strings, such that the columns are all
+// spaced equally.
+// Will use prefix for indentation.
+template <size_t kColumns>
+void PrintTable(std::ostream *os, std::string_view prefix,
+                const std::vector<std::array<std::string, kColumns>> &table) {
+  std::array<size_t, kColumns> widths;
+  widths.fill(0);
+  for (const auto &row : table) {
+    for (size_t ii = 0; ii < kColumns; ++ii) {
+      widths.at(ii) = std::max(widths.at(ii), row.at(ii).size());
+    }
+  }
+  const std::string kSep = " | ";
+  for (const auto &row : table) {
+    *os << prefix << std::setfill(' ');
+    for (size_t ii = 0; ii < widths.size(); ++ii) {
+      *os << std::setw(widths.at(ii)) << row.at(ii);
+      if (ii + 1 != widths.size()) {
+        *os << " | ";
+      }
+    }
+    *os << std::endl;
+  }
+}
+
+// Spacing to use for indentation.
+const std::string kIndent = "  ";
+}  // namespace
+
+void TimingReportDump::PrintTimers(
+    std::ostream *os, std::string_view name,
+    const flatbuffers::Vector<flatbuffers::Offset<timing::Timer>> &timers) {
+  *os << kIndent << name << " (" << timers.size() << "):" << std::endl;
+  std::vector<std::array<std::string, 4>> rows;
+  rows.push_back({"Name", "Count", "Wakeup Latency", "Handler Time"});
+  for (const timing::Timer *timer : timers) {
+    std::stringstream wakeup_latency_stats;
+    CHECK(timer->has_wakeup_latency());
+    wakeup_latency_stats << *timer->wakeup_latency();
+    std::stringstream handler_time_stats;
+    CHECK(timer->has_handler_time());
+    handler_time_stats << *timer->handler_time();
+    rows.push_back({timer->has_name() ? timer->name()->str() : "",
+                    std::to_string(timer->count()), wakeup_latency_stats.str(),
+                    handler_time_stats.str()});
+  }
+  PrintTable(os, kIndent + kIndent, rows);
+}
+
+void TimingReportDump::PrintWatchers(
+    std::ostream *os,
+    const flatbuffers::Vector<flatbuffers::Offset<timing::Watcher>> &watchers) {
+  *os << kIndent << "Watchers (" << watchers.size() << "):" << std::endl;
+  std::vector<std::array<std::string, 5>> rows;
+  rows.push_back(
+      {"Channel Name", "Type", "Count", "Wakeup Latency", "Handler Time"});
+  for (const timing::Watcher *watcher : watchers) {
+    const Channel *channel = GetChannel(watcher->channel_index());
+    std::stringstream latency_stats;
+    std::stringstream handler_stats;
+    CHECK(watcher->has_wakeup_latency());
+    CHECK(watcher->has_handler_time());
+    latency_stats << *watcher->wakeup_latency();
+    handler_stats << *watcher->handler_time();
+    rows.push_back({channel->name()->str(), channel->type()->str(),
+                    std::to_string(watcher->count()), latency_stats.str(),
+                    handler_stats.str()});
+  }
+  PrintTable(os, kIndent + kIndent, rows);
+}
+
+void TimingReportDump::PrintSenders(
+    std::ostream *os,
+    const flatbuffers::Vector<flatbuffers::Offset<timing::Sender>> &senders) {
+  *os << kIndent << "Senders (" << senders.size() << "):" << std::endl;
+  std::vector<std::array<std::string, 5>> rows;
+  rows.push_back({"Channel Name", "Type", "Count", "Size", "Errors"});
+  for (const timing::Sender *sender : senders) {
+    const Channel *channel = GetChannel(sender->channel_index());
+    std::stringstream size_stats;
+    CHECK(sender->has_size());
+    size_stats << *sender->size();
+    std::stringstream errors;
+    CHECK(sender->has_error_counts());
+    for (size_t ii = 0; ii < sender->error_counts()->size(); ++ii) {
+      const size_t error_count =
+          CHECK_NOTNULL(sender->error_counts()->Get(ii))->count();
+      errors << error_count;
+      if (error_count > 0) {
+        // Put send errors onto stderr so that people just interested in
+        // outright errors can find them more readily.
+        LOG(INFO) << configuration::StrippedChannelToString(channel) << ": "
+                  << error_count << " "
+                  << timing::EnumNamesSendError()[static_cast<uint8_t>(
+                         sender->error_counts()->Get(ii)->error())]
+                  << " errors.";
+      }
+      if (ii + 1 != sender->error_counts()->size()) {
+        errors << ", ";
+      }
+    }
+    rows.push_back({channel->name()->str(), channel->type()->str(),
+                    std::to_string(sender->count()), size_stats.str(),
+                    errors.str()});
+  }
+  PrintTable(os, kIndent + kIndent, rows);
+}
+
+void TimingReportDump::PrintFetchers(
+    std::ostream *os,
+    const flatbuffers::Vector<flatbuffers::Offset<timing::Fetcher>> &fetchers) {
+  *os << kIndent << "Fetchers (" << fetchers.size() << "):" << std::endl;
+  std::vector<std::array<std::string, 4>> rows;
+  rows.push_back({"Channel Name", "Type", "Count", "Latency"});
+  for (const timing::Fetcher *fetcher : fetchers) {
+    const Channel *channel = GetChannel(fetcher->channel_index());
+    std::stringstream latency_stats;
+    CHECK(fetcher->has_latency());
+    latency_stats << *fetcher->latency();
+    rows.push_back({channel->name()->str(), channel->type()->str(),
+                    std::to_string(fetcher->count()), latency_stats.str()});
+  }
+  PrintTable(os, kIndent + kIndent, rows);
+}
+
+void TimingReportDump::HandleTimingReport(const timing::Report &report) {
+  CHECK(report.has_name());
+  if (name_filter_.has_value() &&
+      name_filter_.value() != report.name()->string_view()) {
+    return;
+  }
+  if (stream_ == StreamResults::kYes) {
+    PrintReport(report);
+  }
+
+  if (accumulate_ == AccumulateStatistics::kYes) {
+    AccumulateReport(report);
+  }
+}
+
+void TimingReportDump::PrintReport(const timing::Report &report) {
+  VLOG(1) << FlatbufferToJson(&report);
+  if (report.send_failures() != 0) {
+    LOG(INFO) << "Failed to send " << report.send_failures()
+              << " timing report(s) in " << report.name()->string_view();
+  }
+  std::cout << report.name()->string_view() << "[" << report.pid() << "] ("
+            << event_loop_->node()->name()->string_view() << ") ("
+            << event_loop_->context().monotonic_event_time << ","
+            << event_loop_->context().realtime_event_time << "):" << std::endl;
+  if (report.has_watchers() && report.watchers()->size() > 0) {
+    PrintWatchers(&std::cout, *report.watchers());
+  }
+  if (report.has_senders() && report.senders()->size() > 0) {
+    PrintSenders(&std::cout, *report.senders());
+  }
+  if (report.has_fetchers() && report.fetchers()->size() > 0) {
+    PrintFetchers(&std::cout, *report.fetchers());
+  }
+  if (report.has_timers() && report.timers()->size() > 0) {
+    PrintTimers(&std::cout, "Timers", *report.timers());
+  }
+  if (report.has_phased_loops() && report.phased_loops()->size() > 0) {
+    PrintTimers(&std::cout, "Phased Loops", *report.phased_loops());
+  }
+}
+
+TimingReportDump::~TimingReportDump() {
+  if (accumulate_ == AccumulateStatistics::kYes) {
+    if (accumulated_statistics_.size() > 0) {
+      std::cout << "\nAccumulated timing reports for node "
+                << event_loop_->node()->name()->string_view() << ":\n\n";
+    }
+    for (const auto &pair : accumulated_statistics_) {
+      flatbuffers::FlatBufferBuilder fbb;
+      fbb.Finish(timing::Report::Pack(fbb, &pair.second));
+      FlatbufferDetachedBuffer<timing::Report> report_buffer(fbb.Release());
+      const timing::Report &report = report_buffer.message();
+      if (name_filter_.has_value() &&
+          name_filter_.value() != report.name()->string_view()) {
+        return;
+      }
+      PrintReport(report);
+    }
+  }
+}
+
+namespace {
+// Helper function to combine the aggregate statistics from two entries. Most of
+// the complexity is in combining the standard deviations.
+void CombineStatistics(const size_t addition_count,
+                       const timing::StatisticT &addition,
+                       const size_t accumulator_count,
+                       timing::StatisticT *accumulator) {
+  if (addition_count == 0) {
+    return;
+  }
+  // Separate isnan handler to special-case the timing reports (see comment
+  // below).
+  if (accumulator_count == 0 || std::isnan(accumulator->average)) {
+    *accumulator = addition;
+    return;
+  }
+  const double old_average = accumulator->average;
+  accumulator->average = (accumulator->average * accumulator_count +
+                          addition.average * addition_count) /
+                         (accumulator_count + addition_count);
+  accumulator->max = std::max(accumulator->max, addition.max);
+  accumulator->min = std::min(accumulator->min, addition.min);
+  // Borrowing the process from
+  // https://math.stackexchange.com/questions/2971315/how-do-i-combine-standard-deviations-of-two-groups
+  // which gives:
+  //
+  // std_x^2 = sum((x_i - avg(x))^2) / (N - 1)
+  //
+  // If combining two distributions, x and y, with N and M elements
+  // respectively, into a combined distribution z, we will have:
+  //
+  // std_z^2 = (sum((x_i - avg(z))^2) + sum((y_i - avg(z)^2)) / (N + M - 1)
+  // (x_i - avg(z)) = ((x_i - avg(x)) + (avg(x) - avg(z))) =
+  //   (x_i - avg(x))^2 + 2 * (x_i - avg(x)) * (avg(x) - avg(z)) + (avg(x) -
+  //   avg(z))^2
+  // Note that when we do the sum, there is a sum(x - avg(x)) term that we just
+  // zero out.
+  // sum((x_i - avg(z))^2) = (N - 1) * std_x^2 + N * (avg(x) - avg(z))^2
+  // std_z^2 = ((N - 1) * std_x^2 + N * (avg(x) - avg(z))^2 + (M - 1) * std_y^2
+  //           + M * (avg(y) - avg(z)^2)) / (N + M - 1)
+  const double N = addition_count;
+  const double M = accumulator_count;
+  const double var_x = std::pow(addition.standard_deviation, 2);
+  const double var_y = std::pow(accumulator->standard_deviation, 2);
+  const double avg_x = addition.average;
+  const double avg_y = old_average;
+  const double new_variance =
+      ((N - 1) * var_x + N * std::pow(avg_x - accumulator->average, 2) +
+       (M - 1) * var_y + M * std::pow(avg_y - accumulator->average, 2)) /
+      (N + M - 1);
+  accumulator->standard_deviation = std::sqrt(new_variance);
+}
+
+void CombineTimers(
+    const std::vector<std::unique_ptr<timing::TimerT>> &new_timers,
+    std::vector<std::unique_ptr<timing::TimerT>> *aggregate_timers) {
+  for (auto &timer : new_timers) {
+    auto timer_iter =
+        std::find_if(aggregate_timers->begin(), aggregate_timers->end(),
+                     [&timer](const std::unique_ptr<timing::TimerT> &val) {
+                       // For many/most timers, the name will be empty, so we
+                       // just aggregate all of the empty ones together.
+                       return val->name == timer->name;
+                     });
+    if (timer_iter == aggregate_timers->end()) {
+      aggregate_timers->emplace_back(new timing::TimerT());
+      *aggregate_timers->back() = *timer;
+    } else {
+      CombineStatistics(timer->count, *timer->wakeup_latency,
+                        (*timer_iter)->count,
+                        (*timer_iter)->wakeup_latency.get());
+      // TODO(james): This isn't actually correct *for the timing report timer
+      // itself*. On the very first timing report that a process sends out, it
+      // will have count = 1, wakeup_latency = <something real>, handler_time =
+      // nan, because we are still handling the timer.
+      CombineStatistics(timer->count, *timer->handler_time,
+                        (*timer_iter)->count,
+                        (*timer_iter)->handler_time.get());
+      (*timer_iter)->count += timer->count;
+    }
+  }
+}
+}
+
+void TimingReportDump::AccumulateReport(const timing::Report &raw_report) {
+  CHECK(raw_report.has_pid());
+  CHECK(raw_report.has_name());
+  const std::pair<pid_t, std::string> map_key(raw_report.pid(),
+                                              raw_report.name()->str());
+  if (accumulated_statistics_.count(map_key) == 0) {
+    accumulated_statistics_[map_key].name = raw_report.name()->str();
+    accumulated_statistics_[map_key].pid = raw_report.pid();
+  }
+
+  timing::ReportT report;
+  raw_report.UnPackTo(&report);
+  timing::ReportT *summary = &accumulated_statistics_[map_key];
+  for (auto &watcher : report.watchers) {
+    auto watcher_iter =
+        std::find_if(summary->watchers.begin(), summary->watchers.end(),
+                     [&watcher](const std::unique_ptr<timing::WatcherT> &val) {
+                       return val->channel_index == watcher->channel_index;
+                     });
+    if (watcher_iter == summary->watchers.end()) {
+      summary->watchers.push_back(std::move(watcher));
+    } else {
+      CombineStatistics(watcher->count, *watcher->wakeup_latency,
+                        (*watcher_iter)->count,
+                        (*watcher_iter)->wakeup_latency.get());
+      CombineStatistics(watcher->count, *watcher->handler_time,
+                        (*watcher_iter)->count,
+                        (*watcher_iter)->handler_time.get());
+      (*watcher_iter)->count += watcher->count;
+    }
+  }
+  for (auto &sender : report.senders) {
+    auto sender_iter =
+        std::find_if(summary->senders.begin(), summary->senders.end(),
+                     [&sender](const std::unique_ptr<timing::SenderT> &val) {
+                       return val->channel_index == sender->channel_index;
+                     });
+    if (sender_iter == summary->senders.end()) {
+      summary->senders.push_back(std::move(sender));
+    } else {
+      CombineStatistics(sender->count, *sender->size, (*sender_iter)->count,
+                        (*sender_iter)->size.get());
+      (*sender_iter)->count += sender->count;
+      CHECK_EQ((*sender_iter)->error_counts.size(),
+               sender->error_counts.size());
+      for (size_t ii = 0; ii < sender->error_counts.size(); ++ii) {
+        (*sender_iter)->error_counts[ii]->count +=
+            sender->error_counts[ii]->count;
+      }
+    }
+  }
+  for (auto &fetcher : report.fetchers) {
+    auto fetcher_iter =
+        std::find_if(summary->fetchers.begin(), summary->fetchers.end(),
+                     [&fetcher](const std::unique_ptr<timing::FetcherT> &val) {
+                       return val->channel_index == fetcher->channel_index;
+                     });
+    if (fetcher_iter == summary->fetchers.end()) {
+      summary->fetchers.push_back(std::move(fetcher));
+    } else {
+      CombineStatistics(fetcher->count, *fetcher->latency,
+                        (*fetcher_iter)->count, (*fetcher_iter)->latency.get());
+      (*fetcher_iter)->count += fetcher->count;
+    }
+  }
+  CombineTimers(report.timers, &summary->timers);
+  CombineTimers(report.phased_loops, &summary->phased_loops);
+  summary->send_failures += report.send_failures;
+}
+
+const Channel *TimingReportDump::GetChannel(int index) {
+  CHECK_LT(0, index);
+  CHECK_GT(event_loop_->configuration()->channels()->size(), static_cast<size_t>(index));
+  return event_loop_->configuration()->channels()->Get(index);
+}
+
+}  // namespace aos
diff --git a/aos/events/timing_report_dump_lib.h b/aos/events/timing_report_dump_lib.h
new file mode 100644
index 0000000..a33bc3d
--- /dev/null
+++ b/aos/events/timing_report_dump_lib.h
@@ -0,0 +1,68 @@
+#ifndef AOS_EVENTS_TIMING_REPORT_DUMP_LIB_H_
+#define AOS_EVENTS_TIMING_REPORT_DUMP_LIB_H_
+#include <string>
+#include <map>
+
+#include "aos/configuration.h"
+#include "aos/events/event_loop.h"
+#include "aos/events/event_loop_generated.h"
+#include "aos/json_to_flatbuffer.h"
+#include "gflags/gflags.h"
+#include "glog/logging.h"
+
+
+namespace aos {
+// A class to handle printing timing report statistics in a useful format on the
+// command line. Main features:
+// * Correlates channel indices to channel names/types.
+// * Formats timing reports in a more readable manner than pure JSON.
+// * Can filter on application name.
+// * Can accumulate all the timing reports for an application over time and
+//   produce summary statistics.
+class TimingReportDump {
+ public:
+  enum class AccumulateStatistics { kYes, kNo };
+  enum class StreamResults { kYes, kNo };
+  TimingReportDump(aos::EventLoop *event_loop, AccumulateStatistics accumulate,
+                   StreamResults stream);
+  // The destructor handles the final printout of accumulated statistics (if
+  // requested), which for log reading should happen after the log has been
+  // fully replayed and for live systems will happen the user Ctrl-C's.
+  ~TimingReportDump();
+
+  // Filter to use for application name. Currently requires that the provided
+  // name exactly match the name of the application in question.
+  void ApplicationFilter(std::string_view name) {
+    name_filter_ = name;
+  }
+
+ private:
+  void HandleTimingReport(const timing::Report &report);
+  const Channel *GetChannel(int index);
+  void PrintTimers(
+      std::ostream *os, std::string_view name,
+      const flatbuffers::Vector<flatbuffers::Offset<timing::Timer>> &timers);
+  void PrintWatchers(
+      std::ostream *os,
+      const flatbuffers::Vector<flatbuffers::Offset<timing::Watcher>>
+          &watchers);
+  void PrintSenders(
+      std::ostream *os,
+      const flatbuffers::Vector<flatbuffers::Offset<timing::Sender>> &senders);
+  void PrintFetchers(
+      std::ostream *os,
+      const flatbuffers::Vector<flatbuffers::Offset<timing::Fetcher>>
+          &fetchers);
+  void PrintReport(const timing::Report &report);
+  void AccumulateReport(const timing::Report &report);
+
+  aos::EventLoop *event_loop_;
+  AccumulateStatistics accumulate_;
+  StreamResults stream_;
+  std::optional<std::string> name_filter_;
+  // Key is pair of <process id, application name>, since neither is a unique
+  // identifier across time.
+  std::map<std::pair<pid_t, std::string>, timing::ReportT> accumulated_statistics_;
+};
+}  // namespace aos
+#endif  // AOS_EVENTS_TIMING_REPORT_DUMP_LIB_H_