xref: /freebsd/contrib/llvm-project/llvm/tools/llvm-remarkutil/RemarkSizeDiff.cpp (revision b64c5a0ace59af62eff52bfe110a521dc73c937b)
1 //===-------------- RemarkSizeDiff.cpp ------------------------------------===//
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 /// \file
10 /// Diffs instruction count and stack size remarks between two remark files.
11 ///
12 /// This is intended for use by compiler developers who want to see how their
13 /// changes impact program code size.
14 ///
15 //===----------------------------------------------------------------------===//
16 
17 #include "RemarkUtilHelpers.h"
18 #include "RemarkUtilRegistry.h"
19 #include "llvm/ADT/SmallSet.h"
20 #include "llvm/Support/FormatVariadic.h"
21 #include "llvm/Support/JSON.h"
22 
23 using namespace llvm;
24 using namespace remarks;
25 using namespace remarkutil;
26 static cl::SubCommand
27     RemarkSizeDiffUtil("size-diff",
28                        "Diff instruction count and stack size remarks "
29                        "between two remark files");
30 enum ReportStyleOptions { human_output, json_output };
31 static cl::opt<std::string> InputFileNameA(cl::Positional, cl::Required,
32                                            cl::sub(RemarkSizeDiffUtil),
33                                            cl::desc("remarks_a"));
34 static cl::opt<std::string> InputFileNameB(cl::Positional, cl::Required,
35                                            cl::sub(RemarkSizeDiffUtil),
36                                            cl::desc("remarks_b"));
37 static cl::opt<std::string> OutputFilename("o", cl::init("-"),
38                                            cl::sub(RemarkSizeDiffUtil),
39                                            cl::desc("Output"),
40                                            cl::value_desc("file"));
41 INPUT_FORMAT_COMMAND_LINE_OPTIONS(RemarkSizeDiffUtil)
42 static cl::opt<ReportStyleOptions> ReportStyle(
43     "report_style", cl::sub(RemarkSizeDiffUtil),
44     cl::init(ReportStyleOptions::human_output),
45     cl::desc("Choose the report output format:"),
46     cl::values(clEnumValN(human_output, "human", "Human-readable format"),
47                clEnumValN(json_output, "json", "JSON format")));
48 static cl::opt<bool> PrettyPrint("pretty", cl::sub(RemarkSizeDiffUtil),
49                                  cl::init(false),
50                                  cl::desc("Pretty-print JSON"));
51 
52 /// Contains information from size remarks.
53 // This is a little nicer to read than a std::pair.
54 struct InstCountAndStackSize {
55   int64_t InstCount = 0;
56   int64_t StackSize = 0;
57 };
58 
59 /// Represents which files a function appeared in.
60 enum FilesPresent { A, B, BOTH };
61 
62 /// Contains the data from the remarks in file A and file B for some function.
63 /// E.g. instruction count, stack size...
64 struct FunctionDiff {
65   /// Function name from the remark.
66   std::string FuncName;
67   // Idx 0 = A, Idx 1 = B.
68   int64_t InstCount[2] = {0, 0};
69   int64_t StackSize[2] = {0, 0};
70 
71   // Calculate diffs between the first and second files.
72   int64_t getInstDiff() const { return InstCount[1] - InstCount[0]; }
73   int64_t getStackDiff() const { return StackSize[1] - StackSize[0]; }
74 
75   // Accessors for the remarks from the first file.
76   int64_t getInstCountA() const { return InstCount[0]; }
77   int64_t getStackSizeA() const { return StackSize[0]; }
78 
79   // Accessors for the remarks from the second file.
80   int64_t getInstCountB() const { return InstCount[1]; }
81   int64_t getStackSizeB() const { return StackSize[1]; }
82 
83   /// \returns which files this function was present in.
84   FilesPresent getFilesPresent() const {
85     if (getInstCountA() == 0)
86       return B;
87     if (getInstCountB() == 0)
88       return A;
89     return BOTH;
90   }
91 
92   FunctionDiff(StringRef FuncName, const InstCountAndStackSize &A,
93                const InstCountAndStackSize &B)
94       : FuncName(FuncName) {
95     InstCount[0] = A.InstCount;
96     InstCount[1] = B.InstCount;
97     StackSize[0] = A.StackSize;
98     StackSize[1] = B.StackSize;
99   }
100 };
101 
102 /// Organizes the diffs into 3 categories:
103 /// - Functions which only appeared in the first file
104 /// - Functions which only appeared in the second file
105 /// - Functions which appeared in both files
106 struct DiffsCategorizedByFilesPresent {
107   /// Diffs for functions which only appeared in the first file.
108   SmallVector<FunctionDiff> OnlyInA;
109 
110   /// Diffs for functions which only appeared in the second file.
111   SmallVector<FunctionDiff> OnlyInB;
112 
113   /// Diffs for functions which appeared in both files.
114   SmallVector<FunctionDiff> InBoth;
115 
116   /// Add a diff to the appropriate list.
117   void addDiff(FunctionDiff &FD) {
118     switch (FD.getFilesPresent()) {
119     case A:
120       OnlyInA.push_back(FD);
121       break;
122     case B:
123       OnlyInB.push_back(FD);
124       break;
125     case BOTH:
126       InBoth.push_back(FD);
127       break;
128     }
129   }
130 };
131 
132 static void printFunctionDiff(const FunctionDiff &FD, llvm::raw_ostream &OS) {
133   // Describe which files the function had remarks in.
134   FilesPresent FP = FD.getFilesPresent();
135   const std::string &FuncName = FD.FuncName;
136   const int64_t InstDiff = FD.getInstDiff();
137   assert(InstDiff && "Shouldn't get functions with no size change?");
138   const int64_t StackDiff = FD.getStackDiff();
139   // Output an indicator denoting which files the function was present in.
140   switch (FP) {
141   case FilesPresent::A:
142     OS << "-- ";
143     break;
144   case FilesPresent::B:
145     OS << "++ ";
146     break;
147   case FilesPresent::BOTH:
148     OS << "== ";
149     break;
150   }
151   // Output an indicator denoting if a function changed in size.
152   if (InstDiff > 0)
153     OS << "> ";
154   else
155     OS << "< ";
156   OS << FuncName << ", ";
157   OS << InstDiff << " instrs, ";
158   OS << StackDiff << " stack B";
159   OS << "\n";
160 }
161 
162 /// Print an item in the summary section.
163 ///
164 /// \p TotalA - Total count of the metric in file A.
165 /// \p TotalB - Total count of the metric in file B.
166 /// \p Metric - Name of the metric we want to print (e.g. instruction
167 /// count).
168 /// \p OS - The output stream.
169 static void printSummaryItem(int64_t TotalA, int64_t TotalB, StringRef Metric,
170                              llvm::raw_ostream &OS) {
171   OS << "  " << Metric << ": ";
172   int64_t TotalDiff = TotalB - TotalA;
173   if (TotalDiff == 0) {
174     OS << "None\n";
175     return;
176   }
177   OS << TotalDiff << " (" << formatv("{0:p}", TotalDiff / (double)TotalA)
178      << ")\n";
179 }
180 
181 /// Print all contents of \p Diff and a high-level summary of the differences.
182 static void printDiffsCategorizedByFilesPresent(
183     DiffsCategorizedByFilesPresent &DiffsByFilesPresent,
184     llvm::raw_ostream &OS) {
185   int64_t InstrsA = 0;
186   int64_t InstrsB = 0;
187   int64_t StackA = 0;
188   int64_t StackB = 0;
189   // Helper lambda to sort + print a list of diffs.
190   auto PrintDiffList = [&](SmallVector<FunctionDiff> &FunctionDiffList) {
191     if (FunctionDiffList.empty())
192       return;
193     stable_sort(FunctionDiffList,
194                 [](const FunctionDiff &LHS, const FunctionDiff &RHS) {
195                   return LHS.getInstDiff() < RHS.getInstDiff();
196                 });
197     for (const auto &FuncDiff : FunctionDiffList) {
198       // If there is a difference in instruction count, then print out info for
199       // the function.
200       if (FuncDiff.getInstDiff())
201         printFunctionDiff(FuncDiff, OS);
202       InstrsA += FuncDiff.getInstCountA();
203       InstrsB += FuncDiff.getInstCountB();
204       StackA += FuncDiff.getStackSizeA();
205       StackB += FuncDiff.getStackSizeB();
206     }
207   };
208   PrintDiffList(DiffsByFilesPresent.OnlyInA);
209   PrintDiffList(DiffsByFilesPresent.OnlyInB);
210   PrintDiffList(DiffsByFilesPresent.InBoth);
211   OS << "\n### Summary ###\n";
212   OS << "Total change: \n";
213   printSummaryItem(InstrsA, InstrsB, "instruction count", OS);
214   printSummaryItem(StackA, StackB, "stack byte usage", OS);
215 }
216 
217 /// Collects an expected integer value from a given argument index in a remark.
218 ///
219 /// \p Remark - The remark.
220 /// \p ArgIdx - The index where the integer value should be found.
221 /// \p ExpectedKeyName - The expected key name for the index
222 /// (e.g. "InstructionCount")
223 ///
224 /// \returns the integer value at the index if it exists, and the key-value pair
225 /// is what is expected. Otherwise, returns an Error.
226 static Expected<int64_t> getIntValFromKey(const remarks::Remark &Remark,
227                                           unsigned ArgIdx,
228                                           StringRef ExpectedKeyName) {
229   auto KeyName = Remark.Args[ArgIdx].Key;
230   if (KeyName != ExpectedKeyName)
231     return createStringError(
232         inconvertibleErrorCode(),
233         Twine("Unexpected key at argument index " + std::to_string(ArgIdx) +
234               ": Expected '" + ExpectedKeyName + "', got '" + KeyName + "'"));
235   long long Val;
236   auto ValStr = Remark.Args[ArgIdx].Val;
237   if (getAsSignedInteger(ValStr, 0, Val))
238     return createStringError(
239         inconvertibleErrorCode(),
240         Twine("Could not convert string to signed integer: " + ValStr));
241   return static_cast<int64_t>(Val);
242 }
243 
244 /// Collects relevant size information from \p Remark if it is an size-related
245 /// remark of some kind (e.g. instruction count). Otherwise records nothing.
246 ///
247 /// \p Remark - The remark.
248 /// \p FuncNameToSizeInfo - Maps function names to relevant size info.
249 /// \p NumInstCountRemarksParsed - Keeps track of the number of instruction
250 /// count remarks parsed. We need at least 1 in both files to produce a diff.
251 static Error processRemark(const remarks::Remark &Remark,
252                            StringMap<InstCountAndStackSize> &FuncNameToSizeInfo,
253                            unsigned &NumInstCountRemarksParsed) {
254   const auto &RemarkName = Remark.RemarkName;
255   const auto &PassName = Remark.PassName;
256   // Collect remarks which contain the number of instructions in a function.
257   if (PassName == "asm-printer" && RemarkName == "InstructionCount") {
258     // Expecting the 0-th argument to have the key "NumInstructions" and an
259     // integer value.
260     auto MaybeInstCount =
261         getIntValFromKey(Remark, /*ArgIdx = */ 0, "NumInstructions");
262     if (!MaybeInstCount)
263       return MaybeInstCount.takeError();
264     FuncNameToSizeInfo[Remark.FunctionName].InstCount = *MaybeInstCount;
265     ++NumInstCountRemarksParsed;
266   }
267   // Collect remarks which contain the stack size of a function.
268   else if (PassName == "prologepilog" && RemarkName == "StackSize") {
269     // Expecting the 0-th argument to have the key "NumStackBytes" and an
270     // integer value.
271     auto MaybeStackSize =
272         getIntValFromKey(Remark, /*ArgIdx = */ 0, "NumStackBytes");
273     if (!MaybeStackSize)
274       return MaybeStackSize.takeError();
275     FuncNameToSizeInfo[Remark.FunctionName].StackSize = *MaybeStackSize;
276   }
277   // Either we collected a remark, or it's something we don't care about. In
278   // both cases, this is a success.
279   return Error::success();
280 }
281 
282 /// Process all of the size-related remarks in a file.
283 ///
284 /// \param[in] InputFileName - Name of file to read from.
285 /// \param[in, out] FuncNameToSizeInfo - Maps function names to relevant
286 /// size info.
287 static Error readFileAndProcessRemarks(
288     StringRef InputFileName,
289     StringMap<InstCountAndStackSize> &FuncNameToSizeInfo) {
290 
291   auto MaybeBuf = getInputMemoryBuffer(InputFileName);
292   if (!MaybeBuf)
293     return MaybeBuf.takeError();
294   auto MaybeParser =
295       createRemarkParserFromMeta(InputFormat, (*MaybeBuf)->getBuffer());
296   if (!MaybeParser)
297     return MaybeParser.takeError();
298   auto &Parser = **MaybeParser;
299   auto MaybeRemark = Parser.next();
300   unsigned NumInstCountRemarksParsed = 0;
301   for (; MaybeRemark; MaybeRemark = Parser.next()) {
302     if (auto E = processRemark(**MaybeRemark, FuncNameToSizeInfo,
303                                NumInstCountRemarksParsed))
304       return E;
305   }
306   auto E = MaybeRemark.takeError();
307   if (!E.isA<remarks::EndOfFileError>())
308     return E;
309   consumeError(std::move(E));
310   // We need at least one instruction count remark in each file to produce a
311   // meaningful diff.
312   if (NumInstCountRemarksParsed == 0)
313     return createStringError(
314         inconvertibleErrorCode(),
315         "File '" + InputFileName +
316             "' did not contain any instruction-count remarks!");
317   return Error::success();
318 }
319 
320 /// Wrapper function for readFileAndProcessRemarks which handles errors.
321 ///
322 /// \param[in] InputFileName - Name of file to read from.
323 /// \param[out] FuncNameToSizeInfo - Populated with information from size
324 /// remarks in the input file.
325 ///
326 /// \returns true if readFileAndProcessRemarks returned no errors. False
327 /// otherwise.
328 static Error tryReadFileAndProcessRemarks(
329     StringRef InputFileName,
330     StringMap<InstCountAndStackSize> &FuncNameToSizeInfo) {
331   if (Error E = readFileAndProcessRemarks(InputFileName, FuncNameToSizeInfo)) {
332     return E;
333   }
334   return Error::success();
335 }
336 
337 /// Populates \p FuncDiffs with the difference between \p
338 /// FuncNameToSizeInfoA and \p FuncNameToSizeInfoB.
339 ///
340 /// \param[in] FuncNameToSizeInfoA - Size info collected from the first
341 /// remarks file.
342 /// \param[in] FuncNameToSizeInfoB - Size info collected from
343 /// the second remarks file.
344 /// \param[out] DiffsByFilesPresent - Filled with the diff between \p
345 /// FuncNameToSizeInfoA and \p FuncNameToSizeInfoB.
346 static void
347 computeDiff(const StringMap<InstCountAndStackSize> &FuncNameToSizeInfoA,
348             const StringMap<InstCountAndStackSize> &FuncNameToSizeInfoB,
349             DiffsCategorizedByFilesPresent &DiffsByFilesPresent) {
350   SmallSet<std::string, 10> FuncNames;
351   for (const auto &FuncName : FuncNameToSizeInfoA.keys())
352     FuncNames.insert(FuncName.str());
353   for (const auto &FuncName : FuncNameToSizeInfoB.keys())
354     FuncNames.insert(FuncName.str());
355   for (const std::string &FuncName : FuncNames) {
356     const auto &SizeInfoA = FuncNameToSizeInfoA.lookup(FuncName);
357     const auto &SizeInfoB = FuncNameToSizeInfoB.lookup(FuncName);
358     FunctionDiff FuncDiff(FuncName, SizeInfoA, SizeInfoB);
359     DiffsByFilesPresent.addDiff(FuncDiff);
360   }
361 }
362 
363 /// Attempt to get the output stream for writing the diff.
364 static ErrorOr<std::unique_ptr<ToolOutputFile>> getOutputStream() {
365   if (OutputFilename == "")
366     OutputFilename = "-";
367   std::error_code EC;
368   auto Out = std::make_unique<ToolOutputFile>(OutputFilename, EC,
369                                               sys::fs::OF_TextWithCRLF);
370   if (!EC)
371     return std::move(Out);
372   return EC;
373 }
374 
375 /// \return a json::Array representing all FunctionDiffs in \p FunctionDiffs.
376 /// \p WhichFiles represents which files the functions in \p FunctionDiffs
377 /// appeared in (A, B, or both).
378 json::Array
379 getFunctionDiffListAsJSON(const SmallVector<FunctionDiff> &FunctionDiffs,
380                           const FilesPresent &WhichFiles) {
381   json::Array FunctionDiffsAsJSON;
382   int64_t InstCountA, InstCountB, StackSizeA, StackSizeB;
383   for (auto &Diff : FunctionDiffs) {
384     InstCountA = InstCountB = StackSizeA = StackSizeB = 0;
385     switch (WhichFiles) {
386     case BOTH:
387       [[fallthrough]];
388     case A:
389       InstCountA = Diff.getInstCountA();
390       StackSizeA = Diff.getStackSizeA();
391       if (WhichFiles != BOTH)
392         break;
393       [[fallthrough]];
394     case B:
395       InstCountB = Diff.getInstCountB();
396       StackSizeB = Diff.getStackSizeB();
397       break;
398     }
399     // Each metric we care about is represented like:
400     //   "Val": [A, B]
401     // This allows any consumer of the JSON to calculate the diff using B - A.
402     // This is somewhat wasteful for OnlyInA and OnlyInB (we only need A or B).
403     // However, this should make writing consuming tools easier, since the tool
404     // writer doesn't need to think about slightly different formats in each
405     // section.
406     json::Object FunctionObject({{"FunctionName", Diff.FuncName},
407                                  {"InstCount", {InstCountA, InstCountB}},
408                                  {"StackSize", {StackSizeA, StackSizeB}}});
409     FunctionDiffsAsJSON.push_back(std::move(FunctionObject));
410   }
411   return FunctionDiffsAsJSON;
412 }
413 
414 /// Output all diffs in \p DiffsByFilesPresent as a JSON report. This is
415 /// intended for consumption by external tools.
416 ///
417 /// \p InputFileNameA - File A used to produce the report.
418 /// \p InputFileNameB - File B used ot produce the report.
419 /// \p OS - Output stream.
420 ///
421 /// JSON output includes:
422 ///  - \p InputFileNameA and \p InputFileNameB under "Files".
423 ///  - Functions present in both files under "InBoth".
424 ///  - Functions present only in A in "OnlyInA".
425 ///  - Functions present only in B in "OnlyInB".
426 ///  - Instruction count and stack size differences for each function.
427 ///
428 /// Differences are represented using [count_a, count_b]. The actual difference
429 /// can be computed via count_b - count_a.
430 static void
431 outputJSONForAllDiffs(StringRef InputFileNameA, StringRef InputFileNameB,
432                       const DiffsCategorizedByFilesPresent &DiffsByFilesPresent,
433                       llvm::raw_ostream &OS) {
434   json::Object Output;
435   // Include file names in the report.
436   json::Object Files(
437       {{"A", InputFileNameA.str()}, {"B", InputFileNameB.str()}});
438   Output["Files"] = std::move(Files);
439   Output["OnlyInA"] = getFunctionDiffListAsJSON(DiffsByFilesPresent.OnlyInA, A);
440   Output["OnlyInB"] = getFunctionDiffListAsJSON(DiffsByFilesPresent.OnlyInB, B);
441   Output["InBoth"] =
442       getFunctionDiffListAsJSON(DiffsByFilesPresent.InBoth, BOTH);
443   json::OStream JOS(OS, PrettyPrint ? 2 : 0);
444   JOS.value(std::move(Output));
445   OS << '\n';
446 }
447 
448 /// Output all diffs in \p DiffsByFilesPresent using the desired output style.
449 /// \returns Error::success() on success, and an Error otherwise.
450 /// \p InputFileNameA - Name of input file A; may be used in the report.
451 /// \p InputFileNameB - Name of input file B; may be used in the report.
452 static Error
453 outputAllDiffs(StringRef InputFileNameA, StringRef InputFileNameB,
454                DiffsCategorizedByFilesPresent &DiffsByFilesPresent) {
455   auto MaybeOF = getOutputStream();
456   if (std::error_code EC = MaybeOF.getError())
457     return errorCodeToError(EC);
458   std::unique_ptr<ToolOutputFile> OF = std::move(*MaybeOF);
459   switch (ReportStyle) {
460   case human_output:
461     printDiffsCategorizedByFilesPresent(DiffsByFilesPresent, OF->os());
462     break;
463   case json_output:
464     outputJSONForAllDiffs(InputFileNameA, InputFileNameB, DiffsByFilesPresent,
465                           OF->os());
466     break;
467   }
468   OF->keep();
469   return Error::success();
470 }
471 
472 /// Boolean wrapper for outputDiff which handles errors.
473 static Error
474 tryOutputAllDiffs(StringRef InputFileNameA, StringRef InputFileNameB,
475                   DiffsCategorizedByFilesPresent &DiffsByFilesPresent) {
476   if (Error E =
477           outputAllDiffs(InputFileNameA, InputFileNameB, DiffsByFilesPresent)) {
478     return E;
479   }
480   return Error::success();
481 }
482 
483 static Error trySizeSiff() {
484   StringMap<InstCountAndStackSize> FuncNameToSizeInfoA;
485   StringMap<InstCountAndStackSize> FuncNameToSizeInfoB;
486   if (auto E =
487           tryReadFileAndProcessRemarks(InputFileNameA, FuncNameToSizeInfoA))
488     return E;
489   if (auto E =
490           tryReadFileAndProcessRemarks(InputFileNameB, FuncNameToSizeInfoB))
491     return E;
492   DiffsCategorizedByFilesPresent DiffsByFilesPresent;
493   computeDiff(FuncNameToSizeInfoA, FuncNameToSizeInfoB, DiffsByFilesPresent);
494   if (auto E = tryOutputAllDiffs(InputFileNameA, InputFileNameB,
495                                  DiffsByFilesPresent))
496     return E;
497   return Error::success();
498 }
499 
500 static CommandRegistration RemarkSizeSiffRegister(&RemarkSizeDiffUtil,
501                                                   trySizeSiff);