#include "mediapipe/framework/profiler/reporter/reporter.h" #include #include #include #include #include #include #include #include "absl/container/btree_map.h" #include "absl/status/status.h" #include "absl/strings/str_format.h" #include "absl/strings/str_join.h" #include "fstream" #include "map" #include "mediapipe/framework/calculator_profile.pb.h" #include "mediapipe/framework/port/advanced_proto_inc.h" #include "mediapipe/framework/port/canonical_errors.h" #include "mediapipe/framework/port/re2.h" #include "mediapipe/framework/port/status.h" #include "mediapipe/framework/port/status_macros.h" namespace mediapipe { namespace reporter { const LazyRE2 kValidColumnRegex = {"^[a-zA-Z0-9_?*]+$"}; const LazyRE2 kReplace1WildcharRegex = {"\\?"}; const LazyRE2 kReplace0toNWildcharRegex = {"\\*"}; std::string ToStringF(double d) { return absl::StrFormat("%1.2f", d); } std::string ToString(double d) { return absl::StrFormat("%1.0f", d); } absl::btree_map> kColumns = { {"calculator", [](const CalculatorData& d) -> const std::string { return d.name; }}, {"counter", [](const CalculatorData& d) -> const std::string { return ToString(d.counter); }}, {"completed", [](const CalculatorData& d) -> const std::string { return ToString(d.completed); }}, {"dropped", [](const CalculatorData& d) -> const std::string { return ToString(d.dropped); }}, {"fps", [](const CalculatorData& d) -> const std::string { return ToStringF(d.fps); }}, {"frequency", [](const CalculatorData& d) -> const std::string { return ToStringF(d.frequency); }}, {"processing_rate", [](const CalculatorData& d) -> const std::string { return ToStringF(d.processing_rate); }}, {"thread_count", [](const CalculatorData& d) -> const std::string { return ToStringF(d.thread_count); }}, {"time_mean", [](const CalculatorData& d) -> const std::string { return ToStringF(d.time_stat.mean()); }}, {"time_stddev", [](const CalculatorData& d) -> const std::string { return ToStringF(d.time_stat.stddev()); }}, {"time_total", [](const CalculatorData& d) -> const std::string { return ToString(d.time_stat.total()); }}, {"time_percent", [](const CalculatorData& d) -> const std::string { return ToStringF(d.time_percent); }}, {"input_latency_mean", [](const CalculatorData& d) -> const std::string { return ToStringF(d.input_latency_stat.mean()); }}, {"input_latency_stddev", [](const CalculatorData& d) -> const std::string { return ToStringF(d.input_latency_stat.stddev()); }}, {"input_latency_total", [](const CalculatorData& d) -> const std::string { return ToString(d.input_latency_stat.total()); }}}; // Holds calculator traces that have an output trace with a provided stream ID // and Packet timestamp. typedef std::map, const mediapipe::GraphTrace::CalculatorTrace*> PacketKeyToCalcTrace; // Use this to locate the calculator trace with the start time for a given // node its thread_id and its packet timestamp. typedef std::map>, const mediapipe::GraphTrace::CalculatorTrace*> TimestampNodeIdToCalcTrace; // Maps node IDs to names. typedef std::map NameLookup; Reporter::Reporter() { MEDIAPIPE_CHECK_OK(set_columns({"*"})); } int64_t RecursePacketStartTime( const PacketKeyToCalcTrace& output_trace_lookup, const mediapipe::GraphProfile& profile, const mediapipe::GraphTrace::CalculatorTrace& trace, std::vector* visited_calculators) { int64_t child_start_time = trace.has_start_time() ? trace.start_time() : trace.finish_time(); const int32_t node_id = trace.node_id(); if (std::find(visited_calculators->begin(), visited_calculators->end(), node_id) != visited_calculators->end()) { return child_start_time; } visited_calculators->push_back(node_id); for (size_t index = 0; index < trace.input_trace_size(); ++index) { const auto& stream_trace = trace.input_trace(index); // Find the output corresponding to this input. const auto it = output_trace_lookup.find(std::make_pair( stream_trace.packet_timestamp(), stream_trace.stream_id())); if (it != output_trace_lookup.end()) { child_start_time = std::min(child_start_time, RecursePacketStartTime(output_trace_lookup, profile, *it->second, visited_calculators)); } } return child_start_time; } int64_t CalculateInputLatency( const PacketKeyToCalcTrace& output_trace_lookup, const mediapipe::GraphProfile& profile, const mediapipe::GraphTrace::CalculatorTrace& trace) { // Track visited calculators to detect loops. std::vector visited_calculators; // If a calculator has no start time, then there is no latency to measure. const auto result = !trace.has_start_time() ? 0L : trace.start_time() - RecursePacketStartTime(output_trace_lookup, profile, trace, &visited_calculators); return result; } void CacheNodeNameLookup(const mediapipe::GraphProfile& profile, NameLookup* result) { for (const auto& graph_trace : profile.graph_trace()) { int key = 0; for (const auto& calc_name : graph_trace.calculator_name()) { (*result)[key] = calc_name; ++key; } } } void CacheOutputTraceLookup(const mediapipe::GraphProfile& profile, PacketKeyToCalcTrace* output_trace_lookup, TimestampNodeIdToCalcTrace* start_time_lookup) { for (const auto& graph_trace : profile.graph_trace()) { for (const auto& calc_trace : graph_trace.calculator_trace()) { if (calc_trace.event_type() != mediapipe::GraphTrace_EventType_PROCESS) { continue; } if (calc_trace.has_start_time() && !calc_trace.has_finish_time()) { (*start_time_lookup)[std::make_pair( calc_trace.input_timestamp(), std::make_pair(calc_trace.node_id(), calc_trace.thread_id()))] = &calc_trace; } for (const auto& stream_trace : calc_trace.output_trace()) { (*output_trace_lookup)[std::make_pair(stream_trace.packet_timestamp(), stream_trace.stream_id())] = &calc_trace; } } } } void CompleteCalculatorData( const GraphData& graph_data, std::map* calculator_data) { for (auto& calc_entry : *calculator_data) { auto& calc_data = calc_entry.second; const auto& time_stat = calc_data.time_stat; const auto& latency_stat = calc_data.input_latency_stat; const auto time_to_process = time_stat.mean() + latency_stat.mean(); calc_data.fps = time_to_process == 0 ? 0 : 1.0E+6 / time_to_process; const auto duration = graph_data.max_time - graph_data.min_time; calc_data.frequency = calc_data.completed / (duration / 1.0E+6); calc_data.time_percent = 100 * time_stat.total() / graph_data.total_time; calc_data.dropped = calc_data.counter - calc_data.completed; calc_data.processing_rate = calc_data.time_stat.mean() == 0 ? 0 : 1.0 / calc_data.time_stat.mean() * 1.0E+6; calc_data.thread_count = calc_data.threads.size(); } } void Reporter::Accumulate(const mediapipe::GraphProfile& profile) { // Cache nodeID to its std::string name. NameLookup name_lookup; CacheNodeNameLookup(profile, &name_lookup); // Cache some lookups so that we can quickly find the matching output stream // for a given input stream, and so that we can find the start time of a // given timestamp of a node. PacketKeyToCalcTrace output_trace_lookup; TimestampNodeIdToCalcTrace start_event_lookup; CacheOutputTraceLookup(profile, &output_trace_lookup, &start_event_lookup); // Hold the domain of all times found in the trace file. auto& min_time = graph_data_.min_time; auto& max_time = graph_data_.max_time; auto& total_time = graph_data_.total_time; // The start and finish time of PROCESS events might be split between // events. If a start event has been seen, we'll record it so that we can // match it up later. std::map> start_times; for (const auto& graph_trace : profile.graph_trace()) { for (const auto& calc_trace : graph_trace.calculator_trace()) { if (calc_trace.event_type() != mediapipe::GraphTrace_EventType_PROCESS) { continue; } const auto& node_name = name_lookup[calc_trace.node_id()]; auto& calc_data = calculator_data_[node_name]; calc_data.name = node_name; calc_data.threads.insert(calc_trace.thread_id()); // If there is a start time, update the domain of the trace time, and // mark that we've seen a start time for this calculator. if (calc_trace.has_start_time()) { min_time = std::min(min_time, static_cast(calc_trace.start_time() + graph_trace.base_time())); ++calc_data.counter; } // If there is a finish time, update the domain and mark that an event // has been completed. if (calc_trace.has_finish_time()) { const auto finish_time = calc_trace.finish_time() + graph_trace.base_time(); max_time = std::max(max_time, static_cast(finish_time)); absl::optional start_time; if (calc_trace.has_start_time()) { start_time.emplace(calc_trace.start_time()); } else { const auto start_event_it = start_event_lookup.find(std::make_pair( calc_trace.input_timestamp(), std::make_pair(calc_trace.node_id(), calc_trace.thread_id()))); if (start_event_it != start_event_lookup.end()) { start_time.emplace(start_event_it->second->start_time()); } } // Edge case -- If a finish time came in without a start time, then // we know that an event started before the trace became available. // But since we don't know when that is, we can't record its duration // and won't count it. if (start_time) { ++calc_data.completed; // Add up the duration of the events that led up to this start // event. const auto input_latency = CalculateInputLatency(output_trace_lookup, profile, calc_trace); calc_data.input_latency_stat.Push(input_latency); const auto duration = finish_time - (start_time.value() + graph_trace.base_time()); calc_data.time_stat.Push(duration); total_time += duration; } } } } } absl::Status Reporter::set_columns(const std::vector& columns) { bool error = false; std::stringstream warnings; std::vector new_columns({"calculator"}); // Iterate through the desired columns and build a regex. for (const auto& column_matcher : columns) { if (!RE2::PartialMatch(column_matcher, *kValidColumnRegex)) { warnings << "Column '" << column_matcher << "' is invalid." << std::endl; error = true; continue; } std::string colString = column_matcher; RE2::GlobalReplace(&colString, *kReplace0toNWildcharRegex, ".*"); RE2::GlobalReplace(&colString, *kReplace1WildcharRegex, "."); // Iterator through our available columns and add them to our collection // of new columns if they do not already exist. bool matched = false; for (const auto& column : kColumns) { if (RE2::FullMatch(column.first, colString)) { matched = true; if (std::find(new_columns.begin(), new_columns.end(), column.first) == new_columns.end()) { new_columns.push_back(column.first); } } } if (!matched) { warnings << "Column '" << column_matcher << "' did not match any columns." << std::endl; error = true; } } // As long as there is still one column, honor the request, even if error. if (!new_columns.empty()) { columns_.swap(new_columns); } if (!error) { return absl::OkStatus(); } return absl::InvalidArgumentError(warnings.str()); } class ReportImpl : public Report { public: ReportImpl(const std::map& calculator_data, const GraphData& graph_data) : calculator_data_(calculator_data), graph_data_(graph_data) {} void Print(std::ostream& output) override; const std::vector& headers() override { return headers_impl; } const std::vector>& lines() override { return lines_impl; } const GraphData& graph_data() override { return graph_data_; } const std::map& calculator_data() override { return calculator_data_; } // Each header name in alphabetical order, except the first column, which is // always "calculator". std::vector headers_impl; // Values for each calculator, corresponding to the label in headers(). std::vector> lines_impl; // The longest std::string of any value in a given column (including the // header for that column). Used for formatting the output. std::vector char_counts_impl; bool compact_flag = false; const std::map& calculator_data_; const GraphData& graph_data_; }; void ReportImpl::Print(std::ostream& output) { // Print the results, but aside from the last column, add whitespace to // fill space up to char_counts[column] + 1. The strings in the output // are mutable to support padding, hence no const in the for loops. int column_number = 0; // Make a copy of the column std::string because we might be adding spaces. for (auto column : headers_impl) { int padding_needed = char_counts_impl[column_number] + 1 - column.length(); if (compact_flag) { padding_needed = 1; } column.append(padding_needed, ' '); output << column; column_number++; } output << std::endl; for (auto& row : lines_impl) { int column_number = 0; for (auto column : row) { int padding_needed = char_counts_impl[column_number] + 1 - column.length(); if (compact_flag) { padding_needed = 1; } column.append(padding_needed, ' '); output << column; column_number++; } output << std::endl; } } std::unique_ptr Reporter::Report() { CompleteCalculatorData(graph_data_, &calculator_data_); auto report = std::make_unique(calculator_data_, graph_data_); report->compact_flag = compact_flag_; // First row contains the column headers. auto& headers = report->headers_impl; auto& lines = report->lines_impl; auto& char_counts = report->char_counts_impl; headers = columns_; char_counts.resize(headers.size()); std::transform(headers.begin(), headers.end(), char_counts.begin(), [](const auto& header) { return header.size(); }); for (const auto& header : headers) { size_t line_num = 0; for (auto calc_it = calculator_data_.begin(); calc_it != calculator_data_.end(); ++calc_it) { const std::string value = kColumns[header](calc_it->second); if (calc_it->second.name.empty()) { continue; } while (line_num >= lines.size()) { lines.push_back({}); } auto& line = lines[line_num]; line.push_back(value); const size_t char_count_index = line.size() - 1; char_counts[char_count_index] = std::max(char_counts[char_count_index], value.length()); ++line_num; } } return report; } } // namespace reporter } // namespace mediapipe