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);