blob: 7a2c4aff32c5325cb459e4a48cced78f6bb1fddf [file] [log] [blame]
//===- unittests/Support/MemProfTest.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 "llvm/ProfileData/MemProf.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/MapVector.h"
#include "llvm/ADT/STLForwardCompat.h"
#include "llvm/DebugInfo/DIContext.h"
#include "llvm/DebugInfo/Symbolize/SymbolizableModule.h"
#include "llvm/IR/Value.h"
#include "llvm/Object/ObjectFile.h"
#include "llvm/ProfileData/IndexedMemProfData.h"
#include "llvm/ProfileData/MemProfData.inc"
#include "llvm/ProfileData/MemProfRadixTree.h"
#include "llvm/ProfileData/MemProfReader.h"
#include "llvm/Support/raw_ostream.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <initializer_list>
namespace llvm {
namespace memprof {
namespace {
using ::llvm::DIGlobal;
using ::llvm::DIInliningInfo;
using ::llvm::DILineInfo;
using ::llvm::DILineInfoSpecifier;
using ::llvm::DILocal;
using ::llvm::StringRef;
using ::llvm::object::SectionedAddress;
using ::llvm::symbolize::SymbolizableModule;
using ::testing::ElementsAre;
using ::testing::IsEmpty;
using ::testing::Pair;
using ::testing::Return;
using ::testing::SizeIs;
using ::testing::UnorderedElementsAre;
class MockSymbolizer : public SymbolizableModule {
public:
MOCK_CONST_METHOD3(symbolizeInlinedCode,
DIInliningInfo(SectionedAddress, DILineInfoSpecifier,
bool));
// Most of the methods in the interface are unused. We only mock the
// method that we expect to be called from the memprof reader.
virtual DILineInfo symbolizeCode(SectionedAddress, DILineInfoSpecifier,
bool) const {
llvm_unreachable("unused");
}
virtual DIGlobal symbolizeData(SectionedAddress) const {
llvm_unreachable("unused");
}
virtual std::vector<DILocal> symbolizeFrame(SectionedAddress) const {
llvm_unreachable("unused");
}
virtual std::vector<SectionedAddress> findSymbol(StringRef Symbol,
uint64_t Offset) const {
llvm_unreachable("unused");
}
virtual bool isWin32Module() const { llvm_unreachable("unused"); }
virtual uint64_t getModulePreferredBase() const {
llvm_unreachable("unused");
}
};
struct MockInfo {
std::string FunctionName;
uint32_t Line;
uint32_t StartLine;
uint32_t Column;
std::string FileName = "valid/path.cc";
};
DIInliningInfo makeInliningInfo(std::initializer_list<MockInfo> MockFrames) {
DIInliningInfo Result;
for (const auto &Item : MockFrames) {
DILineInfo Frame;
Frame.FunctionName = Item.FunctionName;
Frame.Line = Item.Line;
Frame.StartLine = Item.StartLine;
Frame.Column = Item.Column;
Frame.FileName = Item.FileName;
Result.addFrame(Frame);
}
return Result;
}
llvm::SmallVector<SegmentEntry, 4> makeSegments() {
llvm::SmallVector<SegmentEntry, 4> Result;
// Mimic an entry for a non position independent executable.
Result.emplace_back(0x0, 0x40000, 0x0);
return Result;
}
const DILineInfoSpecifier specifier() {
return DILineInfoSpecifier(
DILineInfoSpecifier::FileLineInfoKind::RawValue,
DILineInfoSpecifier::FunctionNameKind::LinkageName);
}
MATCHER_P4(FrameContains, FunctionName, LineOffset, Column, Inline, "") {
const Frame &F = arg;
const uint64_t ExpectedHash = memprof::getGUID(FunctionName);
if (F.Function != ExpectedHash) {
*result_listener << "Hash mismatch";
return false;
}
if (F.SymbolName && *F.SymbolName != FunctionName) {
*result_listener << "SymbolName mismatch\nWant: " << FunctionName
<< "\nGot: " << *F.SymbolName;
return false;
}
if (F.LineOffset == LineOffset && F.Column == Column &&
F.IsInlineFrame == Inline) {
return true;
}
*result_listener << "LineOffset, Column or Inline mismatch";
return false;
}
TEST(MemProf, FillsValue) {
auto Symbolizer = std::make_unique<MockSymbolizer>();
EXPECT_CALL(*Symbolizer, symbolizeInlinedCode(SectionedAddress{0x1000},
specifier(), false))
.Times(1) // Only once since we remember invalid PCs.
.WillRepeatedly(Return(makeInliningInfo({
{"new", 70, 57, 3, "memprof/memprof_new_delete.cpp"},
})));
EXPECT_CALL(*Symbolizer, symbolizeInlinedCode(SectionedAddress{0x2000},
specifier(), false))
.Times(1) // Only once since we cache the result for future lookups.
.WillRepeatedly(Return(makeInliningInfo({
{"foo", 10, 5, 30},
{"bar", 201, 150, 20},
})));
EXPECT_CALL(*Symbolizer, symbolizeInlinedCode(SectionedAddress{0x3000},
specifier(), false))
.Times(1)
.WillRepeatedly(Return(makeInliningInfo({
{"xyz.llvm.123", 10, 5, 30},
{"abc", 10, 5, 30},
})));
CallStackMap CSM;
CSM[0x1] = {0x1000, 0x2000, 0x3000};
llvm::MapVector<uint64_t, MemInfoBlock> Prof;
Prof[0x1].AllocCount = 1;
auto Seg = makeSegments();
RawMemProfReader Reader(std::move(Symbolizer), Seg, Prof, CSM,
/*KeepName=*/true);
llvm::DenseMap<llvm::GlobalValue::GUID, MemProfRecord> Records;
for (const auto &Pair : Reader)
Records.insert({Pair.first, Pair.second});
// Mock program pseudocode and expected memprof record contents.
//
// AllocSite CallSite
// inline foo() { new(); } Y N
// bar() { foo(); } Y Y
// inline xyz() { bar(); } N Y
// abc() { xyz(); } N Y
// We expect 4 records. We attach alloc site data to foo and bar, i.e.
// all frames bottom up until we find a non-inline frame. We attach call site
// data to bar, xyz and abc.
ASSERT_THAT(Records, SizeIs(4));
// Check the memprof record for foo.
const llvm::GlobalValue::GUID FooId = memprof::getGUID("foo");
ASSERT_TRUE(Records.contains(FooId));
const MemProfRecord &Foo = Records[FooId];
ASSERT_THAT(Foo.AllocSites, SizeIs(1));
EXPECT_EQ(Foo.AllocSites[0].Info.getAllocCount(), 1U);
EXPECT_THAT(Foo.AllocSites[0].CallStack[0],
FrameContains("foo", 5U, 30U, true));
EXPECT_THAT(Foo.AllocSites[0].CallStack[1],
FrameContains("bar", 51U, 20U, false));
EXPECT_THAT(Foo.AllocSites[0].CallStack[2],
FrameContains("xyz", 5U, 30U, true));
EXPECT_THAT(Foo.AllocSites[0].CallStack[3],
FrameContains("abc", 5U, 30U, false));
EXPECT_TRUE(Foo.CallSites.empty());
// Check the memprof record for bar.
const llvm::GlobalValue::GUID BarId = memprof::getGUID("bar");
ASSERT_TRUE(Records.contains(BarId));
const MemProfRecord &Bar = Records[BarId];
ASSERT_THAT(Bar.AllocSites, SizeIs(1));
EXPECT_EQ(Bar.AllocSites[0].Info.getAllocCount(), 1U);
EXPECT_THAT(Bar.AllocSites[0].CallStack[0],
FrameContains("foo", 5U, 30U, true));
EXPECT_THAT(Bar.AllocSites[0].CallStack[1],
FrameContains("bar", 51U, 20U, false));
EXPECT_THAT(Bar.AllocSites[0].CallStack[2],
FrameContains("xyz", 5U, 30U, true));
EXPECT_THAT(Bar.AllocSites[0].CallStack[3],
FrameContains("abc", 5U, 30U, false));
EXPECT_THAT(Bar.CallSites,
ElementsAre(testing::Field(
&CallSiteInfo::Frames,
ElementsAre(FrameContains("foo", 5U, 30U, true),
FrameContains("bar", 51U, 20U, false)))));
// Check the memprof record for xyz.
const llvm::GlobalValue::GUID XyzId = memprof::getGUID("xyz");
ASSERT_TRUE(Records.contains(XyzId));
const MemProfRecord &Xyz = Records[XyzId];
// Expect the entire frame even though in practice we only need the first
// entry here.
EXPECT_THAT(Xyz.CallSites,
ElementsAre(testing::Field(
&CallSiteInfo::Frames,
ElementsAre(FrameContains("xyz", 5U, 30U, true),
FrameContains("abc", 5U, 30U, false)))));
// Check the memprof record for abc.
const llvm::GlobalValue::GUID AbcId = memprof::getGUID("abc");
ASSERT_TRUE(Records.contains(AbcId));
const MemProfRecord &Abc = Records[AbcId];
EXPECT_TRUE(Abc.AllocSites.empty());
EXPECT_THAT(Abc.CallSites,
ElementsAre(testing::Field(
&CallSiteInfo::Frames,
ElementsAre(FrameContains("xyz", 5U, 30U, true),
FrameContains("abc", 5U, 30U, false)))));
}
TEST(MemProf, PortableWrapper) {
MemInfoBlock Info(/*size=*/16, /*access_count=*/7, /*alloc_timestamp=*/1000,
/*dealloc_timestamp=*/2000, /*alloc_cpu=*/3,
/*dealloc_cpu=*/4, /*Histogram=*/0, /*HistogramSize=*/0);
const auto Schema = getFullSchema();
PortableMemInfoBlock WriteBlock(Info, Schema);
std::string Buffer;
llvm::raw_string_ostream OS(Buffer);
WriteBlock.serialize(Schema, OS);
PortableMemInfoBlock ReadBlock(
Schema, reinterpret_cast<const unsigned char *>(Buffer.data()));
EXPECT_EQ(ReadBlock, WriteBlock);
// Here we compare directly with the actual counts instead of MemInfoBlock
// members. Since the MemInfoBlock struct is packed and the EXPECT_EQ macros
// take a reference to the params, this results in unaligned accesses.
EXPECT_EQ(1UL, ReadBlock.getAllocCount());
EXPECT_EQ(7ULL, ReadBlock.getTotalAccessCount());
EXPECT_EQ(3UL, ReadBlock.getAllocCpuId());
}
TEST(MemProf, RecordSerializationRoundTripVerion2) {
const auto Schema = getFullSchema();
MemInfoBlock Info(/*size=*/16, /*access_count=*/7, /*alloc_timestamp=*/1000,
/*dealloc_timestamp=*/2000, /*alloc_cpu=*/3,
/*dealloc_cpu=*/4, /*Histogram=*/0, /*HistogramSize=*/0);
llvm::SmallVector<CallStackId> CallStackIds = {0x123, 0x456};
llvm::SmallVector<CallStackId> CallSiteIds = {0x333, 0x444};
IndexedMemProfRecord Record;
for (const auto &CSId : CallStackIds) {
// Use the same info block for both allocation sites.
Record.AllocSites.emplace_back(CSId, Info);
}
for (auto CSId : CallSiteIds)
Record.CallSites.push_back(IndexedCallSiteInfo(CSId));
std::string Buffer;
llvm::raw_string_ostream OS(Buffer);
Record.serialize(Schema, OS, Version2);
const IndexedMemProfRecord GotRecord = IndexedMemProfRecord::deserialize(
Schema, reinterpret_cast<const unsigned char *>(Buffer.data()), Version2);
EXPECT_EQ(Record, GotRecord);
}
TEST(MemProf, RecordSerializationRoundTripVersion4) {
const auto Schema = getFullSchema();
MemInfoBlock Info(/*size=*/16, /*access_count=*/7, /*alloc_timestamp=*/1000,
/*dealloc_timestamp=*/2000, /*alloc_cpu=*/3,
/*dealloc_cpu=*/4, /*Histogram=*/0, /*HistogramSize=*/0);
llvm::SmallVector<CallStackId> CallStackIds = {0x123, 0x456};
llvm::SmallVector<IndexedCallSiteInfo> CallSites;
CallSites.push_back(
IndexedCallSiteInfo(0x333, {0xaaa, 0xbbb})); // CSId with GUIDs
CallSites.push_back(IndexedCallSiteInfo(0x444)); // CSId without GUIDs
IndexedMemProfRecord Record;
for (const auto &CSId : CallStackIds) {
// Use the same info block for both allocation sites.
Record.AllocSites.emplace_back(CSId, Info);
}
Record.CallSites = std::move(CallSites);
std::string Buffer;
llvm::raw_string_ostream OS(Buffer);
// Need a dummy map for V4 serialization
llvm::DenseMap<CallStackId, LinearCallStackId> DummyMap = {
{0x123, 1}, {0x456, 2}, {0x333, 3}, {0x444, 4}};
Record.serialize(Schema, OS, Version4, &DummyMap);
const IndexedMemProfRecord GotRecord = IndexedMemProfRecord::deserialize(
Schema, reinterpret_cast<const unsigned char *>(Buffer.data()), Version4);
// Create the expected record using the linear IDs from the dummy map.
IndexedMemProfRecord ExpectedRecord;
for (const auto &CSId : CallStackIds) {
ExpectedRecord.AllocSites.emplace_back(DummyMap[CSId], Info);
}
for (const auto &CSInfo :
Record.CallSites) { // Use original Record's CallSites to get GUIDs
ExpectedRecord.CallSites.emplace_back(DummyMap[CSInfo.CSId],
CSInfo.CalleeGuids);
}
EXPECT_EQ(ExpectedRecord, GotRecord);
}
TEST(MemProf, RecordSerializationRoundTripVersion2HotColdSchema) {
const auto Schema = getHotColdSchema();
MemInfoBlock Info;
Info.AllocCount = 11;
Info.TotalSize = 22;
Info.TotalLifetime = 33;
Info.TotalLifetimeAccessDensity = 44;
llvm::SmallVector<CallStackId> CallStackIds = {0x123, 0x456};
llvm::SmallVector<CallStackId> CallSiteIds = {0x333, 0x444};
IndexedMemProfRecord Record;
for (const auto &CSId : CallStackIds) {
// Use the same info block for both allocation sites.
Record.AllocSites.emplace_back(CSId, Info, Schema);
}
for (auto CSId : CallSiteIds)
Record.CallSites.push_back(IndexedCallSiteInfo(CSId));
std::bitset<llvm::to_underlying(Meta::Size)> SchemaBitSet;
for (auto Id : Schema)
SchemaBitSet.set(llvm::to_underlying(Id));
// Verify that SchemaBitSet has the fields we expect and nothing else, which
// we check with count().
EXPECT_EQ(SchemaBitSet.count(), 4U);
EXPECT_TRUE(SchemaBitSet[llvm::to_underlying(Meta::AllocCount)]);
EXPECT_TRUE(SchemaBitSet[llvm::to_underlying(Meta::TotalSize)]);
EXPECT_TRUE(SchemaBitSet[llvm::to_underlying(Meta::TotalLifetime)]);
EXPECT_TRUE(
SchemaBitSet[llvm::to_underlying(Meta::TotalLifetimeAccessDensity)]);
// Verify that Schema has propagated all the way to the Info field in each
// IndexedAllocationInfo.
ASSERT_THAT(Record.AllocSites, SizeIs(2));
EXPECT_EQ(Record.AllocSites[0].Info.getSchema(), SchemaBitSet);
EXPECT_EQ(Record.AllocSites[1].Info.getSchema(), SchemaBitSet);
std::string Buffer;
llvm::raw_string_ostream OS(Buffer);
Record.serialize(Schema, OS, Version2);
const IndexedMemProfRecord GotRecord = IndexedMemProfRecord::deserialize(
Schema, reinterpret_cast<const unsigned char *>(Buffer.data()), Version2);
// Verify that Schema comes back correctly after deserialization. Technically,
// the comparison between Record and GotRecord below includes the comparison
// of their Schemas, but we'll verify the Schemas on our own.
ASSERT_THAT(GotRecord.AllocSites, SizeIs(2));
EXPECT_EQ(GotRecord.AllocSites[0].Info.getSchema(), SchemaBitSet);
EXPECT_EQ(GotRecord.AllocSites[1].Info.getSchema(), SchemaBitSet);
EXPECT_EQ(Record, GotRecord);
}
TEST(MemProf, SymbolizationFilter) {
auto Symbolizer = std::make_unique<MockSymbolizer>();
EXPECT_CALL(*Symbolizer, symbolizeInlinedCode(SectionedAddress{0x1000},
specifier(), false))
.Times(1) // once since we don't lookup invalid PCs repeatedly.
.WillRepeatedly(Return(makeInliningInfo({
{"malloc", 70, 57, 3, "memprof/memprof_malloc_linux.cpp"},
})));
EXPECT_CALL(*Symbolizer, symbolizeInlinedCode(SectionedAddress{0x2000},
specifier(), false))
.Times(1) // once since we don't lookup invalid PCs repeatedly.
.WillRepeatedly(Return(makeInliningInfo({
{"new", 70, 57, 3, "memprof/memprof_new_delete.cpp"},
})));
EXPECT_CALL(*Symbolizer, symbolizeInlinedCode(SectionedAddress{0x3000},
specifier(), false))
.Times(1) // once since we don't lookup invalid PCs repeatedly.
.WillRepeatedly(Return(makeInliningInfo({
{DILineInfo::BadString, 0, 0, 0},
})));
EXPECT_CALL(*Symbolizer, symbolizeInlinedCode(SectionedAddress{0x4000},
specifier(), false))
.Times(1)
.WillRepeatedly(Return(makeInliningInfo({
{"foo", 10, 5, 30, "memprof/memprof_test_file.cpp"},
})));
EXPECT_CALL(*Symbolizer, symbolizeInlinedCode(SectionedAddress{0x5000},
specifier(), false))
.Times(1)
.WillRepeatedly(Return(makeInliningInfo({
// Depending on how the runtime was compiled, only the filename
// may be present in the debug information.
{"malloc", 70, 57, 3, "memprof_malloc_linux.cpp"},
})));
CallStackMap CSM;
CSM[0x1] = {0x1000, 0x2000, 0x3000, 0x4000};
// This entry should be dropped since all PCs are either not
// symbolizable or belong to the runtime.
CSM[0x2] = {0x1000, 0x2000, 0x5000};
llvm::MapVector<uint64_t, MemInfoBlock> Prof;
Prof[0x1].AllocCount = 1;
Prof[0x2].AllocCount = 1;
auto Seg = makeSegments();
RawMemProfReader Reader(std::move(Symbolizer), Seg, Prof, CSM);
llvm::SmallVector<MemProfRecord, 1> Records;
for (const auto &KeyRecordPair : Reader)
Records.push_back(KeyRecordPair.second);
ASSERT_THAT(Records, SizeIs(1));
ASSERT_THAT(Records[0].AllocSites, SizeIs(1));
EXPECT_THAT(Records[0].AllocSites[0].CallStack,
ElementsAre(FrameContains("foo", 5U, 30U, false)));
}
TEST(MemProf, BaseMemProfReader) {
IndexedMemProfData MemProfData;
Frame F1(/*Hash=*/memprof::getGUID("foo"), /*LineOffset=*/20,
/*Column=*/5, /*IsInlineFrame=*/true);
Frame F2(/*Hash=*/memprof::getGUID("bar"), /*LineOffset=*/10,
/*Column=*/2, /*IsInlineFrame=*/false);
auto F1Id = MemProfData.addFrame(F1);
auto F2Id = MemProfData.addFrame(F2);
llvm::SmallVector<FrameId> CallStack{F1Id, F2Id};
CallStackId CSId = MemProfData.addCallStack(std::move(CallStack));
IndexedMemProfRecord FakeRecord;
MemInfoBlock Block;
Block.AllocCount = 1U, Block.TotalAccessDensity = 4,
Block.TotalLifetime = 200001;
FakeRecord.AllocSites.emplace_back(/*CSId=*/CSId, /*MB=*/Block);
MemProfData.Records.try_emplace(0x1234, std::move(FakeRecord));
MemProfReader Reader(std::move(MemProfData));
llvm::SmallVector<MemProfRecord, 1> Records;
for (const auto &KeyRecordPair : Reader)
Records.push_back(KeyRecordPair.second);
ASSERT_THAT(Records, SizeIs(1));
ASSERT_THAT(Records[0].AllocSites, SizeIs(1));
EXPECT_THAT(Records[0].AllocSites[0].CallStack,
ElementsAre(FrameContains("foo", 20U, 5U, true),
FrameContains("bar", 10U, 2U, false)));
}
TEST(MemProf, BaseMemProfReaderWithCSIdMap) {
IndexedMemProfData MemProfData;
Frame F1(/*Hash=*/memprof::getGUID("foo"), /*LineOffset=*/20,
/*Column=*/5, /*IsInlineFrame=*/true);
Frame F2(/*Hash=*/memprof::getGUID("bar"), /*LineOffset=*/10,
/*Column=*/2, /*IsInlineFrame=*/false);
auto F1Id = MemProfData.addFrame(F1);
auto F2Id = MemProfData.addFrame(F2);
llvm::SmallVector<FrameId> CallStack = {F1Id, F2Id};
auto CSId = MemProfData.addCallStack(std::move(CallStack));
IndexedMemProfRecord FakeRecord;
MemInfoBlock Block;
Block.AllocCount = 1U, Block.TotalAccessDensity = 4,
Block.TotalLifetime = 200001;
FakeRecord.AllocSites.emplace_back(/*CSId=*/CSId, /*MB=*/Block);
MemProfData.Records.try_emplace(0x1234, std::move(FakeRecord));
MemProfReader Reader(std::move(MemProfData));
llvm::SmallVector<MemProfRecord, 1> Records;
for (const auto &KeyRecordPair : Reader)
Records.push_back(KeyRecordPair.second);
ASSERT_THAT(Records, SizeIs(1));
ASSERT_THAT(Records[0].AllocSites, SizeIs(1));
EXPECT_THAT(Records[0].AllocSites[0].CallStack,
ElementsAre(FrameContains("foo", 20U, 5U, true),
FrameContains("bar", 10U, 2U, false)));
}
TEST(MemProf, IndexedMemProfRecordToMemProfRecord) {
// Verify that MemProfRecord can be constructed from IndexedMemProfRecord with
// CallStackIds only.
IndexedMemProfData MemProfData;
Frame F1(1, 0, 0, false);
Frame F2(2, 0, 0, false);
Frame F3(3, 0, 0, false);
Frame F4(4, 0, 0, false);
auto F1Id = MemProfData.addFrame(F1);
auto F2Id = MemProfData.addFrame(F2);
auto F3Id = MemProfData.addFrame(F3);
auto F4Id = MemProfData.addFrame(F4);
llvm::SmallVector<FrameId> CS1 = {F1Id, F2Id};
llvm::SmallVector<FrameId> CS2 = {F1Id, F3Id};
llvm::SmallVector<FrameId> CS3 = {F2Id, F3Id};
llvm::SmallVector<FrameId> CS4 = {F2Id, F4Id};
auto CS1Id = MemProfData.addCallStack(std::move(CS1));
auto CS2Id = MemProfData.addCallStack(std::move(CS2));
auto CS3Id = MemProfData.addCallStack(std::move(CS3));
auto CS4Id = MemProfData.addCallStack(std::move(CS4));
IndexedMemProfRecord IndexedRecord;
IndexedAllocationInfo AI;
AI.CSId = CS1Id;
IndexedRecord.AllocSites.push_back(AI);
AI.CSId = CS2Id;
IndexedRecord.AllocSites.push_back(AI);
IndexedRecord.CallSites.push_back(IndexedCallSiteInfo(CS3Id));
IndexedRecord.CallSites.push_back(IndexedCallSiteInfo(CS4Id));
IndexedCallstackIdConverter CSIdConv(MemProfData);
MemProfRecord Record = IndexedRecord.toMemProfRecord(CSIdConv);
// Make sure that all lookups are successful.
ASSERT_EQ(CSIdConv.FrameIdConv.LastUnmappedId, std::nullopt);
ASSERT_EQ(CSIdConv.CSIdConv.LastUnmappedId, std::nullopt);
// Verify the contents of Record.
ASSERT_THAT(Record.AllocSites, SizeIs(2));
EXPECT_THAT(Record.AllocSites[0].CallStack, ElementsAre(F1, F2));
EXPECT_THAT(Record.AllocSites[1].CallStack, ElementsAre(F1, F3));
ASSERT_THAT(Record.CallSites, SizeIs(2));
EXPECT_THAT(Record.CallSites[0].Frames, ElementsAre(F2, F3));
EXPECT_THAT(Record.CallSites[1].Frames, ElementsAre(F2, F4));
}
// Populate those fields returned by getHotColdSchema.
MemInfoBlock makePartialMIB() {
MemInfoBlock MIB;
MIB.AllocCount = 1;
MIB.TotalSize = 5;
MIB.TotalLifetime = 10;
MIB.TotalLifetimeAccessDensity = 23;
return MIB;
}
TEST(MemProf, MissingCallStackId) {
// Use a non-existent CallStackId to trigger a mapping error in
// toMemProfRecord.
IndexedAllocationInfo AI(0xdeadbeefU, makePartialMIB(), getHotColdSchema());
IndexedMemProfRecord IndexedMR;
IndexedMR.AllocSites.push_back(AI);
// Create empty maps.
IndexedMemProfData MemProfData;
IndexedCallstackIdConverter CSIdConv(MemProfData);
// We are only interested in errors, not the return value.
(void)IndexedMR.toMemProfRecord(CSIdConv);
ASSERT_TRUE(CSIdConv.CSIdConv.LastUnmappedId.has_value());
EXPECT_EQ(*CSIdConv.CSIdConv.LastUnmappedId, 0xdeadbeefU);
EXPECT_EQ(CSIdConv.FrameIdConv.LastUnmappedId, std::nullopt);
}
TEST(MemProf, MissingFrameId) {
// An empty Frame map to trigger a mapping error.
IndexedMemProfData MemProfData;
auto CSId = MemProfData.addCallStack(SmallVector<FrameId>{2, 3});
IndexedMemProfRecord IndexedMR;
IndexedMR.AllocSites.emplace_back(CSId, makePartialMIB(), getHotColdSchema());
IndexedCallstackIdConverter CSIdConv(MemProfData);
// We are only interested in errors, not the return value.
(void)IndexedMR.toMemProfRecord(CSIdConv);
EXPECT_EQ(CSIdConv.CSIdConv.LastUnmappedId, std::nullopt);
ASSERT_TRUE(CSIdConv.FrameIdConv.LastUnmappedId.has_value());
EXPECT_EQ(*CSIdConv.FrameIdConv.LastUnmappedId, 3U);
}
// Verify CallStackRadixTreeBuilder can handle empty inputs.
TEST(MemProf, RadixTreeBuilderEmpty) {
llvm::DenseMap<FrameId, LinearFrameId> MemProfFrameIndexes;
IndexedMemProfData MemProfData;
llvm::DenseMap<FrameId, FrameStat> FrameHistogram =
computeFrameHistogram<FrameId>(MemProfData.CallStacks);
CallStackRadixTreeBuilder<FrameId> Builder;
Builder.build(std::move(MemProfData.CallStacks), &MemProfFrameIndexes,
FrameHistogram);
ASSERT_THAT(Builder.getRadixArray(), IsEmpty());
const auto Mappings = Builder.takeCallStackPos();
ASSERT_THAT(Mappings, IsEmpty());
}
// Verify CallStackRadixTreeBuilder can handle one trivial call stack.
TEST(MemProf, RadixTreeBuilderOne) {
llvm::DenseMap<FrameId, LinearFrameId> MemProfFrameIndexes = {
{11, 1}, {12, 2}, {13, 3}};
llvm::SmallVector<FrameId> CS1 = {13, 12, 11};
IndexedMemProfData MemProfData;
auto CS1Id = MemProfData.addCallStack(std::move(CS1));
llvm::DenseMap<FrameId, FrameStat> FrameHistogram =
computeFrameHistogram<FrameId>(MemProfData.CallStacks);
CallStackRadixTreeBuilder<FrameId> Builder;
Builder.build(std::move(MemProfData.CallStacks), &MemProfFrameIndexes,
FrameHistogram);
EXPECT_THAT(Builder.getRadixArray(),
ElementsAre(3U, // Size of CS1,
3U, // MemProfFrameIndexes[13]
2U, // MemProfFrameIndexes[12]
1U // MemProfFrameIndexes[11]
));
const auto Mappings = Builder.takeCallStackPos();
EXPECT_THAT(Mappings, UnorderedElementsAre(Pair(CS1Id, 0U)));
}
// Verify CallStackRadixTreeBuilder can form a link between two call stacks.
TEST(MemProf, RadixTreeBuilderTwo) {
llvm::DenseMap<FrameId, LinearFrameId> MemProfFrameIndexes = {
{11, 1}, {12, 2}, {13, 3}};
llvm::SmallVector<FrameId> CS1 = {12, 11};
llvm::SmallVector<FrameId> CS2 = {13, 12, 11};
IndexedMemProfData MemProfData;
auto CS1Id = MemProfData.addCallStack(std::move(CS1));
auto CS2Id = MemProfData.addCallStack(std::move(CS2));
llvm::DenseMap<FrameId, FrameStat> FrameHistogram =
computeFrameHistogram<FrameId>(MemProfData.CallStacks);
CallStackRadixTreeBuilder<FrameId> Builder;
Builder.build(std::move(MemProfData.CallStacks), &MemProfFrameIndexes,
FrameHistogram);
EXPECT_THAT(Builder.getRadixArray(),
ElementsAre(2U, // Size of CS1
static_cast<uint32_t>(-3), // Jump 3 steps
3U, // Size of CS2
3U, // MemProfFrameIndexes[13]
2U, // MemProfFrameIndexes[12]
1U // MemProfFrameIndexes[11]
));
const auto Mappings = Builder.takeCallStackPos();
EXPECT_THAT(Mappings, UnorderedElementsAre(Pair(CS1Id, 0U), Pair(CS2Id, 2U)));
}
// Verify CallStackRadixTreeBuilder can form a jump to a prefix that itself has
// another jump to another prefix.
TEST(MemProf, RadixTreeBuilderSuccessiveJumps) {
llvm::DenseMap<FrameId, LinearFrameId> MemProfFrameIndexes = {
{11, 1}, {12, 2}, {13, 3}, {14, 4}, {15, 5}, {16, 6}, {17, 7}, {18, 8},
};
llvm::SmallVector<FrameId> CS1 = {14, 13, 12, 11};
llvm::SmallVector<FrameId> CS2 = {15, 13, 12, 11};
llvm::SmallVector<FrameId> CS3 = {17, 16, 12, 11};
llvm::SmallVector<FrameId> CS4 = {18, 16, 12, 11};
IndexedMemProfData MemProfData;
auto CS1Id = MemProfData.addCallStack(std::move(CS1));
auto CS2Id = MemProfData.addCallStack(std::move(CS2));
auto CS3Id = MemProfData.addCallStack(std::move(CS3));
auto CS4Id = MemProfData.addCallStack(std::move(CS4));
llvm::DenseMap<FrameId, FrameStat> FrameHistogram =
computeFrameHistogram<FrameId>(MemProfData.CallStacks);
CallStackRadixTreeBuilder<FrameId> Builder;
Builder.build(std::move(MemProfData.CallStacks), &MemProfFrameIndexes,
FrameHistogram);
EXPECT_THAT(Builder.getRadixArray(),
ElementsAre(4U, // Size of CS1
4U, // MemProfFrameIndexes[14]
static_cast<uint32_t>(-3), // Jump 3 steps
4U, // Size of CS2
5U, // MemProfFrameIndexes[15]
3U, // MemProfFrameIndexes[13]
static_cast<uint32_t>(-7), // Jump 7 steps
4U, // Size of CS3
7U, // MemProfFrameIndexes[17]
static_cast<uint32_t>(-3), // Jump 3 steps
4U, // Size of CS4
8U, // MemProfFrameIndexes[18]
6U, // MemProfFrameIndexes[16]
2U, // MemProfFrameIndexes[12]
1U // MemProfFrameIndexes[11]
));
const auto Mappings = Builder.takeCallStackPos();
EXPECT_THAT(Mappings,
UnorderedElementsAre(Pair(CS1Id, 0U), Pair(CS2Id, 3U),
Pair(CS3Id, 7U), Pair(CS4Id, 10U)));
}
// Verify that we can parse YAML and retrieve IndexedMemProfData as expected.
TEST(MemProf, YAMLParser) {
StringRef YAMLData = R"YAML(
---
HeapProfileRecords:
- GUID: 0xdeadbeef12345678
AllocSites:
- Callstack:
- {Function: 0x100, LineOffset: 11, Column: 10, IsInlineFrame: true}
- {Function: 0x200, LineOffset: 22, Column: 20, IsInlineFrame: false}
MemInfoBlock:
AllocCount: 777
TotalSize: 888
- Callstack:
- {Function: 0x300, LineOffset: 33, Column: 30, IsInlineFrame: false}
- {Function: 0x400, LineOffset: 44, Column: 40, IsInlineFrame: true}
MemInfoBlock:
AllocCount: 666
TotalSize: 555
CallSites:
- Frames:
- {Function: 0x500, LineOffset: 55, Column: 50, IsInlineFrame: true}
- {Function: 0x600, LineOffset: 66, Column: 60, IsInlineFrame: false}
CalleeGuids: [0x1000, 0x2000]
- Frames:
- {Function: 0x700, LineOffset: 77, Column: 70, IsInlineFrame: true}
- {Function: 0x800, LineOffset: 88, Column: 80, IsInlineFrame: false}
CalleeGuids: [0x3000]
)YAML";
YAMLMemProfReader YAMLReader;
YAMLReader.parse(YAMLData);
IndexedMemProfData MemProfData = YAMLReader.takeMemProfData();
// Verify the entire contents of MemProfData.Records.
ASSERT_THAT(MemProfData.Records, SizeIs(1));
const auto &[GUID, IndexedRecord] = MemProfData.Records.front();
EXPECT_EQ(GUID, 0xdeadbeef12345678ULL);
IndexedCallstackIdConverter CSIdConv(MemProfData);
MemProfRecord Record = IndexedRecord.toMemProfRecord(CSIdConv);
ASSERT_THAT(Record.AllocSites, SizeIs(2));
EXPECT_THAT(
Record.AllocSites[0].CallStack,
ElementsAre(Frame(0x100, 11, 10, true), Frame(0x200, 22, 20, false)));
EXPECT_EQ(Record.AllocSites[0].Info.getAllocCount(), 777U);
EXPECT_EQ(Record.AllocSites[0].Info.getTotalSize(), 888U);
EXPECT_THAT(
Record.AllocSites[1].CallStack,
ElementsAre(Frame(0x300, 33, 30, false), Frame(0x400, 44, 40, true)));
EXPECT_EQ(Record.AllocSites[1].Info.getAllocCount(), 666U);
EXPECT_EQ(Record.AllocSites[1].Info.getTotalSize(), 555U);
EXPECT_THAT(
Record.CallSites,
ElementsAre(
AllOf(testing::Field(&CallSiteInfo::Frames,
ElementsAre(Frame(0x500, 55, 50, true),
Frame(0x600, 66, 60, false))),
testing::Field(&CallSiteInfo::CalleeGuids,
ElementsAre(0x1000, 0x2000))),
AllOf(testing::Field(&CallSiteInfo::Frames,
ElementsAre(Frame(0x700, 77, 70, true),
Frame(0x800, 88, 80, false))),
testing::Field(&CallSiteInfo::CalleeGuids,
ElementsAre(0x3000)))));
}
// Verify that the YAML parser accepts a GUID expressed as a function name.
TEST(MemProf, YAMLParserGUID) {
StringRef YAMLData = R"YAML(
---
HeapProfileRecords:
- GUID: _Z3fooi
AllocSites:
- Callstack:
- {Function: 0x100, LineOffset: 11, Column: 10, IsInlineFrame: true}
MemInfoBlock: {}
CallSites: []
)YAML";
YAMLMemProfReader YAMLReader;
YAMLReader.parse(YAMLData);
IndexedMemProfData MemProfData = YAMLReader.takeMemProfData();
// Verify the entire contents of MemProfData.Records.
ASSERT_THAT(MemProfData.Records, SizeIs(1));
const auto &[GUID, IndexedRecord] = MemProfData.Records.front();
EXPECT_EQ(GUID, memprof::getGUID("_Z3fooi"));
IndexedCallstackIdConverter CSIdConv(MemProfData);
MemProfRecord Record = IndexedRecord.toMemProfRecord(CSIdConv);
ASSERT_THAT(Record.AllocSites, SizeIs(1));
EXPECT_THAT(Record.AllocSites[0].CallStack,
ElementsAre(Frame(0x100, 11, 10, true)));
EXPECT_THAT(Record.CallSites, IsEmpty());
}
template <typename T> std::string serializeInYAML(T &Val) {
std::string Out;
llvm::raw_string_ostream OS(Out);
llvm::yaml::Output Yout(OS);
Yout << Val;
return Out;
}
TEST(MemProf, YAMLWriterFrame) {
Frame F(0x0123456789abcdefULL, 22, 33, true);
std::string Out = serializeInYAML(F);
EXPECT_EQ(Out, R"YAML(---
{ Function: 0x123456789abcdef, LineOffset: 22, Column: 33, IsInlineFrame: true }
...
)YAML");
}
TEST(MemProf, YAMLWriterMIB) {
MemInfoBlock MIB;
MIB.AllocCount = 111;
MIB.TotalSize = 222;
MIB.TotalLifetime = 333;
MIB.TotalLifetimeAccessDensity = 444;
PortableMemInfoBlock PMIB(MIB, getHotColdSchema());
std::string Out = serializeInYAML(PMIB);
EXPECT_EQ(Out, R"YAML(---
AllocCount: 111
TotalSize: 222
TotalLifetime: 333
TotalLifetimeAccessDensity: 444
...
)YAML");
}
} // namespace
} // namespace memprof
} // namespace llvm