| //===- xray-account.h - XRay Function Call Accounting ---------------------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| // |
| // This file implements basic function call accounting from an XRay trace. |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include <algorithm> |
| #include <cassert> |
| #include <numeric> |
| #include <system_error> |
| #include <utility> |
| |
| #include "xray-account.h" |
| #include "xray-registry.h" |
| #include "llvm/Support/ErrorHandling.h" |
| #include "llvm/Support/FormatVariadic.h" |
| #include "llvm/XRay/InstrumentationMap.h" |
| #include "llvm/XRay/Trace.h" |
| |
| using namespace llvm; |
| using namespace llvm::xray; |
| |
| static cl::SubCommand Account("account", "Function call accounting"); |
| static cl::opt<std::string> AccountInput(cl::Positional, |
| cl::desc("<xray log file>"), |
| cl::Required, cl::sub(Account)); |
| static cl::opt<bool> |
| AccountKeepGoing("keep-going", cl::desc("Keep going on errors encountered"), |
| cl::sub(Account), cl::init(false)); |
| static cl::alias AccountKeepGoing2("k", cl::aliasopt(AccountKeepGoing), |
| cl::desc("Alias for -keep_going"), |
| cl::sub(Account)); |
| static cl::opt<bool> AccountDeduceSiblingCalls( |
| "deduce-sibling-calls", |
| cl::desc("Deduce sibling calls when unrolling function call stacks"), |
| cl::sub(Account), cl::init(false)); |
| static cl::alias |
| AccountDeduceSiblingCalls2("d", cl::aliasopt(AccountDeduceSiblingCalls), |
| cl::desc("Alias for -deduce_sibling_calls"), |
| cl::sub(Account)); |
| static cl::opt<std::string> |
| AccountOutput("output", cl::value_desc("output file"), cl::init("-"), |
| cl::desc("output file; use '-' for stdout"), |
| cl::sub(Account)); |
| static cl::alias AccountOutput2("o", cl::aliasopt(AccountOutput), |
| cl::desc("Alias for -output"), |
| cl::sub(Account)); |
| enum class AccountOutputFormats { TEXT, CSV }; |
| static cl::opt<AccountOutputFormats> |
| AccountOutputFormat("format", cl::desc("output format"), |
| cl::values(clEnumValN(AccountOutputFormats::TEXT, |
| "text", "report stats in text"), |
| clEnumValN(AccountOutputFormats::CSV, "csv", |
| "report stats in csv")), |
| cl::sub(Account)); |
| static cl::alias AccountOutputFormat2("f", cl::desc("Alias of -format"), |
| cl::aliasopt(AccountOutputFormat), |
| cl::sub(Account)); |
| |
| enum class SortField { |
| FUNCID, |
| COUNT, |
| MIN, |
| MED, |
| PCT90, |
| PCT99, |
| MAX, |
| SUM, |
| FUNC, |
| }; |
| |
| static cl::opt<SortField> AccountSortOutput( |
| "sort", cl::desc("sort output by this field"), cl::value_desc("field"), |
| cl::sub(Account), cl::init(SortField::FUNCID), |
| cl::values(clEnumValN(SortField::FUNCID, "funcid", "function id"), |
| clEnumValN(SortField::COUNT, "count", "funciton call counts"), |
| clEnumValN(SortField::MIN, "min", "minimum function durations"), |
| clEnumValN(SortField::MED, "med", "median function durations"), |
| clEnumValN(SortField::PCT90, "90p", "90th percentile durations"), |
| clEnumValN(SortField::PCT99, "99p", "99th percentile durations"), |
| clEnumValN(SortField::MAX, "max", "maximum function durations"), |
| clEnumValN(SortField::SUM, "sum", "sum of call durations"), |
| clEnumValN(SortField::FUNC, "func", "function names"))); |
| static cl::alias AccountSortOutput2("s", cl::aliasopt(AccountSortOutput), |
| cl::desc("Alias for -sort"), |
| cl::sub(Account)); |
| |
| enum class SortDirection { |
| ASCENDING, |
| DESCENDING, |
| }; |
| static cl::opt<SortDirection> AccountSortOrder( |
| "sortorder", cl::desc("sort ordering"), cl::init(SortDirection::ASCENDING), |
| cl::values(clEnumValN(SortDirection::ASCENDING, "asc", "ascending"), |
| clEnumValN(SortDirection::DESCENDING, "dsc", "descending")), |
| cl::sub(Account)); |
| static cl::alias AccountSortOrder2("r", cl::aliasopt(AccountSortOrder), |
| cl::desc("Alias for -sortorder"), |
| cl::sub(Account)); |
| |
| static cl::opt<int> AccountTop("top", cl::desc("only show the top N results"), |
| cl::value_desc("N"), cl::sub(Account), |
| cl::init(-1)); |
| static cl::alias AccountTop2("p", cl::desc("Alias for -top"), |
| cl::aliasopt(AccountTop), cl::sub(Account)); |
| |
| static cl::opt<std::string> |
| AccountInstrMap("instr_map", |
| cl::desc("binary with the instrumentation map, or " |
| "a separate instrumentation map"), |
| cl::value_desc("binary with xray_instr_map"), |
| cl::sub(Account), cl::init("")); |
| static cl::alias AccountInstrMap2("m", cl::aliasopt(AccountInstrMap), |
| cl::desc("Alias for -instr_map"), |
| cl::sub(Account)); |
| |
| namespace { |
| |
| template <class T, class U> void setMinMax(std::pair<T, T> &MM, U &&V) { |
| if (MM.first == 0 || MM.second == 0) |
| MM = std::make_pair(std::forward<U>(V), std::forward<U>(V)); |
| else |
| MM = std::make_pair(std::min(MM.first, V), std::max(MM.second, V)); |
| } |
| |
| template <class T> T diff(T L, T R) { return std::max(L, R) - std::min(L, R); } |
| |
| } // namespace |
| |
| bool LatencyAccountant::accountRecord(const XRayRecord &Record) { |
| setMinMax(PerThreadMinMaxTSC[Record.TId], Record.TSC); |
| setMinMax(PerCPUMinMaxTSC[Record.CPU], Record.TSC); |
| |
| if (CurrentMaxTSC == 0) |
| CurrentMaxTSC = Record.TSC; |
| |
| if (Record.TSC < CurrentMaxTSC) |
| return false; |
| |
| auto &ThreadStack = PerThreadFunctionStack[Record.TId]; |
| switch (Record.Type) { |
| case RecordTypes::CUSTOM_EVENT: |
| case RecordTypes::TYPED_EVENT: |
| // TODO: Support custom and typed event accounting in the future. |
| return true; |
| case RecordTypes::ENTER: |
| case RecordTypes::ENTER_ARG: { |
| ThreadStack.emplace_back(Record.FuncId, Record.TSC); |
| break; |
| } |
| case RecordTypes::EXIT: |
| case RecordTypes::TAIL_EXIT: { |
| if (ThreadStack.empty()) |
| return false; |
| |
| if (ThreadStack.back().first == Record.FuncId) { |
| const auto &Top = ThreadStack.back(); |
| recordLatency(Top.first, diff(Top.second, Record.TSC)); |
| ThreadStack.pop_back(); |
| break; |
| } |
| |
| if (!DeduceSiblingCalls) |
| return false; |
| |
| // Look for the parent up the stack. |
| auto Parent = |
| std::find_if(ThreadStack.rbegin(), ThreadStack.rend(), |
| [&](const std::pair<const int32_t, uint64_t> &E) { |
| return E.first == Record.FuncId; |
| }); |
| if (Parent == ThreadStack.rend()) |
| return false; |
| |
| // Account time for this apparently sibling call exit up the stack. |
| // Considering the following case: |
| // |
| // f() |
| // g() |
| // h() |
| // |
| // We might only ever see the following entries: |
| // |
| // -> f() |
| // -> g() |
| // -> h() |
| // <- h() |
| // <- f() |
| // |
| // Now we don't see the exit to g() because some older version of the XRay |
| // runtime wasn't instrumenting tail exits. If we don't deduce tail calls, |
| // we may potentially never account time for g() -- and this code would have |
| // already bailed out, because `<- f()` doesn't match the current "top" of |
| // stack where we're waiting for the exit to `g()` instead. This is not |
| // ideal and brittle -- so instead we provide a potentially inaccurate |
| // accounting of g() instead, computing it from the exit of f(). |
| // |
| // While it might be better that we account the time between `-> g()` and |
| // `-> h()` as the proper accounting of time for g() here, this introduces |
| // complexity to do correctly (need to backtrack, etc.). |
| // |
| // FIXME: Potentially implement the more complex deduction algorithm? |
| auto I = std::next(Parent).base(); |
| for (auto &E : make_range(I, ThreadStack.end())) { |
| recordLatency(E.first, diff(E.second, Record.TSC)); |
| } |
| ThreadStack.erase(I, ThreadStack.end()); |
| break; |
| } |
| } |
| |
| return true; |
| } |
| |
| namespace { |
| |
| // We consolidate the data into a struct which we can output in various forms. |
| struct ResultRow { |
| uint64_t Count; |
| double Min; |
| double Median; |
| double Pct90; |
| double Pct99; |
| double Max; |
| double Sum; |
| std::string DebugInfo; |
| std::string Function; |
| }; |
| |
| ResultRow getStats(std::vector<uint64_t> &Timings) { |
| assert(!Timings.empty()); |
| ResultRow R; |
| R.Sum = std::accumulate(Timings.begin(), Timings.end(), 0.0); |
| auto MinMax = std::minmax_element(Timings.begin(), Timings.end()); |
| R.Min = *MinMax.first; |
| R.Max = *MinMax.second; |
| R.Count = Timings.size(); |
| |
| auto MedianOff = Timings.size() / 2; |
| std::nth_element(Timings.begin(), Timings.begin() + MedianOff, Timings.end()); |
| R.Median = Timings[MedianOff]; |
| |
| auto Pct90Off = std::floor(Timings.size() * 0.9); |
| std::nth_element(Timings.begin(), Timings.begin() + Pct90Off, Timings.end()); |
| R.Pct90 = Timings[Pct90Off]; |
| |
| auto Pct99Off = std::floor(Timings.size() * 0.99); |
| std::nth_element(Timings.begin(), Timings.begin() + Pct99Off, Timings.end()); |
| R.Pct99 = Timings[Pct99Off]; |
| return R; |
| } |
| |
| } // namespace |
| |
| using TupleType = std::tuple<int32_t, uint64_t, ResultRow>; |
| |
| template <typename F> |
| static void sortByKey(std::vector<TupleType> &Results, F Fn) { |
| bool ASC = AccountSortOrder == SortDirection::ASCENDING; |
| llvm::sort(Results, [=](const TupleType &L, const TupleType &R) { |
| return ASC ? Fn(L) < Fn(R) : Fn(L) > Fn(R); |
| }); |
| } |
| |
| template <class F> |
| void LatencyAccountant::exportStats(const XRayFileHeader &Header, F Fn) const { |
| std::vector<TupleType> Results; |
| Results.reserve(FunctionLatencies.size()); |
| for (auto FT : FunctionLatencies) { |
| const auto &FuncId = FT.first; |
| auto &Timings = FT.second; |
| Results.emplace_back(FuncId, Timings.size(), getStats(Timings)); |
| auto &Row = std::get<2>(Results.back()); |
| if (Header.CycleFrequency) { |
| double CycleFrequency = Header.CycleFrequency; |
| Row.Min /= CycleFrequency; |
| Row.Median /= CycleFrequency; |
| Row.Pct90 /= CycleFrequency; |
| Row.Pct99 /= CycleFrequency; |
| Row.Max /= CycleFrequency; |
| Row.Sum /= CycleFrequency; |
| } |
| |
| Row.Function = FuncIdHelper.SymbolOrNumber(FuncId); |
| Row.DebugInfo = FuncIdHelper.FileLineAndColumn(FuncId); |
| } |
| |
| // Sort the data according to user-provided flags. |
| switch (AccountSortOutput) { |
| case SortField::FUNCID: |
| sortByKey(Results, [](const TupleType &X) { return std::get<0>(X); }); |
| break; |
| case SortField::COUNT: |
| sortByKey(Results, [](const TupleType &X) { return std::get<1>(X); }); |
| break; |
| case SortField::MIN: |
| sortByKey(Results, [](const TupleType &X) { return std::get<2>(X).Min; }); |
| break; |
| case SortField::MED: |
| sortByKey(Results, [](const TupleType &X) { return std::get<2>(X).Median; }); |
| break; |
| case SortField::PCT90: |
| sortByKey(Results, [](const TupleType &X) { return std::get<2>(X).Pct90; }); |
| break; |
| case SortField::PCT99: |
| sortByKey(Results, [](const TupleType &X) { return std::get<2>(X).Pct99; }); |
| break; |
| case SortField::MAX: |
| sortByKey(Results, [](const TupleType &X) { return std::get<2>(X).Max; }); |
| break; |
| case SortField::SUM: |
| sortByKey(Results, [](const TupleType &X) { return std::get<2>(X).Sum; }); |
| break; |
| case SortField::FUNC: |
| llvm_unreachable("Not implemented"); |
| } |
| |
| if (AccountTop > 0) { |
| auto MaxTop = |
| std::min(AccountTop.getValue(), static_cast<int>(Results.size())); |
| Results.erase(Results.begin() + MaxTop, Results.end()); |
| } |
| |
| for (const auto &R : Results) |
| Fn(std::get<0>(R), std::get<1>(R), std::get<2>(R)); |
| } |
| |
| void LatencyAccountant::exportStatsAsText(raw_ostream &OS, |
| const XRayFileHeader &Header) const { |
| OS << "Functions with latencies: " << FunctionLatencies.size() << "\n"; |
| |
| // We spend some effort to make the text output more readable, so we do the |
| // following formatting decisions for each of the fields: |
| // |
| // - funcid: 32-bit, but we can determine the largest number and be |
| // between |
| // a minimum of 5 characters, up to 9 characters, right aligned. |
| // - count: 64-bit, but we can determine the largest number and be |
| // between |
| // a minimum of 5 characters, up to 9 characters, right aligned. |
| // - min, median, 90pct, 99pct, max: double precision, but we want to keep |
| // the values in seconds, with microsecond precision (0.000'001), so we |
| // have at most 6 significant digits, with the whole number part to be |
| // at |
| // least 1 character. For readability we'll right-align, with full 9 |
| // characters each. |
| // - debug info, function name: we format this as a concatenation of the |
| // debug info and the function name. |
| // |
| static constexpr char StatsHeaderFormat[] = |
| "{0,+9} {1,+10} [{2,+9}, {3,+9}, {4,+9}, {5,+9}, {6,+9}] {7,+9}"; |
| static constexpr char StatsFormat[] = |
| R"({0,+9} {1,+10} [{2,+9:f6}, {3,+9:f6}, {4,+9:f6}, {5,+9:f6}, {6,+9:f6}] {7,+9:f6})"; |
| OS << llvm::formatv(StatsHeaderFormat, "funcid", "count", "min", "med", "90p", |
| "99p", "max", "sum") |
| << llvm::formatv(" {0,-12}\n", "function"); |
| exportStats(Header, [&](int32_t FuncId, size_t Count, const ResultRow &Row) { |
| OS << llvm::formatv(StatsFormat, FuncId, Count, Row.Min, Row.Median, |
| Row.Pct90, Row.Pct99, Row.Max, Row.Sum) |
| << " " << Row.DebugInfo << ": " << Row.Function << "\n"; |
| }); |
| } |
| |
| void LatencyAccountant::exportStatsAsCSV(raw_ostream &OS, |
| const XRayFileHeader &Header) const { |
| OS << "funcid,count,min,median,90%ile,99%ile,max,sum,debug,function\n"; |
| exportStats(Header, [&](int32_t FuncId, size_t Count, const ResultRow &Row) { |
| OS << FuncId << ',' << Count << ',' << Row.Min << ',' << Row.Median << ',' |
| << Row.Pct90 << ',' << Row.Pct99 << ',' << Row.Max << "," << Row.Sum |
| << ",\"" << Row.DebugInfo << "\",\"" << Row.Function << "\"\n"; |
| }); |
| } |
| |
| using namespace llvm::xray; |
| |
| namespace llvm { |
| template <> struct format_provider<llvm::xray::RecordTypes> { |
| static void format(const llvm::xray::RecordTypes &T, raw_ostream &Stream, |
| StringRef Style) { |
| switch (T) { |
| case RecordTypes::ENTER: |
| Stream << "enter"; |
| break; |
| case RecordTypes::ENTER_ARG: |
| Stream << "enter-arg"; |
| break; |
| case RecordTypes::EXIT: |
| Stream << "exit"; |
| break; |
| case RecordTypes::TAIL_EXIT: |
| Stream << "tail-exit"; |
| break; |
| case RecordTypes::CUSTOM_EVENT: |
| Stream << "custom-event"; |
| break; |
| case RecordTypes::TYPED_EVENT: |
| Stream << "typed-event"; |
| break; |
| } |
| } |
| }; |
| } // namespace llvm |
| |
| static CommandRegistration Unused(&Account, []() -> Error { |
| InstrumentationMap Map; |
| if (!AccountInstrMap.empty()) { |
| auto InstrumentationMapOrError = loadInstrumentationMap(AccountInstrMap); |
| if (!InstrumentationMapOrError) |
| return joinErrors(make_error<StringError>( |
| Twine("Cannot open instrumentation map '") + |
| AccountInstrMap + "'", |
| std::make_error_code(std::errc::invalid_argument)), |
| InstrumentationMapOrError.takeError()); |
| Map = std::move(*InstrumentationMapOrError); |
| } |
| |
| std::error_code EC; |
| raw_fd_ostream OS(AccountOutput, EC, sys::fs::OpenFlags::F_Text); |
| if (EC) |
| return make_error<StringError>( |
| Twine("Cannot open file '") + AccountOutput + "' for writing.", EC); |
| |
| const auto &FunctionAddresses = Map.getFunctionAddresses(); |
| symbolize::LLVMSymbolizer::Options Opts( |
| symbolize::FunctionNameKind::LinkageName, true, true, false, ""); |
| symbolize::LLVMSymbolizer Symbolizer(Opts); |
| llvm::xray::FuncIdConversionHelper FuncIdHelper(AccountInstrMap, Symbolizer, |
| FunctionAddresses); |
| xray::LatencyAccountant FCA(FuncIdHelper, AccountDeduceSiblingCalls); |
| auto TraceOrErr = loadTraceFile(AccountInput); |
| if (!TraceOrErr) |
| return joinErrors( |
| make_error<StringError>( |
| Twine("Failed loading input file '") + AccountInput + "'", |
| std::make_error_code(std::errc::executable_format_error)), |
| TraceOrErr.takeError()); |
| |
| auto &T = *TraceOrErr; |
| for (const auto &Record : T) { |
| if (FCA.accountRecord(Record)) |
| continue; |
| errs() |
| << "Error processing record: " |
| << llvm::formatv( |
| R"({{type: {0}; cpu: {1}; record-type: {2}; function-id: {3}; tsc: {4}; thread-id: {5}; process-id: {6}}})", |
| Record.RecordType, Record.CPU, Record.Type, Record.FuncId, |
| Record.TSC, Record.TId, Record.PId) |
| << '\n'; |
| for (const auto &ThreadStack : FCA.getPerThreadFunctionStack()) { |
| errs() << "Thread ID: " << ThreadStack.first << "\n"; |
| if (ThreadStack.second.empty()) { |
| errs() << " (empty stack)\n"; |
| continue; |
| } |
| auto Level = ThreadStack.second.size(); |
| for (const auto &Entry : llvm::reverse(ThreadStack.second)) |
| errs() << " #" << Level-- << "\t" |
| << FuncIdHelper.SymbolOrNumber(Entry.first) << '\n'; |
| } |
| if (!AccountKeepGoing) |
| return make_error<StringError>( |
| Twine("Failed accounting function calls in file '") + AccountInput + |
| "'.", |
| std::make_error_code(std::errc::executable_format_error)); |
| } |
| switch (AccountOutputFormat) { |
| case AccountOutputFormats::TEXT: |
| FCA.exportStatsAsText(OS, T.getFileHeader()); |
| break; |
| case AccountOutputFormats::CSV: |
| FCA.exportStatsAsCSV(OS, T.getFileHeader()); |
| break; |
| } |
| |
| return Error::success(); |
| }); |