in src/runtime/profiling.cc [201:392]
String ReportNode::AsTable(bool sort, bool aggregate) const {
// aggregate calls by op hash (or op name if hash is not set) + argument shapes
std::vector<Map<String, ObjectRef>> aggregated_calls;
if (aggregate) {
std::unordered_map<std::string, std::vector<size_t>> aggregates;
for (size_t i = 0; i < calls.size(); i++) {
auto& frame = calls[i];
auto it = frame.find("Hash");
std::string name = Downcast<String>(frame["Name"]);
if (it != frame.end()) {
name = Downcast<String>((*it).second);
}
if (frame.find("Argument Shapes") != frame.end()) {
name += Downcast<String>(frame["Argument Shapes"]);
}
if (aggregates.find(name) == aggregates.end()) {
aggregates[name] = {i};
} else {
aggregates[name].push_back(i);
}
}
for (const auto& p : aggregates) {
std::unordered_map<String, ObjectRef> aggregated;
for (auto i : p.second) {
for (auto& metric : calls[i]) {
auto it = aggregated.find(metric.first);
if (it == aggregated.end()) {
aggregated[metric.first] = metric.second;
} else {
if (metric.second.as<DurationNode>()) {
aggregated[metric.first] = ObjectRef(
make_object<DurationNode>(it->second.as<DurationNode>()->microseconds +
metric.second.as<DurationNode>()->microseconds));
} else if (metric.second.as<CountNode>()) {
aggregated[metric.first] = ObjectRef(make_object<CountNode>(
it->second.as<CountNode>()->value + metric.second.as<CountNode>()->value));
} else if (metric.second.as<PercentNode>()) {
aggregated[metric.first] =
ObjectRef(make_object<PercentNode>(it->second.as<PercentNode>()->percent +
metric.second.as<PercentNode>()->percent));
} else if (metric.second.as<StringObj>()) {
// Don't do anything. Assume the two strings are the same.
} else {
LOG(FATAL) << "Can only aggregate metrics with types DurationNode, CountNode, "
"PercentNode, and StringObj, but got "
<< metric.second->GetTypeKey();
}
}
}
}
aggregated_calls.push_back(aggregated);
}
} else {
for (auto call : calls) {
aggregated_calls.push_back(call);
}
}
// sort rows by duration
if (sort) {
std::sort(aggregated_calls.begin(), aggregated_calls.end(),
[&](const Map<String, ObjectRef>& a, const Map<String, ObjectRef>& b) {
return a.at("Duration (us)").as<DurationNode>()->microseconds >
b.at("Duration (us)").as<DurationNode>()->microseconds;
});
}
// compute columnwise sums
std::unordered_map<String, ObjectRef> col_sums;
for (auto call : aggregated_calls) {
for (auto p : call) {
if (p.second.as<CountNode>()) {
int64_t val = p.second.as<CountNode>()->value;
auto it = col_sums.find(p.first);
if (it != col_sums.end()) {
val += it->second.as<CountNode>()->value;
}
col_sums[p.first] = ObjectRef(make_object<CountNode>(val));
} else if (p.second.as<DurationNode>()) {
double val = p.second.as<DurationNode>()->microseconds;
auto it = col_sums.find(p.first);
if (it != col_sums.end()) {
val += it->second.as<DurationNode>()->microseconds;
}
col_sums[p.first] = ObjectRef(make_object<DurationNode>(val));
} else if (p.second.as<PercentNode>()) {
double val = p.second.as<PercentNode>()->percent;
auto it = col_sums.find(p.first);
if (it != col_sums.end()) {
val += it->second.as<PercentNode>()->percent;
}
col_sums[p.first] = ObjectRef(make_object<PercentNode>(val));
}
}
}
col_sums["Name"] = String("Sum");
aggregated_calls.push_back({{String("Name"), String("----------")}}); // separator
aggregated_calls.push_back(col_sums);
// per-device metrics
for (auto p : device_metrics) {
Map<String, ObjectRef> metrics = p.second;
metrics.Set("Name", String("Total"));
aggregated_calls.push_back(metrics);
}
// Table formatting
std::unordered_set<std::string> unique_headers;
for (auto row : aggregated_calls) {
for (auto p : row) {
unique_headers.insert(p.first);
}
}
std::vector<std::string> headers = {"Name", "Duration (us)",
"Percent"}; // always include these headers
for (auto header : unique_headers) {
if (header != "Name" && header != "Duration (us)" && header != "Percent") {
headers.push_back(header);
}
}
// Switch layout from row major to column major so we can easily compute column widths.
std::vector<std::vector<std::string>> cols;
for (auto header : headers) {
cols.push_back({header});
}
for (auto row : aggregated_calls) {
for (size_t i = 0; i < headers.size(); i++) {
auto it = row.find(headers[i]);
if (it == row.end()) {
// fill empty data with empty strings
cols[i].push_back("");
} else {
std::string val;
if ((*it).second.as<CountNode>()) {
std::stringstream s;
s.imbue(std::locale("")); // for 1000s seperators
s << std::fixed << (*it).second.as<CountNode>()->value;
val = s.str();
} else if ((*it).second.as<DurationNode>()) {
std::stringstream s;
s.imbue(std::locale("")); // for 1000s seperators
s << std::fixed << std::setprecision(2) << (*it).second.as<DurationNode>()->microseconds;
val = s.str();
} else if ((*it).second.as<PercentNode>()) {
std::stringstream s;
s << std::fixed << std::setprecision(2) << (*it).second.as<PercentNode>()->percent;
val = s.str();
} else if ((*it).second.as<StringObj>()) {
val = Downcast<String>((*it).second);
}
cols[i].push_back(val);
}
}
}
std::vector<size_t> widths;
for (auto v : cols) {
size_t width = 0;
for (auto x : v) {
width = std::max(width, x.size());
}
widths.push_back(width);
}
size_t length = 0;
for (auto v : cols) {
length = std::max(length, v.size());
}
std::stringstream s;
for (size_t row = 0; row < length; row++) {
for (size_t col = 0; col < cols.size(); col++) {
// left align first column
if (col == 0) {
s << std::left;
} else {
s << std::right;
}
if (row < cols[col].size()) {
s << std::setw(widths[col]) << cols[col][row] << " ";
} else {
s << std::setw(widths[col]) << ""
<< " ";
}
}
s << std::endl;
}
return s.str();
}