[llvm] [ProfileData] Preserve cross-image targets in concatenated raw profiles (PR #212715)
via llvm-commits
llvm-commits at lists.llvm.org
Sun Aug 2 05:53:19 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-pgo
Author: Karim Alweheshy (karim-alweheshy)
<details>
<summary>Changes</summary>
## Summary
- Build one raw-profile symbol table from every concatenated header before deserializing value records.
- Preserve indirect-call and vtable targets whose addresses belong to another image in the same raw profile buffer.
- Keep the header pre-scan lightweight by reading value-record sizes without allocating or copying their payloads.
## Problem
An instrumented process can write one `.profraw` buffer containing concatenated profiles from multiple images. `RawInstrProfReader` currently creates or replaces the symbol table as each header is entered. Records from image A are deserialized before header B is entered, so a value-profile target whose address belongs to B cannot be translated into B's name hash. When header B is entered, mappings from A are replaced as well. Cross-image targets become external/zero and are lost during merge.
This prevents downstream ICP and WPD from seeing valid cross-image targets even though the relevant function or vtable address/name mappings are present in the same raw buffer.
## Fix
Pre-scan the concatenated headers, accumulating function and vtable address mappings into one symbol table. The scan advances through value data using validated `TotalSize` fields, restores the first header's iteration state, and then preserves normal record iteration for valid profiles. Binary IDs are collected once.
Malformed data in a later concatenated profile may now be diagnosed during the initial pre-scan rather than after records from earlier profiles have been returned.
No raw profile format change is required.
## Scope
This change covers concatenated raw profiles that carry inline profile data/name mappings. The existing limitation around externally correlating one raw-profile buffer containing multiple build IDs is unchanged.
## Testing
- Extended `raw-two-profiles.test`:
- header A owns `foo`, an indirect-call target at `0x2000`, and a vtable target at `0x3008`;
- header B alone maps `0x2000` to `bar` and maps `[0x3000, 0x3010)` to `_ZTV1A`;
- the merged targets must be `bar` with count 7 and `_ZTV1A` with count 9;
- a malformed value-profile `TotalSize` is rejected during the pre-scan.
- `ninja -C /tmp/llvm-pr-212715-build check-llvm-tools-llvm-profdata`: 102 discovered, 88 passed, 14 unsupported.
- `ProfileDataTests`: 255/255 passed.
---
Full diff: https://github.com/llvm/llvm-project/pull/212715.diff
3 Files Affected:
- (modified) llvm/include/llvm/ProfileData/InstrProfReader.h (+5-1)
- (modified) llvm/lib/ProfileData/InstrProfReader.cpp (+78-15)
- (modified) llvm/test/tools/llvm-profdata/raw-two-profiles.test (+32-8)
``````````diff
diff --git a/llvm/include/llvm/ProfileData/InstrProfReader.h b/llvm/include/llvm/ProfileData/InstrProfReader.h
index 51b97e9f88ee3..776c3ab859840 100644
--- a/llvm/include/llvm/ProfileData/InstrProfReader.h
+++ b/llvm/include/llvm/ProfileData/InstrProfReader.h
@@ -448,8 +448,12 @@ class RawInstrProfReader : public InstrProfReader {
private:
Error createSymtab(InstrProfSymtab &Symtab);
+ Expected<const RawInstrProf::Header *>
+ getNextHeader(const char *CurrentPos) const;
+ Expected<const char *> getNextHeaderPosForCurrentHeader() const;
Error readNextHeader(const char *CurrentPos);
- Error readHeader(const RawInstrProf::Header &Header);
+ Error readHeader(const RawInstrProf::Header &Header,
+ InstrProfSymtab *SymtabToPopulate, bool RecordBinaryIds);
template <class IntT> IntT swap(IntT Int) const {
return ShouldSwapBytes ? llvm::byteswap(Int) : Int;
diff --git a/llvm/lib/ProfileData/InstrProfReader.cpp b/llvm/lib/ProfileData/InstrProfReader.cpp
index 05770f83ed160..dbe0aec6c4938 100644
--- a/llvm/lib/ProfileData/InstrProfReader.cpp
+++ b/llvm/lib/ProfileData/InstrProfReader.cpp
@@ -514,21 +514,46 @@ Error RawInstrProfReader<IntPtrT>::readHeader() {
return error(instrprof_error::bad_magic);
if (DataBuffer->getBufferSize() < sizeof(RawInstrProf::Header))
return error(instrprof_error::bad_header);
- auto *Header = reinterpret_cast<const RawInstrProf::Header *>(
+ auto *FirstHeader = reinterpret_cast<const RawInstrProf::Header *>(
DataBuffer->getBufferStart());
- ShouldSwapBytes = Header->Magic != RawInstrProf::getMagic<IntPtrT>();
- return readHeader(*Header);
+ ShouldSwapBytes = FirstHeader->Magic != RawInstrProf::getMagic<IntPtrT>();
+
+ BinaryIds.clear();
+ Symtab = std::make_unique<InstrProfSymtab>();
+
+ // A value-profile target recorded by one image can name a function or
+ // vtable in a later concatenated raw profile. Build the address-to-name
+ // mappings for the entire buffer before deserializing the first record.
+ const RawInstrProf::Header *Header = FirstHeader;
+ while (Header) {
+ if (Error E = readHeader(*Header, Symtab.get(), true))
+ return E;
+
+ Expected<const char *> NextPos = getNextHeaderPosForCurrentHeader();
+ if (Error E = NextPos.takeError())
+ return error(std::move(E));
+
+ Expected<const RawInstrProf::Header *> NextHeader = getNextHeader(*NextPos);
+ if (Error E = NextHeader.takeError())
+ return error(std::move(E));
+ Header = *NextHeader;
+ }
+
+ // Restore the first header's iteration state without adding its mappings or
+ // binary IDs a second time.
+ return readHeader(*FirstHeader, nullptr, false);
}
template <class IntPtrT>
-Error RawInstrProfReader<IntPtrT>::readNextHeader(const char *CurrentPos) {
+Expected<const RawInstrProf::Header *>
+RawInstrProfReader<IntPtrT>::getNextHeader(const char *CurrentPos) const {
const char *End = DataBuffer->getBufferEnd();
// Skip zero padding between profiles.
while (CurrentPos != End && *CurrentPos == 0)
++CurrentPos;
// If there's nothing left, we're done.
if (CurrentPos == End)
- return make_error<InstrProfError>(instrprof_error::eof);
+ return nullptr;
// If there isn't enough space for another header, this is probably just
// garbage at the end of the file.
if (CurrentPos + sizeof(RawInstrProf::Header) > End)
@@ -543,9 +568,48 @@ Error RawInstrProfReader<IntPtrT>::readNextHeader(const char *CurrentPos) {
if (Magic != swap(RawInstrProf::getMagic<IntPtrT>()))
return make_error<InstrProfError>(instrprof_error::bad_magic);
- // There's another profile to read, so we need to process the header.
- auto *Header = reinterpret_cast<const RawInstrProf::Header *>(CurrentPos);
- return readHeader(*Header);
+ return reinterpret_cast<const RawInstrProf::Header *>(CurrentPos);
+}
+
+template <class IntPtrT>
+Expected<const char *>
+RawInstrProfReader<IntPtrT>::getNextHeaderPosForCurrentHeader() const {
+ const uint8_t *CurrentPos = ValueDataStart;
+ const uint8_t *BufferEnd =
+ reinterpret_cast<const uint8_t *>(DataBuffer->getBufferEnd());
+ for (const RawInstrProf::ProfileData<IntPtrT> *I = Data; I != DataEnd; ++I) {
+ uint32_t NumValueKinds = 0;
+ for (uint32_t K = 0; K < IPVK_Last + 1; ++K)
+ NumValueKinds += I->NumValueSites[K] != 0;
+ if (!NumValueKinds)
+ continue;
+
+ if (BufferEnd - CurrentPos < static_cast<ptrdiff_t>(sizeof(ValueProfData)))
+ return make_error<InstrProfError>(instrprof_error::truncated);
+
+ uint32_t TotalSize =
+ support::endian::read32(CurrentPos, getDataEndianness());
+ if (TotalSize < sizeof(ValueProfData) || TotalSize % sizeof(uint64_t) != 0)
+ return make_error<InstrProfError>(
+ instrprof_error::malformed,
+ "invalid total size for value profile data");
+ if (TotalSize > static_cast<uint64_t>(BufferEnd - CurrentPos))
+ return make_error<InstrProfError>(instrprof_error::too_large);
+ CurrentPos += TotalSize;
+ }
+ return reinterpret_cast<const char *>(CurrentPos);
+}
+
+template <class IntPtrT>
+Error RawInstrProfReader<IntPtrT>::readNextHeader(const char *CurrentPos) {
+ Expected<const RawInstrProf::Header *> Header = getNextHeader(CurrentPos);
+ if (Error E = Header.takeError())
+ return E;
+ if (!*Header)
+ return make_error<InstrProfError>(instrprof_error::eof);
+
+ // The full-buffer symtab and binary ID list were built by readHeader().
+ return readHeader(**Header, nullptr, false);
}
template <class IntPtrT>
@@ -579,7 +643,8 @@ Error RawInstrProfReader<IntPtrT>::createSymtab(InstrProfSymtab &Symtab) {
template <class IntPtrT>
Error RawInstrProfReader<IntPtrT>::readHeader(
- const RawInstrProf::Header &Header) {
+ const RawInstrProf::Header &Header, InstrProfSymtab *SymtabToPopulate,
+ bool RecordBinaryIds) {
Version = swap(Header.Version);
if (GET_VERSION(Version) != RawInstrProf::Version)
return error(instrprof_error::raw_profile_version_mismatch,
@@ -599,7 +664,7 @@ Error RawInstrProfReader<IntPtrT>::readHeader(
if (BinaryIdSize % sizeof(uint64_t) || BinaryIdEnd > BufferEnd)
return error(instrprof_error::bad_header);
ArrayRef<uint8_t> BinaryIdsBuffer(BinaryIdStart, BinaryIdSize);
- if (!BinaryIdsBuffer.empty()) {
+ if (RecordBinaryIds && !BinaryIdsBuffer.empty()) {
if (Error Err = readBinaryIdsInternal(*DataBuffer, BinaryIdsBuffer,
BinaryIds, getDataEndianness()))
return Err;
@@ -705,11 +770,9 @@ Error RawInstrProfReader<IntPtrT>::readHeader(
UniformCountersEnd = UniformCountersStart + UniformCountersSectionSize;
ValueDataStart = reinterpret_cast<const uint8_t *>(Start + ValueDataOffset);
- std::unique_ptr<InstrProfSymtab> NewSymtab = std::make_unique<InstrProfSymtab>();
- if (Error E = createSymtab(*NewSymtab))
- return E;
-
- Symtab = std::move(NewSymtab);
+ if (SymtabToPopulate)
+ if (Error E = createSymtab(*SymtabToPopulate))
+ return E;
return success();
}
diff --git a/llvm/test/tools/llvm-profdata/raw-two-profiles.test b/llvm/test/tools/llvm-profdata/raw-two-profiles.test
index 80fbba33e8e01..04d101c60d3f5 100644
--- a/llvm/test/tools/llvm-profdata/raw-two-profiles.test
+++ b/llvm/test/tools/llvm-profdata/raw-two-profiles.test
@@ -22,20 +22,29 @@ RUN: printf '\0\0\0\0\0\0\0\0' >> %t-foo.profraw
RUN: printf '\0\0\4\0\2\0\0\0' >> %t-foo.profraw
RUN: printf '\0\0\0\0\0\0\0\0' >> %t-foo.profraw
RUN: printf '\0\0\0\0\0\0\0\0' >> %t-foo.profraw
-RUN: printf '\0\0\0\0\0\0\0\0' >> %t-foo.profraw
+RUN: printf '\2\0\0\0\0\0\0\0' >> %t-foo.profraw
RUN: printf '\254\275\030\333\114\302\370\134' >> %t-foo.profraw
RUN: printf '\1\0\0\0\0\0\0\0' >> %t-foo.profraw
RUN: printf '\0\0\4\0\1\0\0\0' >> %t-foo.profraw
RUN: printf '\0\0\0\0\0\0\0\0' >> %t-foo.profraw
RUN: printf '\0\0\0\0\0\0\0\0' >> %t-foo.profraw
+RUN: printf '\0\20\0\0\0\0\0\0' >> %t-foo.profraw
RUN: printf '\0\0\0\0\0\0\0\0' >> %t-foo.profraw
-RUN: printf '\0\0\0\0\0\0\0\0' >> %t-foo.profraw
+RUN: printf '\1\0\0\0\1\0\0\0' >> %t-foo.profraw
RUN: printf '\1\0\0\0\0\0\0\0' >> %t-foo.profraw
-RUN: printf '\0\0\0\0\0\0\0\0' >> %t-foo.profraw
RUN: printf '\023\0\0\0\0\0\0\0' >> %t-foo.profraw
RUN: printf '\3\0foo\0\0\0' >> %t-foo.profraw
+RUN: printf '\110\0\0\0\2\0\0\0' >> %t-foo.profraw
+RUN: printf '\0\0\0\0\1\0\0\0' >> %t-foo.profraw
+RUN: printf '\1\0\0\0\0\0\0\0' >> %t-foo.profraw
+RUN: printf '\0\40\0\0\0\0\0\0' >> %t-foo.profraw
+RUN: printf '\7\0\0\0\0\0\0\0' >> %t-foo.profraw
+RUN: printf '\2\0\0\0\1\0\0\0' >> %t-foo.profraw
+RUN: printf '\1\0\0\0\0\0\0\0' >> %t-foo.profraw
+RUN: printf '\10\60\0\0\0\0\0\0' >> %t-foo.profraw
+RUN: printf '\11\0\0\0\0\0\0\0' >> %t-foo.profraw
RUN: printf '\201rforpl\377' > %t-bar.profraw
RUN: printf '\13\0\0\0\0\0\0\0' >> %t-bar.profraw
@@ -53,16 +62,16 @@ RUN: printf '\10\0\0\0\0\0\0\0' >> %t-bar.profraw
RUN: printf '\0\0\6\0\1\0\0\0' >> %t-bar.profraw
RUN: printf '\0\0\0\0\0\0\0\0' >> %t-bar.profraw
RUN: printf '\0\0\6\0\2\0\0\0' >> %t-bar.profraw
-RUN: printf '\0\0\0\0\0\0\0\0' >> %t-bar.profraw
-RUN: printf '\0\0\0\0\0\0\0\0' >> %t-bar.profraw
-RUN: printf '\0\0\0\0\0\0\0\0' >> %t-bar.profraw
+RUN: printf '\1\0\0\0\0\0\0\0' >> %t-bar.profraw
+RUN: printf '\10\0\0\0\0\0\0\0' >> %t-bar.profraw
+RUN: printf '\2\0\0\0\0\0\0\0' >> %t-bar.profraw
RUN: printf '\067\265\035\031\112\165\023\344' >> %t-bar.profraw
RUN: printf '\02\0\0\0\0\0\0\0' >> %t-bar.profraw
RUN: printf '\0\0\6\0\1\0\0\0' >> %t-bar.profraw
RUN: printf '\0\0\0\0\0\0\0\0' >> %t-bar.profraw
RUN: printf '\0\0\0\0\0\0\0\0' >> %t-bar.profraw
-RUN: printf '\0\0\0\0\0\0\0\0' >> %t-bar.profraw
+RUN: printf '\0\40\0\0\0\0\0\0' >> %t-bar.profraw
RUN: printf '\0\0\0\0\0\0\0\0' >> %t-bar.profraw
RUN: printf '\02\0\0\0\0\0\0\0' >> %t-bar.profraw
RUN: printf '\0\0\0\0\0\0\0\0' >> %t-bar.profraw
@@ -70,16 +79,29 @@ RUN: printf '\0\0\0\0\0\0\0\0' >> %t-bar.profraw
RUN: printf '\067\0\0\0\0\0\0\0' >> %t-bar.profraw
RUN: printf '\101\0\0\0\0\0\0\0' >> %t-bar.profraw
RUN: printf '\3\0bar\0\0\0' >> %t-bar.profraw
+RUN: printf '\320\115\170\140\375\107\0\250' >> %t-bar.profraw
+RUN: printf '\0\60\0\0\0\0\0\0' >> %t-bar.profraw
+RUN: printf '\20\0\0\0\0\0\0\0' >> %t-bar.profraw
+RUN: printf '\6\0_ZTV1A' >> %t-bar.profraw
RUN: cat %t-foo.profraw %t-bar.profraw > %t-pad.profraw
-RUN: llvm-profdata show %t-pad.profraw -all-functions -counts | FileCheck %s
+RUN: llvm-profdata show %t-pad.profraw -all-functions -counts -ic-targets -show-vtables | FileCheck %s
+RUN: cp %t-pad.profraw %t-bad-value-size.profraw
+RUN: printf '\1\0\0\0' | dd of=%t-bad-value-size.profraw bs=1 seek=240 conv=notrunc 2>/dev/null
+RUN: not llvm-profdata show %t-bad-value-size.profraw 2>&1 | FileCheck %s --check-prefix=BAD-VALUE-SIZE
CHECK: Counters:
CHECK: foo:
CHECK: Hash: 0x0000000000000001
CHECK: Counters: 1
CHECK: Function count: 19
+CHECK: Indirect Call Site Count: 1
+CHECK: Number of instrumented vtables: 1
CHECK: Block counts: []
+CHECK: Indirect Target Results:
+CHECK: [ 0, bar, 7 ]
+CHECK: VTable Results:
+CHECK: [ 0, _ZTV1A, 9 ]
CHECK: bar:
CHECK: Hash: 0x0000000000000002
CHECK: Counters: 2
@@ -89,3 +111,5 @@ CHECK: Functions shown: 2
CHECK: Total functions: 2
CHECK: Maximum function count: 55
CHECK: Maximum internal block count: 65
+
+BAD-VALUE-SIZE: malformed instrumentation profile data: invalid total size for value profile data
``````````
</details>
https://github.com/llvm/llvm-project/pull/212715
More information about the llvm-commits
mailing list