| //===- bolt/Profile/Heatmap.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 | |
| // | |
| //===----------------------------------------------------------------------===// | |
| #include "bolt/Profile/Heatmap.h" | |
| #include "bolt/Utils/CommandLineOpts.h" | |
| #include "llvm/ADT/AddressRanges.h" | |
| #include "llvm/ADT/StringMap.h" | |
| #include "llvm/ADT/Twine.h" | |
| #include "llvm/Support/Debug.h" | |
| #include "llvm/Support/FileSystem.h" | |
| #include "llvm/Support/Format.h" | |
| #include "llvm/Support/FormatVariadic.h" | |
| #include "llvm/Support/MathExtras.h" | |
| #include "llvm/Support/raw_ostream.h" | |
| #include <algorithm> | |
| #include <cctype> | |
| #include <cmath> | |
| #include <vector> | |
| #define DEBUG_TYPE "bolt-heatmap" | |
| using namespace llvm; | |
| namespace llvm { | |
| namespace bolt { | |
| void Heatmap::registerAddressRange(uint64_t StartAddress, uint64_t EndAddress, | |
| uint64_t Count) { | |
| if (ignoreAddress(StartAddress)) { | |
| ++NumSkippedRanges; | |
| return; | |
| } | |
| if (StartAddress > EndAddress || EndAddress - StartAddress > 64 * 1024) { | |
| LLVM_DEBUG(dbgs() << "invalid range : 0x" << Twine::utohexstr(StartAddress) | |
| << " -> 0x" << Twine::utohexstr(EndAddress) << '\n'); | |
| ++NumSkippedRanges; | |
| return; | |
| } | |
| for (uint64_t Bucket = StartAddress / BucketSize; | |
| Bucket <= EndAddress / BucketSize; ++Bucket) | |
| Map[Bucket] += Count; | |
| } | |
| void Heatmap::print(StringRef FileName) const { | |
| std::error_code EC; | |
| raw_fd_ostream OS(FileName, EC, sys::fs::OpenFlags::OF_None); | |
| if (EC) { | |
| errs() << "error opening output file: " << EC.message() << '\n'; | |
| exit(1); | |
| } | |
| outs() << "HEATMAP: dumping heatmap with bucket size " << BucketSize << " to " | |
| << FileName << '\n'; | |
| print(OS); | |
| } | |
| void Heatmap::print(raw_ostream &OS) const { | |
| const char FillChar = '.'; | |
| const auto DefaultColor = raw_ostream::WHITE; | |
| auto changeColor = [&](raw_ostream::Colors Color) -> void { | |
| static auto CurrentColor = raw_ostream::BLACK; | |
| if (CurrentColor == Color) | |
| return; | |
| OS.changeColor(Color); | |
| CurrentColor = Color; | |
| }; | |
| const uint64_t BytesPerLine = opts::BucketsPerLine * BucketSize; | |
| // Calculate the max value for scaling. | |
| uint64_t MaxValue = 0; | |
| for (const std::pair<const uint64_t, uint64_t> &Entry : Map) | |
| MaxValue = std::max<uint64_t>(MaxValue, Entry.second); | |
| // Print start of the line and fill it with an empty space right before | |
| // the Address. | |
| auto startLine = [&](uint64_t Address, bool Empty = false) { | |
| changeColor(DefaultColor); | |
| const uint64_t LineAddress = Address / BytesPerLine * BytesPerLine; | |
| if (MaxAddress > 0xffffffff) | |
| OS << format("0x%016" PRIx64 ": ", LineAddress); | |
| else | |
| OS << format("0x%08" PRIx64 ": ", LineAddress); | |
| if (Empty) | |
| Address = LineAddress + BytesPerLine; | |
| for (uint64_t Fill = LineAddress; Fill < Address; Fill += BucketSize) | |
| OS << FillChar; | |
| }; | |
| // Finish line after \p Address was printed. | |
| auto finishLine = [&](uint64_t Address) { | |
| const uint64_t End = alignTo(Address + 1, BytesPerLine); | |
| for (uint64_t Fill = Address + BucketSize; Fill < End; Fill += BucketSize) | |
| OS << FillChar; | |
| OS << '\n'; | |
| }; | |
| // Fill empty space in (Start, End) range. | |
| auto fillRange = [&](uint64_t Start, uint64_t End) { | |
| if ((Start / BytesPerLine) == (End / BytesPerLine)) { | |
| for (uint64_t Fill = Start + BucketSize; Fill < End; Fill += BucketSize) { | |
| changeColor(DefaultColor); | |
| OS << FillChar; | |
| } | |
| return; | |
| } | |
| changeColor(DefaultColor); | |
| finishLine(Start); | |
| Start = alignTo(Start + BucketSize, BytesPerLine); | |
| uint64_t NumEmptyLines = (End - Start) / BytesPerLine; | |
| if (NumEmptyLines > 32) { | |
| OS << '\n'; | |
| } else { | |
| while (NumEmptyLines--) { | |
| startLine(Start, /*Empty=*/true); | |
| OS << '\n'; | |
| Start += BytesPerLine; | |
| } | |
| } | |
| startLine(End); | |
| }; | |
| static raw_ostream::Colors Colors[] = { | |
| raw_ostream::WHITE, raw_ostream::WHITE, raw_ostream::CYAN, | |
| raw_ostream::GREEN, raw_ostream::YELLOW, raw_ostream::RED}; | |
| constexpr size_t NumRanges = sizeof(Colors) / sizeof(Colors[0]); | |
| uint64_t Range[NumRanges]; | |
| for (uint64_t I = 0; I < NumRanges; ++I) | |
| Range[I] = std::max(I + 1, (uint64_t)std::pow((double)MaxValue, | |
| (double)(I + 1) / NumRanges)); | |
| Range[NumRanges - 1] = std::max((uint64_t)NumRanges, MaxValue); | |
| // Print scaled value | |
| auto printValue = [&](uint64_t Value, char Character, bool ResetColor) { | |
| assert(Value && "should only print positive values"); | |
| for (unsigned I = 0; I < sizeof(Range) / sizeof(Range[0]); ++I) { | |
| if (Value <= Range[I]) { | |
| changeColor(Colors[I]); | |
| break; | |
| } | |
| } | |
| if (Value <= Range[0]) | |
| OS << static_cast<char>(std::tolower(Character)); | |
| else | |
| OS << static_cast<char>(std::toupper(Character)); | |
| if (ResetColor) | |
| changeColor(DefaultColor); | |
| }; | |
| // Print against black background | |
| OS.changeColor(raw_ostream::BLACK, /*Bold=*/false, /*Background=*/true); | |
| changeColor(DefaultColor); | |
| // Print map legend | |
| OS << "Legend:\n"; | |
| OS << "\nRanges:\n"; | |
| uint64_t PrevValue = 0; | |
| for (unsigned I = 0; I < sizeof(Range) / sizeof(Range[0]); ++I) { | |
| const uint64_t Value = Range[I]; | |
| OS << " "; | |
| printValue(Value, 'o', /*ResetColor=*/true); | |
| OS << " : (" << PrevValue << ", " << Value << "]\n"; | |
| PrevValue = Value; | |
| } | |
| if (opts::HeatmapPrintMappings) { | |
| OS << "\nSections:\n"; | |
| unsigned SectionIdx = 0; | |
| for (auto TxtSeg : TextSections) { | |
| const char Upper = static_cast<char>('A' + ((SectionIdx++) % 26)); | |
| const char Lower = static_cast<char>(std::tolower(Upper)); | |
| OS << formatv(" {0}/{1} : {2,-10} ", Lower, Upper, TxtSeg.Name); | |
| if (MaxAddress > 0xffffffff) | |
| OS << format("0x%016" PRIx64, TxtSeg.BeginAddress) << "-" | |
| << format("0x%016" PRIx64, TxtSeg.EndAddress) << "\n"; | |
| else | |
| OS << format("0x%08" PRIx64, TxtSeg.BeginAddress) << "-" | |
| << format("0x%08" PRIx64, TxtSeg.EndAddress) << "\n"; | |
| } | |
| OS << "\n"; | |
| } | |
| // Pos - character position from right in hex form. | |
| auto printHeader = [&](unsigned Pos) { | |
| OS << " "; | |
| if (MaxAddress > 0xffffffff) | |
| OS << " "; | |
| unsigned PrevValue = unsigned(-1); | |
| for (unsigned I = 0; I < BytesPerLine; I += BucketSize) { | |
| const unsigned Value = (I & ((1 << Pos * 4) - 1)) >> (Pos - 1) * 4; | |
| if (Value != PrevValue) { | |
| OS << Twine::utohexstr(Value); | |
| PrevValue = Value; | |
| } else { | |
| OS << ' '; | |
| } | |
| } | |
| OS << '\n'; | |
| }; | |
| for (unsigned I = 5; I > 0; --I) | |
| printHeader(I); | |
| auto SectionStart = TextSections.begin(); | |
| uint64_t PrevAddress = 0; | |
| bool IsFirst = true; | |
| for (auto MI = Map.begin(), ME = Map.end(); MI != ME; ++MI) { | |
| const std::pair<const uint64_t, uint64_t> &Entry = *MI; | |
| uint64_t Address = Entry.first * BucketSize; | |
| char Character = 'o'; | |
| // Check if address is in the current or any later section. | |
| auto Section = std::find_if( | |
| SectionStart, TextSections.end(), [&](const SectionNameAndRange &S) { | |
| return Address >= S.BeginAddress && Address < S.EndAddress; | |
| }); | |
| if (Section != TextSections.end()) { | |
| // Shift the section forward (if SectionStart is different from Section). | |
| // This works, because TextSections is sorted by start address. | |
| SectionStart = Section; | |
| Character = 'a' + ((Section - TextSections.begin()) % 26); | |
| } | |
| if (IsFirst) | |
| startLine(Address); | |
| else | |
| fillRange(PrevAddress, Address); | |
| printValue(Entry.second, Character, /*ResetColor=*/false); | |
| PrevAddress = Address; | |
| IsFirst = false; | |
| } | |
| if (!IsFirst) { | |
| changeColor(DefaultColor); | |
| finishLine(PrevAddress); | |
| } | |
| } | |
| void Heatmap::printCDF(StringRef FileName, StringRef Label) const { | |
| std::error_code EC; | |
| raw_fd_ostream OS(FileName, EC, sys::fs::OpenFlags::OF_None); | |
| if (EC) { | |
| errs() << "error opening output file: " << EC.message() << '\n'; | |
| exit(1); | |
| } | |
| printCDF(OS, Label); | |
| } | |
| void Heatmap::printCDF(raw_ostream &OS, StringRef Label) const { | |
| uint64_t NumTotalCounts = 0; | |
| std::vector<uint64_t> Counts; | |
| for (const std::pair<const uint64_t, uint64_t> &KV : Map) { | |
| Counts.push_back(KV.second); | |
| NumTotalCounts += KV.second; | |
| } | |
| llvm::sort(Counts, std::greater<uint64_t>()); | |
| double RatioLeftInKB = (1.0 * BucketSize) / 1024; | |
| assert(NumTotalCounts > 0 && | |
| "total number of heatmap buckets should be greater than 0"); | |
| double RatioRightInPercent = 100.0 / NumTotalCounts; | |
| uint64_t RunningCount = 0; | |
| // Buckets covering the cutoff share of the samples. | |
| const uint64_t CutOff = opts::HeatmapCdfPct; | |
| assert(CutOff <= 1000000 && "cutoff must be at most 1000000"); | |
| const uint64_t Target = (NumTotalCounts * CutOff) / 1000000; | |
| uint64_t NumBuckets = 0; | |
| OS << "Bucket counts, Size (KB), CDF (%)\n"; | |
| for (uint64_t I = 0; I < Counts.size(); I++) { | |
| RunningCount += Counts[I]; | |
| if (!NumBuckets && RunningCount >= Target) | |
| NumBuckets = I + 1; | |
| OS << format("%llu", (I + 1)) << ", " | |
| << format("%.4f", RatioLeftInKB * (I + 1)) << ", " | |
| << format("%.4f", RatioRightInPercent * (RunningCount)) << "\n"; | |
| } | |
| outs() << "HEATMAP: working set @ bucket size " << Label << " p" | |
| << format("%g", CutOff / 10000.0) << "/total: " << NumBuckets << "/" | |
| << Counts.size() << '\n'; | |
| } | |
| void Heatmap::printSectionHotness(StringRef FileName) const { | |
| std::error_code EC; | |
| raw_fd_ostream OS(FileName, EC, sys::fs::OpenFlags::OF_None); | |
| if (EC) { | |
| errs() << "error opening output file: " << EC.message() << '\n'; | |
| exit(1); | |
| } | |
| printSectionHotness(OS); | |
| } | |
| void Heatmap::printSectionHotness(raw_ostream &OS) const { | |
| uint64_t NumTotalCounts = 0; | |
| StringMap<uint64_t> SectionHotness; | |
| StringMap<uint64_t> BucketUtilization; | |
| unsigned TextSectionIndex = 0; | |
| if (TextSections.empty()) | |
| return; | |
| uint64_t UnmappedHotness = 0; | |
| auto RecordUnmappedBucket = [&](uint64_t Address, uint64_t Frequency) { | |
| if (opts::Verbosity >= 1) | |
| errs() << "Couldn't map the address bucket [0x" | |
| << Twine::utohexstr(Address) << ", 0x" | |
| << Twine::utohexstr(Address + BucketSize) << "] containing " | |
| << Frequency << " samples to a text section in the binary."; | |
| UnmappedHotness += Frequency; | |
| }; | |
| AddressRange HotTextRange(HotStart, HotEnd); | |
| StringRef HotTextName = "[hot text]"; | |
| for (const std::pair<const uint64_t, uint64_t> &KV : Map) { | |
| NumTotalCounts += KV.second; | |
| // We map an address bucket to the first section (lowest address) | |
| // overlapping with that bucket. | |
| auto Address = KV.first * BucketSize; | |
| while (TextSectionIndex < TextSections.size() && | |
| Address >= TextSections[TextSectionIndex].EndAddress) | |
| TextSectionIndex++; | |
| if (TextSectionIndex >= TextSections.size() || | |
| Address + BucketSize < TextSections[TextSectionIndex].BeginAddress) { | |
| RecordUnmappedBucket(Address, KV.second); | |
| continue; | |
| } | |
| SectionHotness[TextSections[TextSectionIndex].Name] += KV.second; | |
| ++BucketUtilization[TextSections[TextSectionIndex].Name]; | |
| if (HotTextRange.contains(Address)) { | |
| SectionHotness[HotTextName] += KV.second; | |
| ++BucketUtilization[HotTextName]; | |
| } | |
| } | |
| std::vector<SectionNameAndRange> Sections(TextSections); | |
| // Append synthetic hot text section to TextSections | |
| if (!HotTextRange.empty()) | |
| Sections.emplace_back(SectionNameAndRange{HotTextName, HotStart, HotEnd}); | |
| assert(NumTotalCounts > 0 && | |
| "total number of heatmap buckets should be greater than 0"); | |
| OS << "Section Name, Begin Address, End Address, Percentage Hotness, " | |
| << "Utilization Pct, Partition Score\n"; | |
| const uint64_t MappedCounts = NumTotalCounts - UnmappedHotness; | |
| for (const auto [Name, Begin, End] : Sections) { | |
| const float Hotness = 1. * SectionHotness[Name] / NumTotalCounts; | |
| const float MappedHotness = | |
| MappedCounts ? 1. * SectionHotness[Name] / MappedCounts : 0; | |
| const uint64_t NumBuckets = | |
| End / BucketSize + !!(End % BucketSize) - Begin / BucketSize; | |
| const float Utilization = 1. * BucketUtilization[Name] / NumBuckets; | |
| const float PartitionScore = MappedHotness * Utilization; | |
| OS << formatv("{0}, {1:x}, {2:x}, {3:f4}, {4:f4}, {5:f4}\n", Name, Begin, | |
| End, 100. * Hotness, 100. * Utilization, PartitionScore); | |
| } | |
| if (UnmappedHotness > 0) | |
| OS << formatv("[unmapped], 0x0, 0x0, {0:f4}, 0, 0\n", | |
| 100.0 * UnmappedHotness / NumTotalCounts); | |
| } | |
| void Heatmap::resizeBucket(uint64_t NewSize) { | |
| std::map<uint64_t, uint64_t> NewMap; | |
| for (const auto [Bucket, Count] : Map) | |
| NewMap[Bucket * BucketSize / NewSize] += Count; | |
| Map = NewMap; | |
| BucketSize = NewSize; | |
| } | |
| } // namespace bolt | |
| } // namespace llvm |