| //===- RawMemProfReader.cpp - Instrumented memory profiling reader --------===// |
| // |
| // 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 contains support for reading MemProf profiling data. |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include <algorithm> |
| #include <cstdint> |
| #include <memory> |
| #include <type_traits> |
| |
| #include "llvm/ADT/ArrayRef.h" |
| #include "llvm/ADT/DenseMap.h" |
| #include "llvm/ADT/SetVector.h" |
| #include "llvm/ADT/SmallVector.h" |
| #include "llvm/ADT/StringExtras.h" |
| #include "llvm/DebugInfo/DWARF/DWARFContext.h" |
| #include "llvm/DebugInfo/Symbolize/SymbolizableModule.h" |
| #include "llvm/DebugInfo/Symbolize/SymbolizableObjectFile.h" |
| #include "llvm/Object/Binary.h" |
| #include "llvm/Object/ELFObjectFile.h" |
| #include "llvm/Object/ObjectFile.h" |
| #include "llvm/ProfileData/InstrProf.h" |
| #include "llvm/ProfileData/MemProf.h" |
| #include "llvm/ProfileData/MemProfData.inc" |
| #include "llvm/ProfileData/RawMemProfReader.h" |
| #include "llvm/Support/Endian.h" |
| #include "llvm/Support/Path.h" |
| |
| #define DEBUG_TYPE "memprof" |
| |
| namespace llvm { |
| namespace memprof { |
| namespace { |
| template <class T = uint64_t> inline T alignedRead(const char *Ptr) { |
| static_assert(std::is_pod<T>::value, "Not a pod type."); |
| assert(reinterpret_cast<size_t>(Ptr) % sizeof(T) == 0 && "Unaligned Read"); |
| return *reinterpret_cast<const T *>(Ptr); |
| } |
| |
| Error checkBuffer(const MemoryBuffer &Buffer) { |
| if (!RawMemProfReader::hasFormat(Buffer)) |
| return make_error<InstrProfError>(instrprof_error::bad_magic); |
| |
| if (Buffer.getBufferSize() == 0) |
| return make_error<InstrProfError>(instrprof_error::empty_raw_profile); |
| |
| if (Buffer.getBufferSize() < sizeof(Header)) { |
| return make_error<InstrProfError>(instrprof_error::truncated); |
| } |
| |
| // The size of the buffer can be > header total size since we allow repeated |
| // serialization of memprof profiles to the same file. |
| uint64_t TotalSize = 0; |
| const char *Next = Buffer.getBufferStart(); |
| while (Next < Buffer.getBufferEnd()) { |
| auto *H = reinterpret_cast<const Header *>(Next); |
| if (H->Version != MEMPROF_RAW_VERSION) { |
| return make_error<InstrProfError>(instrprof_error::unsupported_version); |
| } |
| |
| TotalSize += H->TotalSize; |
| Next += H->TotalSize; |
| } |
| |
| if (Buffer.getBufferSize() != TotalSize) { |
| return make_error<InstrProfError>(instrprof_error::malformed); |
| } |
| return Error::success(); |
| } |
| |
| llvm::SmallVector<SegmentEntry> readSegmentEntries(const char *Ptr) { |
| using namespace support; |
| |
| const uint64_t NumItemsToRead = |
| endian::readNext<uint64_t, little, unaligned>(Ptr); |
| llvm::SmallVector<SegmentEntry> Items; |
| for (uint64_t I = 0; I < NumItemsToRead; I++) { |
| Items.push_back(*reinterpret_cast<const SegmentEntry *>( |
| Ptr + I * sizeof(SegmentEntry))); |
| } |
| return Items; |
| } |
| |
| llvm::SmallVector<std::pair<uint64_t, MemInfoBlock>> |
| readMemInfoBlocks(const char *Ptr) { |
| using namespace support; |
| |
| const uint64_t NumItemsToRead = |
| endian::readNext<uint64_t, little, unaligned>(Ptr); |
| llvm::SmallVector<std::pair<uint64_t, MemInfoBlock>> Items; |
| for (uint64_t I = 0; I < NumItemsToRead; I++) { |
| const uint64_t Id = endian::readNext<uint64_t, little, unaligned>(Ptr); |
| const MemInfoBlock MIB = *reinterpret_cast<const MemInfoBlock *>(Ptr); |
| Items.push_back({Id, MIB}); |
| // Only increment by size of MIB since readNext implicitly increments. |
| Ptr += sizeof(MemInfoBlock); |
| } |
| return Items; |
| } |
| |
| CallStackMap readStackInfo(const char *Ptr) { |
| using namespace support; |
| |
| const uint64_t NumItemsToRead = |
| endian::readNext<uint64_t, little, unaligned>(Ptr); |
| CallStackMap Items; |
| |
| for (uint64_t I = 0; I < NumItemsToRead; I++) { |
| const uint64_t StackId = endian::readNext<uint64_t, little, unaligned>(Ptr); |
| const uint64_t NumPCs = endian::readNext<uint64_t, little, unaligned>(Ptr); |
| |
| SmallVector<uint64_t> CallStack; |
| for (uint64_t J = 0; J < NumPCs; J++) { |
| CallStack.push_back(endian::readNext<uint64_t, little, unaligned>(Ptr)); |
| } |
| |
| Items[StackId] = CallStack; |
| } |
| return Items; |
| } |
| |
| // Merges the contents of stack information in \p From to \p To. Returns true if |
| // any stack ids observed previously map to a different set of program counter |
| // addresses. |
| bool mergeStackMap(const CallStackMap &From, CallStackMap &To) { |
| for (const auto &IdStack : From) { |
| auto I = To.find(IdStack.first); |
| if (I == To.end()) { |
| To[IdStack.first] = IdStack.second; |
| } else { |
| // Check that the PCs are the same (in order). |
| if (IdStack.second != I->second) |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| Error report(Error E, const StringRef Context) { |
| return joinErrors(createStringError(inconvertibleErrorCode(), Context), |
| std::move(E)); |
| } |
| |
| bool isRuntimePath(const StringRef Path) { |
| return StringRef(llvm::sys::path::convert_to_slash(Path)) |
| .contains("memprof/memprof_"); |
| } |
| |
| std::string getBuildIdString(const SegmentEntry &Entry) { |
| constexpr size_t Size = sizeof(Entry.BuildId) / sizeof(uint8_t); |
| constexpr uint8_t Zeros[Size] = {0}; |
| // If the build id is unset print a helpful string instead of all zeros. |
| if (memcmp(Entry.BuildId, Zeros, Size) == 0) |
| return "<None>"; |
| |
| std::string Str; |
| raw_string_ostream OS(Str); |
| for (size_t I = 0; I < Size; I++) { |
| OS << format_hex_no_prefix(Entry.BuildId[I], 2); |
| } |
| return OS.str(); |
| } |
| } // namespace |
| |
| Expected<std::unique_ptr<RawMemProfReader>> |
| RawMemProfReader::create(const Twine &Path, const StringRef ProfiledBinary, |
| bool KeepName) { |
| auto BufferOr = MemoryBuffer::getFileOrSTDIN(Path); |
| if (std::error_code EC = BufferOr.getError()) |
| return report(errorCodeToError(EC), Path.getSingleStringRef()); |
| |
| std::unique_ptr<MemoryBuffer> Buffer(BufferOr.get().release()); |
| if (Error E = checkBuffer(*Buffer)) |
| return report(std::move(E), Path.getSingleStringRef()); |
| |
| if (ProfiledBinary.empty()) |
| return report( |
| errorCodeToError(make_error_code(std::errc::invalid_argument)), |
| "Path to profiled binary is empty!"); |
| |
| auto BinaryOr = llvm::object::createBinary(ProfiledBinary); |
| if (!BinaryOr) { |
| return report(BinaryOr.takeError(), ProfiledBinary); |
| } |
| |
| // Use new here since constructor is private. |
| std::unique_ptr<RawMemProfReader> Reader( |
| new RawMemProfReader(std::move(BinaryOr.get()), KeepName)); |
| if (Error E = Reader->initialize(std::move(Buffer))) { |
| return std::move(E); |
| } |
| return std::move(Reader); |
| } |
| |
| bool RawMemProfReader::hasFormat(const StringRef Path) { |
| auto BufferOr = MemoryBuffer::getFileOrSTDIN(Path); |
| if (!BufferOr) |
| return false; |
| |
| std::unique_ptr<MemoryBuffer> Buffer(BufferOr.get().release()); |
| return hasFormat(*Buffer); |
| } |
| |
| bool RawMemProfReader::hasFormat(const MemoryBuffer &Buffer) { |
| if (Buffer.getBufferSize() < sizeof(uint64_t)) |
| return false; |
| // Aligned read to sanity check that the buffer was allocated with at least 8b |
| // alignment. |
| const uint64_t Magic = alignedRead(Buffer.getBufferStart()); |
| return Magic == MEMPROF_RAW_MAGIC_64; |
| } |
| |
| void RawMemProfReader::printYAML(raw_ostream &OS) { |
| uint64_t NumAllocFunctions = 0, NumMibInfo = 0; |
| for (const auto &KV : FunctionProfileData) { |
| const size_t NumAllocSites = KV.second.AllocSites.size(); |
| if (NumAllocSites > 0) { |
| NumAllocFunctions++; |
| NumMibInfo += NumAllocSites; |
| } |
| } |
| |
| OS << "MemprofProfile:\n"; |
| OS << " Summary:\n"; |
| OS << " Version: " << MEMPROF_RAW_VERSION << "\n"; |
| OS << " NumSegments: " << SegmentInfo.size() << "\n"; |
| OS << " NumMibInfo: " << NumMibInfo << "\n"; |
| OS << " NumAllocFunctions: " << NumAllocFunctions << "\n"; |
| OS << " NumStackOffsets: " << StackMap.size() << "\n"; |
| // Print out the segment information. |
| OS << " Segments:\n"; |
| for (const auto &Entry : SegmentInfo) { |
| OS << " -\n"; |
| OS << " BuildId: " << getBuildIdString(Entry) << "\n"; |
| OS << " Start: 0x" << llvm::utohexstr(Entry.Start) << "\n"; |
| OS << " End: 0x" << llvm::utohexstr(Entry.End) << "\n"; |
| OS << " Offset: 0x" << llvm::utohexstr(Entry.Offset) << "\n"; |
| } |
| // Print out the merged contents of the profiles. |
| OS << " Records:\n"; |
| for (const auto &Entry : *this) { |
| OS << " -\n"; |
| OS << " FunctionGUID: " << Entry.first << "\n"; |
| Entry.second.print(OS); |
| } |
| } |
| |
| Error RawMemProfReader::initialize(std::unique_ptr<MemoryBuffer> DataBuffer) { |
| const StringRef FileName = Binary.getBinary()->getFileName(); |
| |
| auto *ElfObject = dyn_cast<object::ELFObjectFileBase>(Binary.getBinary()); |
| if (!ElfObject) { |
| return report(make_error<StringError>(Twine("Not an ELF file: "), |
| inconvertibleErrorCode()), |
| FileName); |
| } |
| |
| // Check whether the profiled binary was built with position independent code |
| // (PIC). For now we provide a error message until symbolization support |
| // is added for pic. |
| auto* Elf64LEObject = llvm::cast<llvm::object::ELF64LEObjectFile>(ElfObject); |
| const llvm::object::ELF64LEFile& ElfFile = Elf64LEObject->getELFFile(); |
| auto PHdrsOr = ElfFile.program_headers(); |
| if(!PHdrsOr) |
| return report(make_error<StringError>(Twine("Could not read program headers: "), |
| inconvertibleErrorCode()), |
| FileName); |
| auto FirstLoadHeader = PHdrsOr->begin(); |
| while (FirstLoadHeader->p_type != llvm::ELF::PT_LOAD) |
| ++FirstLoadHeader; |
| if(FirstLoadHeader->p_vaddr == 0) |
| return report(make_error<StringError>(Twine("Unsupported position independent code"), |
| inconvertibleErrorCode()), |
| FileName); |
| |
| auto Triple = ElfObject->makeTriple(); |
| if (!Triple.isX86()) |
| return report(make_error<StringError>(Twine("Unsupported target: ") + |
| Triple.getArchName(), |
| inconvertibleErrorCode()), |
| FileName); |
| |
| auto *Object = cast<object::ObjectFile>(Binary.getBinary()); |
| std::unique_ptr<DIContext> Context = DWARFContext::create( |
| *Object, DWARFContext::ProcessDebugRelocations::Process); |
| |
| auto SOFOr = symbolize::SymbolizableObjectFile::create( |
| Object, std::move(Context), /*UntagAddresses=*/false); |
| if (!SOFOr) |
| return report(SOFOr.takeError(), FileName); |
| Symbolizer = std::move(SOFOr.get()); |
| |
| if (Error E = readRawProfile(std::move(DataBuffer))) |
| return E; |
| |
| if (Error E = symbolizeAndFilterStackFrames()) |
| return E; |
| |
| return mapRawProfileToRecords(); |
| } |
| |
| Error RawMemProfReader::mapRawProfileToRecords() { |
| // Hold a mapping from function to each callsite location we encounter within |
| // it that is part of some dynamic allocation context. The location is stored |
| // as a pointer to a symbolized list of inline frames. |
| using LocationPtr = const llvm::SmallVector<FrameId> *; |
| llvm::MapVector<GlobalValue::GUID, llvm::SetVector<LocationPtr>> |
| PerFunctionCallSites; |
| |
| // Convert the raw profile callstack data into memprof records. While doing so |
| // keep track of related contexts so that we can fill these in later. |
| for (const auto &Entry : CallstackProfileData) { |
| const uint64_t StackId = Entry.first; |
| |
| auto It = StackMap.find(StackId); |
| if (It == StackMap.end()) |
| return make_error<InstrProfError>( |
| instrprof_error::malformed, |
| "memprof callstack record does not contain id: " + Twine(StackId)); |
| |
| // Construct the symbolized callstack. |
| llvm::SmallVector<FrameId> Callstack; |
| Callstack.reserve(It->getSecond().size()); |
| |
| llvm::ArrayRef<uint64_t> Addresses = It->getSecond(); |
| for (size_t I = 0; I < Addresses.size(); I++) { |
| const uint64_t Address = Addresses[I]; |
| assert(SymbolizedFrame.count(Address) > 0 && |
| "Address not found in SymbolizedFrame map"); |
| const SmallVector<FrameId> &Frames = SymbolizedFrame[Address]; |
| |
| assert(!idToFrame(Frames.back()).IsInlineFrame && |
| "The last frame should not be inlined"); |
| |
| // Record the callsites for each function. Skip the first frame of the |
| // first address since it is the allocation site itself that is recorded |
| // as an alloc site. |
| for (size_t J = 0; J < Frames.size(); J++) { |
| if (I == 0 && J == 0) |
| continue; |
| // We attach the entire bottom-up frame here for the callsite even |
| // though we only need the frames up to and including the frame for |
| // Frames[J].Function. This will enable better deduplication for |
| // compression in the future. |
| const GlobalValue::GUID Guid = idToFrame(Frames[J]).Function; |
| PerFunctionCallSites[Guid].insert(&Frames); |
| } |
| |
| // Add all the frames to the current allocation callstack. |
| Callstack.append(Frames.begin(), Frames.end()); |
| } |
| |
| // We attach the memprof record to each function bottom-up including the |
| // first non-inline frame. |
| for (size_t I = 0; /*Break out using the condition below*/; I++) { |
| const Frame &F = idToFrame(Callstack[I]); |
| auto Result = |
| FunctionProfileData.insert({F.Function, IndexedMemProfRecord()}); |
| IndexedMemProfRecord &Record = Result.first->second; |
| Record.AllocSites.emplace_back(Callstack, Entry.second); |
| |
| if (!F.IsInlineFrame) |
| break; |
| } |
| } |
| |
| // Fill in the related callsites per function. |
| for (const auto &[Id, Locs] : PerFunctionCallSites) { |
| // Some functions may have only callsite data and no allocation data. Here |
| // we insert a new entry for callsite data if we need to. |
| auto Result = FunctionProfileData.insert({Id, IndexedMemProfRecord()}); |
| IndexedMemProfRecord &Record = Result.first->second; |
| for (LocationPtr Loc : Locs) { |
| Record.CallSites.push_back(*Loc); |
| } |
| } |
| |
| return Error::success(); |
| } |
| |
| Error RawMemProfReader::symbolizeAndFilterStackFrames() { |
| // The specifier to use when symbolization is requested. |
| const DILineInfoSpecifier Specifier( |
| DILineInfoSpecifier::FileLineInfoKind::RawValue, |
| DILineInfoSpecifier::FunctionNameKind::LinkageName); |
| |
| // For entries where all PCs in the callstack are discarded, we erase the |
| // entry from the stack map. |
| llvm::SmallVector<uint64_t> EntriesToErase; |
| // We keep track of all prior discarded entries so that we can avoid invoking |
| // the symbolizer for such entries. |
| llvm::DenseSet<uint64_t> AllVAddrsToDiscard; |
| for (auto &Entry : StackMap) { |
| for (const uint64_t VAddr : Entry.getSecond()) { |
| // Check if we have already symbolized and cached the result or if we |
| // don't want to attempt symbolization since we know this address is bad. |
| // In this case the address is also removed from the current callstack. |
| if (SymbolizedFrame.count(VAddr) > 0 || |
| AllVAddrsToDiscard.contains(VAddr)) |
| continue; |
| |
| Expected<DIInliningInfo> DIOr = Symbolizer->symbolizeInlinedCode( |
| getModuleOffset(VAddr), Specifier, /*UseSymbolTable=*/false); |
| if (!DIOr) |
| return DIOr.takeError(); |
| DIInliningInfo DI = DIOr.get(); |
| |
| // Drop frames which we can't symbolize or if they belong to the runtime. |
| if (DI.getFrame(0).FunctionName == DILineInfo::BadString || |
| isRuntimePath(DI.getFrame(0).FileName)) { |
| AllVAddrsToDiscard.insert(VAddr); |
| continue; |
| } |
| |
| for (size_t I = 0, NumFrames = DI.getNumberOfFrames(); I < NumFrames; |
| I++) { |
| const auto &DIFrame = DI.getFrame(I); |
| const uint64_t Guid = |
| IndexedMemProfRecord::getGUID(DIFrame.FunctionName); |
| const Frame F(Guid, DIFrame.Line - DIFrame.StartLine, DIFrame.Column, |
| // Only the last entry is not an inlined location. |
| I != NumFrames - 1); |
| // Here we retain a mapping from the GUID to symbol name instead of |
| // adding it to the frame object directly to reduce memory overhead. |
| // This is because there can be many unique frames, particularly for |
| // callsite frames. |
| if (KeepSymbolName) |
| GuidToSymbolName.insert({Guid, DIFrame.FunctionName}); |
| |
| const FrameId Hash = F.hash(); |
| IdToFrame.insert({Hash, F}); |
| SymbolizedFrame[VAddr].push_back(Hash); |
| } |
| } |
| |
| auto &CallStack = Entry.getSecond(); |
| llvm::erase_if(CallStack, [&AllVAddrsToDiscard](const uint64_t A) { |
| return AllVAddrsToDiscard.contains(A); |
| }); |
| if (CallStack.empty()) |
| EntriesToErase.push_back(Entry.getFirst()); |
| } |
| |
| // Drop the entries where the callstack is empty. |
| for (const uint64_t Id : EntriesToErase) { |
| StackMap.erase(Id); |
| CallstackProfileData.erase(Id); |
| } |
| |
| if (StackMap.empty()) |
| return make_error<InstrProfError>( |
| instrprof_error::malformed, |
| "no entries in callstack map after symbolization"); |
| |
| return Error::success(); |
| } |
| |
| Error RawMemProfReader::readRawProfile( |
| std::unique_ptr<MemoryBuffer> DataBuffer) { |
| const char *Next = DataBuffer->getBufferStart(); |
| |
| while (Next < DataBuffer->getBufferEnd()) { |
| auto *Header = reinterpret_cast<const memprof::Header *>(Next); |
| |
| // Read in the segment information, check whether its the same across all |
| // profiles in this binary file. |
| const llvm::SmallVector<SegmentEntry> Entries = |
| readSegmentEntries(Next + Header->SegmentOffset); |
| if (!SegmentInfo.empty() && SegmentInfo != Entries) { |
| // We do not expect segment information to change when deserializing from |
| // the same binary profile file. This can happen if dynamic libraries are |
| // loaded/unloaded between profile dumping. |
| return make_error<InstrProfError>( |
| instrprof_error::malformed, |
| "memprof raw profile has different segment information"); |
| } |
| SegmentInfo.assign(Entries.begin(), Entries.end()); |
| |
| // Read in the MemInfoBlocks. Merge them based on stack id - we assume that |
| // raw profiles in the same binary file are from the same process so the |
| // stackdepot ids are the same. |
| for (const auto &Value : readMemInfoBlocks(Next + Header->MIBOffset)) { |
| if (CallstackProfileData.count(Value.first)) { |
| CallstackProfileData[Value.first].Merge(Value.second); |
| } else { |
| CallstackProfileData[Value.first] = Value.second; |
| } |
| } |
| |
| // Read in the callstack for each ids. For multiple raw profiles in the same |
| // file, we expect that the callstack is the same for a unique id. |
| const CallStackMap CSM = readStackInfo(Next + Header->StackOffset); |
| if (StackMap.empty()) { |
| StackMap = CSM; |
| } else { |
| if (mergeStackMap(CSM, StackMap)) |
| return make_error<InstrProfError>( |
| instrprof_error::malformed, |
| "memprof raw profile got different call stack for same id"); |
| } |
| |
| Next += Header->TotalSize; |
| } |
| |
| return Error::success(); |
| } |
| |
| object::SectionedAddress |
| RawMemProfReader::getModuleOffset(const uint64_t VirtualAddress) { |
| LLVM_DEBUG({ |
| SegmentEntry *ContainingSegment = nullptr; |
| for (auto &SE : SegmentInfo) { |
| if (VirtualAddress > SE.Start && VirtualAddress <= SE.End) { |
| ContainingSegment = &SE; |
| } |
| } |
| |
| // Ensure that the virtual address is valid. |
| assert(ContainingSegment && "Could not find a segment entry"); |
| }); |
| |
| // TODO: Compute the file offset based on the maps and program headers. For |
| // now this only works for non PIE binaries. |
| return object::SectionedAddress{VirtualAddress}; |
| } |
| |
| Error RawMemProfReader::readNextRecord(GuidMemProfRecordPair &GuidRecord) { |
| if (FunctionProfileData.empty()) |
| return make_error<InstrProfError>(instrprof_error::empty_raw_profile); |
| |
| if (Iter == FunctionProfileData.end()) |
| return make_error<InstrProfError>(instrprof_error::eof); |
| |
| auto IdToFrameCallback = [this](const FrameId Id) { |
| Frame F = this->idToFrame(Id); |
| if (!this->KeepSymbolName) |
| return F; |
| auto Iter = this->GuidToSymbolName.find(F.Function); |
| assert(Iter != this->GuidToSymbolName.end()); |
| F.SymbolName = Iter->getSecond(); |
| return F; |
| }; |
| |
| const IndexedMemProfRecord &IndexedRecord = Iter->second; |
| GuidRecord = {Iter->first, MemProfRecord(IndexedRecord, IdToFrameCallback)}; |
| Iter++; |
| return Error::success(); |
| } |
| } // namespace memprof |
| } // namespace llvm |