| //===-------------- RemarkSizeDiff.cpp ------------------------------------===// |
| // |
| // 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 |
| // |
| //===----------------------------------------------------------------------===// |
| /// |
| /// \file |
| /// Diffs instruction count and stack size remarks between two remark files. |
| /// |
| /// This is intended for use by compiler developers who want to see how their |
| /// changes impact program code size. |
| /// |
| //===----------------------------------------------------------------------===// |
| |
| #include "RemarkUtilHelpers.h" |
| #include "RemarkUtilRegistry.h" |
| #include "llvm/ADT/SmallSet.h" |
| #include "llvm/Support/FormatVariadic.h" |
| #include "llvm/Support/JSON.h" |
| |
| using namespace llvm; |
| using namespace remarks; |
| using namespace remarkutil; |
| static cl::SubCommand |
| RemarkSizeDiffUtil("size-diff", |
| "Diff instruction count and stack size remarks " |
| "between two remark files"); |
| enum ReportStyleOptions { human_output, json_output }; |
| static cl::opt<std::string> InputFileNameA(cl::Positional, cl::Required, |
| cl::sub(RemarkSizeDiffUtil), |
| cl::desc("remarks_a")); |
| static cl::opt<std::string> InputFileNameB(cl::Positional, cl::Required, |
| cl::sub(RemarkSizeDiffUtil), |
| cl::desc("remarks_b")); |
| static cl::opt<std::string> OutputFilename("o", cl::init("-"), |
| cl::sub(RemarkSizeDiffUtil), |
| cl::desc("Output"), |
| cl::value_desc("file")); |
| INPUT_FORMAT_COMMAND_LINE_OPTIONS(RemarkSizeDiffUtil) |
| static cl::opt<ReportStyleOptions> ReportStyle( |
| "report_style", cl::sub(RemarkSizeDiffUtil), |
| cl::init(ReportStyleOptions::human_output), |
| cl::desc("Choose the report output format:"), |
| cl::values(clEnumValN(human_output, "human", "Human-readable format"), |
| clEnumValN(json_output, "json", "JSON format"))); |
| static cl::opt<bool> PrettyPrint("pretty", cl::sub(RemarkSizeDiffUtil), |
| cl::init(false), |
| cl::desc("Pretty-print JSON")); |
| |
| /// Contains information from size remarks. |
| // This is a little nicer to read than a std::pair. |
| struct InstCountAndStackSize { |
| int64_t InstCount = 0; |
| int64_t StackSize = 0; |
| }; |
| |
| /// Represents which files a function appeared in. |
| enum FilesPresent { A, B, BOTH }; |
| |
| /// Contains the data from the remarks in file A and file B for some function. |
| /// E.g. instruction count, stack size... |
| struct FunctionDiff { |
| /// Function name from the remark. |
| std::string FuncName; |
| // Idx 0 = A, Idx 1 = B. |
| int64_t InstCount[2] = {0, 0}; |
| int64_t StackSize[2] = {0, 0}; |
| |
| // Calculate diffs between the first and second files. |
| int64_t getInstDiff() const { return InstCount[1] - InstCount[0]; } |
| int64_t getStackDiff() const { return StackSize[1] - StackSize[0]; } |
| |
| // Accessors for the remarks from the first file. |
| int64_t getInstCountA() const { return InstCount[0]; } |
| int64_t getStackSizeA() const { return StackSize[0]; } |
| |
| // Accessors for the remarks from the second file. |
| int64_t getInstCountB() const { return InstCount[1]; } |
| int64_t getStackSizeB() const { return StackSize[1]; } |
| |
| /// \returns which files this function was present in. |
| FilesPresent getFilesPresent() const { |
| if (getInstCountA() == 0) |
| return B; |
| if (getInstCountB() == 0) |
| return A; |
| return BOTH; |
| } |
| |
| FunctionDiff(StringRef FuncName, const InstCountAndStackSize &A, |
| const InstCountAndStackSize &B) |
| : FuncName(FuncName) { |
| InstCount[0] = A.InstCount; |
| InstCount[1] = B.InstCount; |
| StackSize[0] = A.StackSize; |
| StackSize[1] = B.StackSize; |
| } |
| }; |
| |
| /// Organizes the diffs into 3 categories: |
| /// - Functions which only appeared in the first file |
| /// - Functions which only appeared in the second file |
| /// - Functions which appeared in both files |
| struct DiffsCategorizedByFilesPresent { |
| /// Diffs for functions which only appeared in the first file. |
| SmallVector<FunctionDiff> OnlyInA; |
| |
| /// Diffs for functions which only appeared in the second file. |
| SmallVector<FunctionDiff> OnlyInB; |
| |
| /// Diffs for functions which appeared in both files. |
| SmallVector<FunctionDiff> InBoth; |
| |
| /// Add a diff to the appropriate list. |
| void addDiff(FunctionDiff &FD) { |
| switch (FD.getFilesPresent()) { |
| case A: |
| OnlyInA.push_back(FD); |
| break; |
| case B: |
| OnlyInB.push_back(FD); |
| break; |
| case BOTH: |
| InBoth.push_back(FD); |
| break; |
| } |
| } |
| }; |
| |
| static void printFunctionDiff(const FunctionDiff &FD, llvm::raw_ostream &OS) { |
| // Describe which files the function had remarks in. |
| FilesPresent FP = FD.getFilesPresent(); |
| const std::string &FuncName = FD.FuncName; |
| const int64_t InstDiff = FD.getInstDiff(); |
| assert(InstDiff && "Shouldn't get functions with no size change?"); |
| const int64_t StackDiff = FD.getStackDiff(); |
| // Output an indicator denoting which files the function was present in. |
| switch (FP) { |
| case FilesPresent::A: |
| OS << "-- "; |
| break; |
| case FilesPresent::B: |
| OS << "++ "; |
| break; |
| case FilesPresent::BOTH: |
| OS << "== "; |
| break; |
| } |
| // Output an indicator denoting if a function changed in size. |
| if (InstDiff > 0) |
| OS << "> "; |
| else |
| OS << "< "; |
| OS << FuncName << ", "; |
| OS << InstDiff << " instrs, "; |
| OS << StackDiff << " stack B"; |
| OS << "\n"; |
| } |
| |
| /// Print an item in the summary section. |
| /// |
| /// \p TotalA - Total count of the metric in file A. |
| /// \p TotalB - Total count of the metric in file B. |
| /// \p Metric - Name of the metric we want to print (e.g. instruction |
| /// count). |
| /// \p OS - The output stream. |
| static void printSummaryItem(int64_t TotalA, int64_t TotalB, StringRef Metric, |
| llvm::raw_ostream &OS) { |
| OS << " " << Metric << ": "; |
| int64_t TotalDiff = TotalB - TotalA; |
| if (TotalDiff == 0) { |
| OS << "None\n"; |
| return; |
| } |
| OS << TotalDiff << " (" << formatv("{0:p}", TotalDiff / (double)TotalA) |
| << ")\n"; |
| } |
| |
| /// Print all contents of \p Diff and a high-level summary of the differences. |
| static void printDiffsCategorizedByFilesPresent( |
| DiffsCategorizedByFilesPresent &DiffsByFilesPresent, |
| llvm::raw_ostream &OS) { |
| int64_t InstrsA = 0; |
| int64_t InstrsB = 0; |
| int64_t StackA = 0; |
| int64_t StackB = 0; |
| // Helper lambda to sort + print a list of diffs. |
| auto PrintDiffList = [&](SmallVector<FunctionDiff> &FunctionDiffList) { |
| if (FunctionDiffList.empty()) |
| return; |
| stable_sort(FunctionDiffList, |
| [](const FunctionDiff &LHS, const FunctionDiff &RHS) { |
| return LHS.getInstDiff() < RHS.getInstDiff(); |
| }); |
| for (const auto &FuncDiff : FunctionDiffList) { |
| // If there is a difference in instruction count, then print out info for |
| // the function. |
| if (FuncDiff.getInstDiff()) |
| printFunctionDiff(FuncDiff, OS); |
| InstrsA += FuncDiff.getInstCountA(); |
| InstrsB += FuncDiff.getInstCountB(); |
| StackA += FuncDiff.getStackSizeA(); |
| StackB += FuncDiff.getStackSizeB(); |
| } |
| }; |
| PrintDiffList(DiffsByFilesPresent.OnlyInA); |
| PrintDiffList(DiffsByFilesPresent.OnlyInB); |
| PrintDiffList(DiffsByFilesPresent.InBoth); |
| OS << "\n### Summary ###\n"; |
| OS << "Total change: \n"; |
| printSummaryItem(InstrsA, InstrsB, "instruction count", OS); |
| printSummaryItem(StackA, StackB, "stack byte usage", OS); |
| } |
| |
| /// Collects an expected integer value from a given argument index in a remark. |
| /// |
| /// \p Remark - The remark. |
| /// \p ArgIdx - The index where the integer value should be found. |
| /// \p ExpectedKeyName - The expected key name for the index |
| /// (e.g. "InstructionCount") |
| /// |
| /// \returns the integer value at the index if it exists, and the key-value pair |
| /// is what is expected. Otherwise, returns an Error. |
| static Expected<int64_t> getIntValFromKey(const remarks::Remark &Remark, |
| unsigned ArgIdx, |
| StringRef ExpectedKeyName) { |
| auto KeyName = Remark.Args[ArgIdx].Key; |
| if (KeyName != ExpectedKeyName) |
| return createStringError( |
| inconvertibleErrorCode(), |
| Twine("Unexpected key at argument index " + std::to_string(ArgIdx) + |
| ": Expected '" + ExpectedKeyName + "', got '" + KeyName + "'")); |
| long long Val; |
| auto ValStr = Remark.Args[ArgIdx].Val; |
| if (getAsSignedInteger(ValStr, 0, Val)) |
| return createStringError( |
| inconvertibleErrorCode(), |
| Twine("Could not convert string to signed integer: " + ValStr)); |
| return static_cast<int64_t>(Val); |
| } |
| |
| /// Collects relevant size information from \p Remark if it is an size-related |
| /// remark of some kind (e.g. instruction count). Otherwise records nothing. |
| /// |
| /// \p Remark - The remark. |
| /// \p FuncNameToSizeInfo - Maps function names to relevant size info. |
| /// \p NumInstCountRemarksParsed - Keeps track of the number of instruction |
| /// count remarks parsed. We need at least 1 in both files to produce a diff. |
| static Error processRemark(const remarks::Remark &Remark, |
| StringMap<InstCountAndStackSize> &FuncNameToSizeInfo, |
| unsigned &NumInstCountRemarksParsed) { |
| const auto &RemarkName = Remark.RemarkName; |
| const auto &PassName = Remark.PassName; |
| // Collect remarks which contain the number of instructions in a function. |
| if (PassName == "asm-printer" && RemarkName == "InstructionCount") { |
| // Expecting the 0-th argument to have the key "NumInstructions" and an |
| // integer value. |
| auto MaybeInstCount = |
| getIntValFromKey(Remark, /*ArgIdx = */ 0, "NumInstructions"); |
| if (!MaybeInstCount) |
| return MaybeInstCount.takeError(); |
| FuncNameToSizeInfo[Remark.FunctionName].InstCount = *MaybeInstCount; |
| ++NumInstCountRemarksParsed; |
| } |
| // Collect remarks which contain the stack size of a function. |
| else if (PassName == "prologepilog" && RemarkName == "StackSize") { |
| // Expecting the 0-th argument to have the key "NumStackBytes" and an |
| // integer value. |
| auto MaybeStackSize = |
| getIntValFromKey(Remark, /*ArgIdx = */ 0, "NumStackBytes"); |
| if (!MaybeStackSize) |
| return MaybeStackSize.takeError(); |
| FuncNameToSizeInfo[Remark.FunctionName].StackSize = *MaybeStackSize; |
| } |
| // Either we collected a remark, or it's something we don't care about. In |
| // both cases, this is a success. |
| return Error::success(); |
| } |
| |
| /// Process all of the size-related remarks in a file. |
| /// |
| /// \param[in] InputFileName - Name of file to read from. |
| /// \param[in, out] FuncNameToSizeInfo - Maps function names to relevant |
| /// size info. |
| static Error readFileAndProcessRemarks( |
| StringRef InputFileName, |
| StringMap<InstCountAndStackSize> &FuncNameToSizeInfo) { |
| |
| auto MaybeBuf = getInputMemoryBuffer(InputFileName); |
| if (!MaybeBuf) |
| return MaybeBuf.takeError(); |
| auto MaybeParser = |
| createRemarkParserFromMeta(InputFormat, (*MaybeBuf)->getBuffer()); |
| if (!MaybeParser) |
| return MaybeParser.takeError(); |
| auto &Parser = **MaybeParser; |
| auto MaybeRemark = Parser.next(); |
| unsigned NumInstCountRemarksParsed = 0; |
| for (; MaybeRemark; MaybeRemark = Parser.next()) { |
| if (auto E = processRemark(**MaybeRemark, FuncNameToSizeInfo, |
| NumInstCountRemarksParsed)) |
| return E; |
| } |
| auto E = MaybeRemark.takeError(); |
| if (!E.isA<remarks::EndOfFileError>()) |
| return E; |
| consumeError(std::move(E)); |
| // We need at least one instruction count remark in each file to produce a |
| // meaningful diff. |
| if (NumInstCountRemarksParsed == 0) |
| return createStringError( |
| inconvertibleErrorCode(), |
| "File '" + InputFileName + |
| "' did not contain any instruction-count remarks!"); |
| return Error::success(); |
| } |
| |
| /// Wrapper function for readFileAndProcessRemarks which handles errors. |
| /// |
| /// \param[in] InputFileName - Name of file to read from. |
| /// \param[out] FuncNameToSizeInfo - Populated with information from size |
| /// remarks in the input file. |
| /// |
| /// \returns true if readFileAndProcessRemarks returned no errors. False |
| /// otherwise. |
| static Error tryReadFileAndProcessRemarks( |
| StringRef InputFileName, |
| StringMap<InstCountAndStackSize> &FuncNameToSizeInfo) { |
| if (Error E = readFileAndProcessRemarks(InputFileName, FuncNameToSizeInfo)) { |
| return E; |
| } |
| return Error::success(); |
| } |
| |
| /// Populates \p FuncDiffs with the difference between \p |
| /// FuncNameToSizeInfoA and \p FuncNameToSizeInfoB. |
| /// |
| /// \param[in] FuncNameToSizeInfoA - Size info collected from the first |
| /// remarks file. |
| /// \param[in] FuncNameToSizeInfoB - Size info collected from |
| /// the second remarks file. |
| /// \param[out] DiffsByFilesPresent - Filled with the diff between \p |
| /// FuncNameToSizeInfoA and \p FuncNameToSizeInfoB. |
| static void |
| computeDiff(const StringMap<InstCountAndStackSize> &FuncNameToSizeInfoA, |
| const StringMap<InstCountAndStackSize> &FuncNameToSizeInfoB, |
| DiffsCategorizedByFilesPresent &DiffsByFilesPresent) { |
| SmallSet<std::string, 10> FuncNames; |
| for (const auto &FuncName : FuncNameToSizeInfoA.keys()) |
| FuncNames.insert(FuncName.str()); |
| for (const auto &FuncName : FuncNameToSizeInfoB.keys()) |
| FuncNames.insert(FuncName.str()); |
| for (const std::string &FuncName : FuncNames) { |
| const auto &SizeInfoA = FuncNameToSizeInfoA.lookup(FuncName); |
| const auto &SizeInfoB = FuncNameToSizeInfoB.lookup(FuncName); |
| FunctionDiff FuncDiff(FuncName, SizeInfoA, SizeInfoB); |
| DiffsByFilesPresent.addDiff(FuncDiff); |
| } |
| } |
| |
| /// Attempt to get the output stream for writing the diff. |
| static ErrorOr<std::unique_ptr<ToolOutputFile>> getOutputStream() { |
| if (OutputFilename == "") |
| OutputFilename = "-"; |
| std::error_code EC; |
| auto Out = std::make_unique<ToolOutputFile>(OutputFilename, EC, |
| sys::fs::OF_TextWithCRLF); |
| if (!EC) |
| return std::move(Out); |
| return EC; |
| } |
| |
| /// \return a json::Array representing all FunctionDiffs in \p FunctionDiffs. |
| /// \p WhichFiles represents which files the functions in \p FunctionDiffs |
| /// appeared in (A, B, or both). |
| json::Array |
| getFunctionDiffListAsJSON(const SmallVector<FunctionDiff> &FunctionDiffs, |
| const FilesPresent &WhichFiles) { |
| json::Array FunctionDiffsAsJSON; |
| int64_t InstCountA, InstCountB, StackSizeA, StackSizeB; |
| for (auto &Diff : FunctionDiffs) { |
| InstCountA = InstCountB = StackSizeA = StackSizeB = 0; |
| switch (WhichFiles) { |
| case BOTH: |
| [[fallthrough]]; |
| case A: |
| InstCountA = Diff.getInstCountA(); |
| StackSizeA = Diff.getStackSizeA(); |
| if (WhichFiles != BOTH) |
| break; |
| [[fallthrough]]; |
| case B: |
| InstCountB = Diff.getInstCountB(); |
| StackSizeB = Diff.getStackSizeB(); |
| break; |
| } |
| // Each metric we care about is represented like: |
| // "Val": [A, B] |
| // This allows any consumer of the JSON to calculate the diff using B - A. |
| // This is somewhat wasteful for OnlyInA and OnlyInB (we only need A or B). |
| // However, this should make writing consuming tools easier, since the tool |
| // writer doesn't need to think about slightly different formats in each |
| // section. |
| json::Object FunctionObject({{"FunctionName", Diff.FuncName}, |
| {"InstCount", {InstCountA, InstCountB}}, |
| {"StackSize", {StackSizeA, StackSizeB}}}); |
| FunctionDiffsAsJSON.push_back(std::move(FunctionObject)); |
| } |
| return FunctionDiffsAsJSON; |
| } |
| |
| /// Output all diffs in \p DiffsByFilesPresent as a JSON report. This is |
| /// intended for consumption by external tools. |
| /// |
| /// \p InputFileNameA - File A used to produce the report. |
| /// \p InputFileNameB - File B used ot produce the report. |
| /// \p OS - Output stream. |
| /// |
| /// JSON output includes: |
| /// - \p InputFileNameA and \p InputFileNameB under "Files". |
| /// - Functions present in both files under "InBoth". |
| /// - Functions present only in A in "OnlyInA". |
| /// - Functions present only in B in "OnlyInB". |
| /// - Instruction count and stack size differences for each function. |
| /// |
| /// Differences are represented using [count_a, count_b]. The actual difference |
| /// can be computed via count_b - count_a. |
| static void |
| outputJSONForAllDiffs(StringRef InputFileNameA, StringRef InputFileNameB, |
| const DiffsCategorizedByFilesPresent &DiffsByFilesPresent, |
| llvm::raw_ostream &OS) { |
| json::Object Output; |
| // Include file names in the report. |
| json::Object Files( |
| {{"A", InputFileNameA.str()}, {"B", InputFileNameB.str()}}); |
| Output["Files"] = std::move(Files); |
| Output["OnlyInA"] = getFunctionDiffListAsJSON(DiffsByFilesPresent.OnlyInA, A); |
| Output["OnlyInB"] = getFunctionDiffListAsJSON(DiffsByFilesPresent.OnlyInB, B); |
| Output["InBoth"] = |
| getFunctionDiffListAsJSON(DiffsByFilesPresent.InBoth, BOTH); |
| json::OStream JOS(OS, PrettyPrint ? 2 : 0); |
| JOS.value(std::move(Output)); |
| OS << '\n'; |
| } |
| |
| /// Output all diffs in \p DiffsByFilesPresent using the desired output style. |
| /// \returns Error::success() on success, and an Error otherwise. |
| /// \p InputFileNameA - Name of input file A; may be used in the report. |
| /// \p InputFileNameB - Name of input file B; may be used in the report. |
| static Error |
| outputAllDiffs(StringRef InputFileNameA, StringRef InputFileNameB, |
| DiffsCategorizedByFilesPresent &DiffsByFilesPresent) { |
| auto MaybeOF = getOutputStream(); |
| if (std::error_code EC = MaybeOF.getError()) |
| return errorCodeToError(EC); |
| std::unique_ptr<ToolOutputFile> OF = std::move(*MaybeOF); |
| switch (ReportStyle) { |
| case human_output: |
| printDiffsCategorizedByFilesPresent(DiffsByFilesPresent, OF->os()); |
| break; |
| case json_output: |
| outputJSONForAllDiffs(InputFileNameA, InputFileNameB, DiffsByFilesPresent, |
| OF->os()); |
| break; |
| } |
| OF->keep(); |
| return Error::success(); |
| } |
| |
| /// Boolean wrapper for outputDiff which handles errors. |
| static Error |
| tryOutputAllDiffs(StringRef InputFileNameA, StringRef InputFileNameB, |
| DiffsCategorizedByFilesPresent &DiffsByFilesPresent) { |
| if (Error E = |
| outputAllDiffs(InputFileNameA, InputFileNameB, DiffsByFilesPresent)) { |
| return E; |
| } |
| return Error::success(); |
| } |
| |
| static Error trySizeSiff() { |
| StringMap<InstCountAndStackSize> FuncNameToSizeInfoA; |
| StringMap<InstCountAndStackSize> FuncNameToSizeInfoB; |
| if (auto E = |
| tryReadFileAndProcessRemarks(InputFileNameA, FuncNameToSizeInfoA)) |
| return E; |
| if (auto E = |
| tryReadFileAndProcessRemarks(InputFileNameB, FuncNameToSizeInfoB)) |
| return E; |
| DiffsCategorizedByFilesPresent DiffsByFilesPresent; |
| computeDiff(FuncNameToSizeInfoA, FuncNameToSizeInfoB, DiffsByFilesPresent); |
| if (auto E = tryOutputAllDiffs(InputFileNameA, InputFileNameB, |
| DiffsByFilesPresent)) |
| return E; |
| return Error::success(); |
| } |
| |
| static CommandRegistration RemarkSizeSiffRegister(&RemarkSizeDiffUtil, |
| trySizeSiff); |