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_