xref: /freebsd/contrib/llvm-project/llvm/tools/llvm-xray/xray-account.cpp (revision 094517119c62c23369d545a7475ae982d86330a3)
1 //===- xray-account.h - XRay Function Call Accounting ---------------------===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 //
9 // This file implements basic function call accounting from an XRay trace.
10 //
11 //===----------------------------------------------------------------------===//
12 
13 #include <algorithm>
14 #include <cassert>
15 #include <numeric>
16 #include <system_error>
17 #include <utility>
18 
19 #include "xray-account.h"
20 #include "xray-registry.h"
21 #include "llvm/Support/ErrorHandling.h"
22 #include "llvm/Support/FormatVariadic.h"
23 #include "llvm/XRay/InstrumentationMap.h"
24 #include "llvm/XRay/Trace.h"
25 
26 using namespace llvm;
27 using namespace llvm::xray;
28 
29 static cl::SubCommand Account("account", "Function call accounting");
30 static cl::opt<std::string> AccountInput(cl::Positional,
31                                          cl::desc("<xray log file>"),
32                                          cl::Required, cl::sub(Account));
33 static cl::opt<bool>
34     AccountKeepGoing("keep-going", cl::desc("Keep going on errors encountered"),
35                      cl::sub(Account), cl::init(false));
36 static cl::alias AccountKeepGoing2("k", cl::aliasopt(AccountKeepGoing),
37                                    cl::desc("Alias for -keep_going"));
38 static cl::opt<bool> AccountRecursiveCallsOnly(
39     "recursive-calls-only", cl::desc("Only count the calls that are recursive"),
40     cl::sub(Account), cl::init(false));
41 static cl::opt<bool> AccountDeduceSiblingCalls(
42     "deduce-sibling-calls",
43     cl::desc("Deduce sibling calls when unrolling function call stacks"),
44     cl::sub(Account), cl::init(false));
45 static cl::alias
46     AccountDeduceSiblingCalls2("d", cl::aliasopt(AccountDeduceSiblingCalls),
47                                cl::desc("Alias for -deduce_sibling_calls"));
48 static cl::opt<std::string>
49     AccountOutput("output", cl::value_desc("output file"), cl::init("-"),
50                   cl::desc("output file; use '-' for stdout"),
51                   cl::sub(Account));
52 static cl::alias AccountOutput2("o", cl::aliasopt(AccountOutput),
53                                 cl::desc("Alias for -output"));
54 enum class AccountOutputFormats { TEXT, CSV };
55 static cl::opt<AccountOutputFormats>
56     AccountOutputFormat("format", cl::desc("output format"),
57                         cl::values(clEnumValN(AccountOutputFormats::TEXT,
58                                               "text", "report stats in text"),
59                                    clEnumValN(AccountOutputFormats::CSV, "csv",
60                                               "report stats in csv")),
61                         cl::sub(Account));
62 static cl::alias AccountOutputFormat2("f", cl::desc("Alias of -format"),
63                                       cl::aliasopt(AccountOutputFormat));
64 
65 enum class SortField {
66   FUNCID,
67   COUNT,
68   MIN,
69   MED,
70   PCT90,
71   PCT99,
72   MAX,
73   SUM,
74   FUNC,
75 };
76 
77 static cl::opt<SortField> AccountSortOutput(
78     "sort", cl::desc("sort output by this field"), cl::value_desc("field"),
79     cl::sub(Account), cl::init(SortField::FUNCID),
80     cl::values(clEnumValN(SortField::FUNCID, "funcid", "function id"),
81                clEnumValN(SortField::COUNT, "count", "funciton call counts"),
82                clEnumValN(SortField::MIN, "min", "minimum function durations"),
83                clEnumValN(SortField::MED, "med", "median function durations"),
84                clEnumValN(SortField::PCT90, "90p", "90th percentile durations"),
85                clEnumValN(SortField::PCT99, "99p", "99th percentile durations"),
86                clEnumValN(SortField::MAX, "max", "maximum function durations"),
87                clEnumValN(SortField::SUM, "sum", "sum of call durations"),
88                clEnumValN(SortField::FUNC, "func", "function names")));
89 static cl::alias AccountSortOutput2("s", cl::aliasopt(AccountSortOutput),
90                                     cl::desc("Alias for -sort"));
91 
92 enum class SortDirection {
93   ASCENDING,
94   DESCENDING,
95 };
96 static cl::opt<SortDirection> AccountSortOrder(
97     "sortorder", cl::desc("sort ordering"), cl::init(SortDirection::ASCENDING),
98     cl::values(clEnumValN(SortDirection::ASCENDING, "asc", "ascending"),
99                clEnumValN(SortDirection::DESCENDING, "dsc", "descending")),
100     cl::sub(Account));
101 static cl::alias AccountSortOrder2("r", cl::aliasopt(AccountSortOrder),
102                                    cl::desc("Alias for -sortorder"));
103 
104 static cl::opt<int> AccountTop("top", cl::desc("only show the top N results"),
105                                cl::value_desc("N"), cl::sub(Account),
106                                cl::init(-1));
107 static cl::alias AccountTop2("p", cl::desc("Alias for -top"),
108                              cl::aliasopt(AccountTop));
109 
110 static cl::opt<std::string>
111     AccountInstrMap("instr_map",
112                     cl::desc("binary with the instrumentation map, or "
113                              "a separate instrumentation map"),
114                     cl::value_desc("binary with xray_instr_map"),
115                     cl::sub(Account), cl::init(""));
116 static cl::alias AccountInstrMap2("m", cl::aliasopt(AccountInstrMap),
117                                   cl::desc("Alias for -instr_map"));
118 
119 namespace {
120 
121 template <class T, class U> void setMinMax(std::pair<T, T> &MM, U &&V) {
122   if (MM.first == 0 || MM.second == 0)
123     MM = std::make_pair(std::forward<U>(V), std::forward<U>(V));
124   else
125     MM = std::make_pair(std::min(MM.first, V), std::max(MM.second, V));
126 }
127 
128 template <class T> T diff(T L, T R) { return std::max(L, R) - std::min(L, R); }
129 
130 } // namespace
131 
132 using RecursionStatus = LatencyAccountant::FunctionStack::RecursionStatus;
133 RecursionStatus &RecursionStatus::operator++() {
134   auto Depth = Bitfield::get<RecursionStatus::Depth>(Storage);
135   assert(Depth >= 0 && Depth < std::numeric_limits<decltype(Depth)>::max());
136   ++Depth;
137   Bitfield::set<RecursionStatus::Depth>(Storage, Depth); // ++Storage
138   // Did this function just (maybe indirectly) call itself the first time?
139   if (!isRecursive() && Depth == 2) // Storage == 2  /  Storage s> 1
140     Bitfield::set<RecursionStatus::IsRecursive>(Storage,
141                                                 true); // Storage |= INT_MIN
142   return *this;
143 }
144 RecursionStatus &RecursionStatus::operator--() {
145   auto Depth = Bitfield::get<RecursionStatus::Depth>(Storage);
146   assert(Depth > 0);
147   --Depth;
148   Bitfield::set<RecursionStatus::Depth>(Storage, Depth); // --Storage
149   // Did we leave a function that previouly (maybe indirectly) called itself?
150   if (isRecursive() && Depth == 0) // Storage == INT_MIN
151     Bitfield::set<RecursionStatus::IsRecursive>(Storage, false); // Storage = 0
152   return *this;
153 }
154 bool RecursionStatus::isRecursive() const {
155   return Bitfield::get<RecursionStatus::IsRecursive>(Storage); // Storage s< 0
156 }
157 
158 bool LatencyAccountant::accountRecord(const XRayRecord &Record) {
159   setMinMax(PerThreadMinMaxTSC[Record.TId], Record.TSC);
160   setMinMax(PerCPUMinMaxTSC[Record.CPU], Record.TSC);
161 
162   if (CurrentMaxTSC == 0)
163     CurrentMaxTSC = Record.TSC;
164 
165   if (Record.TSC < CurrentMaxTSC)
166     return false;
167 
168   auto &ThreadStack = PerThreadFunctionStack[Record.TId];
169   if (RecursiveCallsOnly && !ThreadStack.RecursionDepth)
170     ThreadStack.RecursionDepth.emplace();
171   switch (Record.Type) {
172   case RecordTypes::CUSTOM_EVENT:
173   case RecordTypes::TYPED_EVENT:
174     // TODO: Support custom and typed event accounting in the future.
175     return true;
176   case RecordTypes::ENTER:
177   case RecordTypes::ENTER_ARG: {
178     ThreadStack.Stack.emplace_back(Record.FuncId, Record.TSC);
179     if (ThreadStack.RecursionDepth)
180       ++(*ThreadStack.RecursionDepth)[Record.FuncId];
181     break;
182   }
183   case RecordTypes::EXIT:
184   case RecordTypes::TAIL_EXIT: {
185     if (ThreadStack.Stack.empty())
186       return false;
187 
188     if (ThreadStack.Stack.back().first == Record.FuncId) {
189       const auto &Top = ThreadStack.Stack.back();
190       if (!ThreadStack.RecursionDepth ||
191           (*ThreadStack.RecursionDepth)[Top.first].isRecursive())
192         recordLatency(Top.first, diff(Top.second, Record.TSC));
193       if (ThreadStack.RecursionDepth)
194         --(*ThreadStack.RecursionDepth)[Top.first];
195       ThreadStack.Stack.pop_back();
196       break;
197     }
198 
199     if (!DeduceSiblingCalls)
200       return false;
201 
202     // Look for the parent up the stack.
203     auto Parent =
204         std::find_if(ThreadStack.Stack.rbegin(), ThreadStack.Stack.rend(),
205                      [&](const std::pair<const int32_t, uint64_t> &E) {
206                        return E.first == Record.FuncId;
207                      });
208     if (Parent == ThreadStack.Stack.rend())
209       return false;
210 
211     // Account time for this apparently sibling call exit up the stack.
212     // Considering the following case:
213     //
214     //   f()
215     //    g()
216     //      h()
217     //
218     // We might only ever see the following entries:
219     //
220     //   -> f()
221     //   -> g()
222     //   -> h()
223     //   <- h()
224     //   <- f()
225     //
226     // Now we don't see the exit to g() because some older version of the XRay
227     // runtime wasn't instrumenting tail exits. If we don't deduce tail calls,
228     // we may potentially never account time for g() -- and this code would have
229     // already bailed out, because `<- f()` doesn't match the current "top" of
230     // stack where we're waiting for the exit to `g()` instead. This is not
231     // ideal and brittle -- so instead we provide a potentially inaccurate
232     // accounting of g() instead, computing it from the exit of f().
233     //
234     // While it might be better that we account the time between `-> g()` and
235     // `-> h()` as the proper accounting of time for g() here, this introduces
236     // complexity to do correctly (need to backtrack, etc.).
237     //
238     // FIXME: Potentially implement the more complex deduction algorithm?
239     auto R = make_range(std::next(Parent).base(), ThreadStack.Stack.end());
240     for (auto &E : R) {
241       if (!ThreadStack.RecursionDepth ||
242           (*ThreadStack.RecursionDepth)[E.first].isRecursive())
243         recordLatency(E.first, diff(E.second, Record.TSC));
244     }
245     for (auto &Top : reverse(R)) {
246       if (ThreadStack.RecursionDepth)
247         --(*ThreadStack.RecursionDepth)[Top.first];
248       ThreadStack.Stack.pop_back();
249     }
250     break;
251   }
252   }
253 
254   return true;
255 }
256 
257 namespace {
258 
259 // We consolidate the data into a struct which we can output in various forms.
260 struct ResultRow {
261   uint64_t Count;
262   double Min;
263   double Median;
264   double Pct90;
265   double Pct99;
266   double Max;
267   double Sum;
268   std::string DebugInfo;
269   std::string Function;
270 };
271 
272 ResultRow getStats(MutableArrayRef<uint64_t> Timings) {
273   assert(!Timings.empty());
274   ResultRow R;
275   R.Sum = std::accumulate(Timings.begin(), Timings.end(), 0.0);
276   auto MinMax = std::minmax_element(Timings.begin(), Timings.end());
277   R.Min = *MinMax.first;
278   R.Max = *MinMax.second;
279   R.Count = Timings.size();
280 
281   auto MedianOff = Timings.size() / 2;
282   std::nth_element(Timings.begin(), Timings.begin() + MedianOff, Timings.end());
283   R.Median = Timings[MedianOff];
284 
285   auto Pct90Off = std::floor(Timings.size() * 0.9);
286   std::nth_element(Timings.begin(), Timings.begin() + (uint64_t)Pct90Off,
287                    Timings.end());
288   R.Pct90 = Timings[Pct90Off];
289 
290   auto Pct99Off = std::floor(Timings.size() * 0.99);
291   std::nth_element(Timings.begin(), Timings.begin() + (uint64_t)Pct99Off,
292                    Timings.end());
293   R.Pct99 = Timings[Pct99Off];
294   return R;
295 }
296 
297 } // namespace
298 
299 using TupleType = std::tuple<int32_t, uint64_t, ResultRow>;
300 
301 template <typename F>
302 static void sortByKey(std::vector<TupleType> &Results, F Fn) {
303   bool ASC = AccountSortOrder == SortDirection::ASCENDING;
304   llvm::sort(Results, [=](const TupleType &L, const TupleType &R) {
305     return ASC ? Fn(L) < Fn(R) : Fn(L) > Fn(R);
306   });
307 }
308 
309 template <class F>
310 void LatencyAccountant::exportStats(const XRayFileHeader &Header, F Fn) const {
311   std::vector<TupleType> Results;
312   Results.reserve(FunctionLatencies.size());
313   for (auto FT : FunctionLatencies) {
314     const auto &FuncId = FT.first;
315     auto &Timings = FT.second;
316     Results.emplace_back(FuncId, Timings.size(), getStats(Timings));
317     auto &Row = std::get<2>(Results.back());
318     if (Header.CycleFrequency) {
319       double CycleFrequency = Header.CycleFrequency;
320       Row.Min /= CycleFrequency;
321       Row.Median /= CycleFrequency;
322       Row.Pct90 /= CycleFrequency;
323       Row.Pct99 /= CycleFrequency;
324       Row.Max /= CycleFrequency;
325       Row.Sum /= CycleFrequency;
326     }
327 
328     Row.Function = FuncIdHelper.SymbolOrNumber(FuncId);
329     Row.DebugInfo = FuncIdHelper.FileLineAndColumn(FuncId);
330   }
331 
332   // Sort the data according to user-provided flags.
333   switch (AccountSortOutput) {
334   case SortField::FUNCID:
335     sortByKey(Results, [](const TupleType &X) { return std::get<0>(X); });
336     break;
337   case SortField::COUNT:
338     sortByKey(Results, [](const TupleType &X) { return std::get<1>(X); });
339     break;
340   case SortField::MIN:
341     sortByKey(Results, [](const TupleType &X) { return std::get<2>(X).Min; });
342     break;
343   case SortField::MED:
344     sortByKey(Results, [](const TupleType &X) { return std::get<2>(X).Median; });
345     break;
346   case SortField::PCT90:
347     sortByKey(Results, [](const TupleType &X) { return std::get<2>(X).Pct90; });
348     break;
349   case SortField::PCT99:
350     sortByKey(Results, [](const TupleType &X) { return std::get<2>(X).Pct99; });
351     break;
352   case SortField::MAX:
353     sortByKey(Results, [](const TupleType &X) { return std::get<2>(X).Max; });
354     break;
355   case SortField::SUM:
356     sortByKey(Results, [](const TupleType &X) { return std::get<2>(X).Sum; });
357     break;
358   case SortField::FUNC:
359     llvm_unreachable("Not implemented");
360   }
361 
362   if (AccountTop > 0) {
363     auto MaxTop =
364         std::min(AccountTop.getValue(), static_cast<int>(Results.size()));
365     Results.erase(Results.begin() + MaxTop, Results.end());
366   }
367 
368   for (const auto &R : Results)
369     Fn(std::get<0>(R), std::get<1>(R), std::get<2>(R));
370 }
371 
372 void LatencyAccountant::exportStatsAsText(raw_ostream &OS,
373                                           const XRayFileHeader &Header) const {
374   OS << "Functions with latencies: " << FunctionLatencies.size() << "\n";
375 
376   // We spend some effort to make the text output more readable, so we do the
377   // following formatting decisions for each of the fields:
378   //
379   //   - funcid: 32-bit, but we can determine the largest number and be
380   //   between
381   //     a minimum of 5 characters, up to 9 characters, right aligned.
382   //   - count:  64-bit, but we can determine the largest number and be
383   //   between
384   //     a minimum of 5 characters, up to 9 characters, right aligned.
385   //   - min, median, 90pct, 99pct, max: double precision, but we want to keep
386   //     the values in seconds, with microsecond precision (0.000'001), so we
387   //     have at most 6 significant digits, with the whole number part to be
388   //     at
389   //     least 1 character. For readability we'll right-align, with full 9
390   //     characters each.
391   //   - debug info, function name: we format this as a concatenation of the
392   //     debug info and the function name.
393   //
394   static constexpr char StatsHeaderFormat[] =
395       "{0,+9} {1,+10} [{2,+9}, {3,+9}, {4,+9}, {5,+9}, {6,+9}] {7,+9}";
396   static constexpr char StatsFormat[] =
397       R"({0,+9} {1,+10} [{2,+9:f6}, {3,+9:f6}, {4,+9:f6}, {5,+9:f6}, {6,+9:f6}] {7,+9:f6})";
398   OS << llvm::formatv(StatsHeaderFormat, "funcid", "count", "min", "med", "90p",
399                       "99p", "max", "sum")
400      << llvm::formatv("  {0,-12}\n", "function");
401   exportStats(Header, [&](int32_t FuncId, size_t Count, const ResultRow &Row) {
402     OS << llvm::formatv(StatsFormat, FuncId, Count, Row.Min, Row.Median,
403                         Row.Pct90, Row.Pct99, Row.Max, Row.Sum)
404        << "  " << Row.DebugInfo << ": " << Row.Function << "\n";
405   });
406 }
407 
408 void LatencyAccountant::exportStatsAsCSV(raw_ostream &OS,
409                                          const XRayFileHeader &Header) const {
410   OS << "funcid,count,min,median,90%ile,99%ile,max,sum,debug,function\n";
411   exportStats(Header, [&](int32_t FuncId, size_t Count, const ResultRow &Row) {
412     OS << FuncId << ',' << Count << ',' << Row.Min << ',' << Row.Median << ','
413        << Row.Pct90 << ',' << Row.Pct99 << ',' << Row.Max << "," << Row.Sum
414        << ",\"" << Row.DebugInfo << "\",\"" << Row.Function << "\"\n";
415   });
416 }
417 
418 using namespace llvm::xray;
419 
420 namespace llvm {
421 template <> struct format_provider<llvm::xray::RecordTypes> {
422   static void format(const llvm::xray::RecordTypes &T, raw_ostream &Stream,
423                      StringRef Style) {
424     switch (T) {
425     case RecordTypes::ENTER:
426       Stream << "enter";
427       break;
428     case RecordTypes::ENTER_ARG:
429       Stream << "enter-arg";
430       break;
431     case RecordTypes::EXIT:
432       Stream << "exit";
433       break;
434     case RecordTypes::TAIL_EXIT:
435       Stream << "tail-exit";
436       break;
437     case RecordTypes::CUSTOM_EVENT:
438       Stream << "custom-event";
439       break;
440     case RecordTypes::TYPED_EVENT:
441       Stream << "typed-event";
442       break;
443     }
444   }
445 };
446 } // namespace llvm
447 
448 static CommandRegistration Unused(&Account, []() -> Error {
449   InstrumentationMap Map;
450   if (!AccountInstrMap.empty()) {
451     auto InstrumentationMapOrError = loadInstrumentationMap(AccountInstrMap);
452     if (!InstrumentationMapOrError)
453       return joinErrors(make_error<StringError>(
454                             Twine("Cannot open instrumentation map '") +
455                                 AccountInstrMap + "'",
456                             std::make_error_code(std::errc::invalid_argument)),
457                         InstrumentationMapOrError.takeError());
458     Map = std::move(*InstrumentationMapOrError);
459   }
460 
461   std::error_code EC;
462   raw_fd_ostream OS(AccountOutput, EC, sys::fs::OpenFlags::OF_TextWithCRLF);
463   if (EC)
464     return make_error<StringError>(
465         Twine("Cannot open file '") + AccountOutput + "' for writing.", EC);
466 
467   const auto &FunctionAddresses = Map.getFunctionAddresses();
468   symbolize::LLVMSymbolizer Symbolizer;
469   llvm::xray::FuncIdConversionHelper FuncIdHelper(AccountInstrMap, Symbolizer,
470                                                   FunctionAddresses);
471   xray::LatencyAccountant FCA(FuncIdHelper, AccountRecursiveCallsOnly,
472                               AccountDeduceSiblingCalls);
473   auto TraceOrErr = loadTraceFile(AccountInput);
474   if (!TraceOrErr)
475     return joinErrors(
476         make_error<StringError>(
477             Twine("Failed loading input file '") + AccountInput + "'",
478             std::make_error_code(std::errc::executable_format_error)),
479         TraceOrErr.takeError());
480 
481   auto &T = *TraceOrErr;
482   for (const auto &Record : T) {
483     if (FCA.accountRecord(Record))
484       continue;
485     errs()
486         << "Error processing record: "
487         << llvm::formatv(
488                R"({{type: {0}; cpu: {1}; record-type: {2}; function-id: {3}; tsc: {4}; thread-id: {5}; process-id: {6}}})",
489                Record.RecordType, Record.CPU, Record.Type, Record.FuncId,
490                Record.TSC, Record.TId, Record.PId)
491         << '\n';
492     for (const auto &ThreadStack : FCA.getPerThreadFunctionStack()) {
493       errs() << "Thread ID: " << ThreadStack.first << "\n";
494       if (ThreadStack.second.Stack.empty()) {
495         errs() << "  (empty stack)\n";
496         continue;
497       }
498       auto Level = ThreadStack.second.Stack.size();
499       for (const auto &Entry : llvm::reverse(ThreadStack.second.Stack))
500         errs() << "  #" << Level-- << "\t"
501                << FuncIdHelper.SymbolOrNumber(Entry.first) << '\n';
502     }
503     if (!AccountKeepGoing)
504       return make_error<StringError>(
505           Twine("Failed accounting function calls in file '") + AccountInput +
506               "'.",
507           std::make_error_code(std::errc::executable_format_error));
508   }
509   switch (AccountOutputFormat) {
510   case AccountOutputFormats::TEXT:
511     FCA.exportStatsAsText(OS, T.getFileHeader());
512     break;
513   case AccountOutputFormats::CSV:
514     FCA.exportStatsAsCSV(OS, T.getFileHeader());
515     break;
516   }
517 
518   return Error::success();
519 });
520