llvm-project/llvm/unittests/Analysis/MemoryProfileInfoTest.cpp
Teresa Johnson 1dad6247d2 [MemProf] Add memprof metadata related analysis utilities
Adds a number of utilities that are used to help create and update
memprof related metadata. These will be used during profile matching
and annotation, as well as by the inliner when updating the metadata.
Also adds unit tests for the utilities.

See also related RFCs:
RFC: Sanitizer-based Heap Profiler [1]
RFC: A binary serialization format for MemProf [2]
RFC: IR metadata format for MemProf [3]
(Note that the IR metadata format has changed from the RFC during
implementation, as described in the preceeding patch adding the basic
metadata and verification support.)

Depends on D128141.

Differential Revision: https://reviews.llvm.org/D128854
2022-07-21 13:46:01 -07:00

363 lines
13 KiB
C++

//===- MemoryProfileInfoTest.cpp - Memory Profile Info Unit Tests-===//
//
// 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/Analysis/MemoryProfileInfo.h"
#include "llvm/AsmParser/Parser.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/SourceMgr.h"
#include "gtest/gtest.h"
#include <cstring>
using namespace llvm;
using namespace llvm::memprof;
extern cl::opt<float> MemProfAccessesPerByteColdThreshold;
extern cl::opt<unsigned> MemProfMinLifetimeColdThreshold;
namespace {
class MemoryProfileInfoTest : public testing::Test {
protected:
std::unique_ptr<Module> makeLLVMModule(LLVMContext &C, const char *IR) {
SMDiagnostic Err;
std::unique_ptr<Module> Mod = parseAssemblyString(IR, Err, C);
if (!Mod)
Err.print("MemoryProfileInfoTest", errs());
return Mod;
}
// This looks for a call that has the given value name, which
// is the name of the value being assigned the call return value.
CallBase *findCall(Function &F, const char *Name = nullptr) {
for (auto &BB : F)
for (auto &I : BB)
if (auto *CB = dyn_cast<CallBase>(&I))
if (!Name || CB->getName() == Name)
return CB;
return nullptr;
}
};
// Test getAllocType helper.
// Basic checks on the allocation type for values just above and below
// the thresholds.
TEST_F(MemoryProfileInfoTest, GetAllocType) {
// Long lived with more accesses per byte than threshold is not cold.
EXPECT_EQ(
getAllocType(/*MaxAccessCount=*/MemProfAccessesPerByteColdThreshold + 1,
/*MinSize=*/1,
/*MinLifetime=*/MemProfMinLifetimeColdThreshold * 1000 + 1),
AllocationType::NotCold);
// Long lived with less accesses per byte than threshold is cold.
EXPECT_EQ(
getAllocType(/*MaxAccessCount=*/MemProfAccessesPerByteColdThreshold - 1,
/*MinSize=*/1,
/*MinLifetime=*/MemProfMinLifetimeColdThreshold * 1000 + 1),
AllocationType::Cold);
// Short lived with more accesses per byte than threshold is not cold.
EXPECT_EQ(
getAllocType(/*MaxAccessCount=*/MemProfAccessesPerByteColdThreshold + 1,
/*MinSize=*/1,
/*MinLifetime=*/MemProfMinLifetimeColdThreshold * 1000 - 1),
AllocationType::NotCold);
// Short lived with less accesses per byte than threshold is not cold.
EXPECT_EQ(
getAllocType(/*MaxAccessCount=*/MemProfAccessesPerByteColdThreshold - 1,
/*MinSize=*/1,
/*MinLifetime=*/MemProfMinLifetimeColdThreshold * 1000 - 1),
AllocationType::NotCold);
}
// Test buildCallstackMetadata helper.
TEST_F(MemoryProfileInfoTest, BuildCallStackMD) {
LLVMContext C;
MDNode *CallStack = buildCallstackMetadata({1, 2, 3}, C);
ASSERT_EQ(CallStack->getNumOperands(), 3u);
unsigned ExpectedId = 1;
for (auto &Op : CallStack->operands()) {
auto *StackId = mdconst::dyn_extract<ConstantInt>(Op);
EXPECT_EQ(StackId->getZExtValue(), ExpectedId++);
}
}
// Test CallStackTrie::addCallStack interface taking allocation type and list of
// call stack ids.
// Check that allocations with a single allocation type along all call stacks
// get an attribute instead of memprof metadata.
TEST_F(MemoryProfileInfoTest, Attribute) {
LLVMContext C;
std::unique_ptr<Module> M = makeLLVMModule(C,
R"IR(
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
define i32* @test() {
entry:
%call1 = call noalias dereferenceable_or_null(40) i8* @malloc(i64 noundef 40)
%0 = bitcast i8* %call1 to i32*
%call2 = call noalias dereferenceable_or_null(40) i8* @malloc(i64 noundef 40)
%1 = bitcast i8* %call2 to i32*
ret i32* %1
}
declare dso_local noalias noundef i8* @malloc(i64 noundef)
)IR");
Function *Func = M->getFunction("test");
// First call has all cold contexts.
CallStackTrie Trie1;
Trie1.addCallStack(AllocationType::Cold, {1, 2});
Trie1.addCallStack(AllocationType::Cold, {1, 3, 4});
CallBase *Call1 = findCall(*Func, "call1");
Trie1.buildAndAttachMIBMetadata(Call1);
EXPECT_FALSE(Call1->hasMetadata(LLVMContext::MD_memprof));
EXPECT_TRUE(Call1->hasFnAttr("memprof"));
EXPECT_EQ(Call1->getFnAttr("memprof").getValueAsString(), "cold");
// Second call has all non-cold contexts.
CallStackTrie Trie2;
Trie2.addCallStack(AllocationType::NotCold, {5, 6});
Trie2.addCallStack(AllocationType::NotCold, {5, 7, 8});
CallBase *Call2 = findCall(*Func, "call2");
Trie2.buildAndAttachMIBMetadata(Call2);
EXPECT_FALSE(Call2->hasMetadata(LLVMContext::MD_memprof));
EXPECT_TRUE(Call2->hasFnAttr("memprof"));
EXPECT_EQ(Call2->getFnAttr("memprof").getValueAsString(), "notcold");
}
// Test CallStackTrie::addCallStack interface taking allocation type and list of
// call stack ids.
// Test that an allocation call reached by both cold and non cold call stacks
// gets memprof metadata representing the different allocation type contexts.
TEST_F(MemoryProfileInfoTest, ColdAndNotColdMIB) {
LLVMContext C;
std::unique_ptr<Module> M = makeLLVMModule(C,
R"IR(
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
define i32* @test() {
entry:
%call = call noalias dereferenceable_or_null(40) i8* @malloc(i64 noundef 40)
%0 = bitcast i8* %call to i32*
ret i32* %0
}
declare dso_local noalias noundef i8* @malloc(i64 noundef)
)IR");
Function *Func = M->getFunction("test");
CallStackTrie Trie;
Trie.addCallStack(AllocationType::Cold, {1, 2});
Trie.addCallStack(AllocationType::NotCold, {1, 3});
CallBase *Call = findCall(*Func, "call");
Trie.buildAndAttachMIBMetadata(Call);
EXPECT_FALSE(Call->hasFnAttr("memprof"));
EXPECT_TRUE(Call->hasMetadata(LLVMContext::MD_memprof));
MDNode *MemProfMD = Call->getMetadata(LLVMContext::MD_memprof);
ASSERT_EQ(MemProfMD->getNumOperands(), 2u);
for (auto &MIBOp : MemProfMD->operands()) {
MDNode *MIB = dyn_cast<MDNode>(MIBOp);
MDNode *StackMD = getMIBStackNode(MIB);
ASSERT_NE(StackMD, nullptr);
ASSERT_EQ(StackMD->getNumOperands(), 2u);
auto *StackId = mdconst::dyn_extract<ConstantInt>(StackMD->getOperand(0));
ASSERT_EQ(StackId->getZExtValue(), 1u);
StackId = mdconst::dyn_extract<ConstantInt>(StackMD->getOperand(1));
if (StackId->getZExtValue() == 2u)
EXPECT_EQ(getMIBAllocType(MIB), AllocationType::Cold);
else {
ASSERT_EQ(StackId->getZExtValue(), 3u);
EXPECT_EQ(getMIBAllocType(MIB), AllocationType::NotCold);
}
}
}
// Test CallStackTrie::addCallStack interface taking allocation type and list of
// call stack ids.
// Test that an allocation call reached by multiple call stacks has memprof
// metadata with the contexts trimmed to the minimum context required to
// identify the allocation type.
TEST_F(MemoryProfileInfoTest, TrimmedMIBContext) {
LLVMContext C;
std::unique_ptr<Module> M = makeLLVMModule(C,
R"IR(
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
define i32* @test() {
entry:
%call = call noalias dereferenceable_or_null(40) i8* @malloc(i64 noundef 40)
%0 = bitcast i8* %call to i32*
ret i32* %0
}
declare dso_local noalias noundef i8* @malloc(i64 noundef)
)IR");
Function *Func = M->getFunction("test");
CallStackTrie Trie;
// We should be able to trim the following two and combine into a single MIB
// with the cold context {1, 2}.
Trie.addCallStack(AllocationType::Cold, {1, 2, 3});
Trie.addCallStack(AllocationType::Cold, {1, 2, 4});
// We should be able to trim the following two and combine into a single MIB
// with the non-cold context {1, 5}.
Trie.addCallStack(AllocationType::NotCold, {1, 5, 6});
Trie.addCallStack(AllocationType::NotCold, {1, 5, 7});
CallBase *Call = findCall(*Func, "call");
Trie.buildAndAttachMIBMetadata(Call);
EXPECT_FALSE(Call->hasFnAttr("memprof"));
EXPECT_TRUE(Call->hasMetadata(LLVMContext::MD_memprof));
MDNode *MemProfMD = Call->getMetadata(LLVMContext::MD_memprof);
ASSERT_EQ(MemProfMD->getNumOperands(), 2u);
for (auto &MIBOp : MemProfMD->operands()) {
MDNode *MIB = dyn_cast<MDNode>(MIBOp);
MDNode *StackMD = getMIBStackNode(MIB);
ASSERT_NE(StackMD, nullptr);
ASSERT_EQ(StackMD->getNumOperands(), 2u);
auto *StackId = mdconst::dyn_extract<ConstantInt>(StackMD->getOperand(0));
EXPECT_EQ(StackId->getZExtValue(), 1u);
StackId = mdconst::dyn_extract<ConstantInt>(StackMD->getOperand(1));
if (StackId->getZExtValue() == 2u)
EXPECT_EQ(getMIBAllocType(MIB), AllocationType::Cold);
else {
ASSERT_EQ(StackId->getZExtValue(), 5u);
EXPECT_EQ(getMIBAllocType(MIB), AllocationType::NotCold);
}
}
}
// Test CallStackTrie::addCallStack interface taking memprof MIB metadata.
// Check that allocations annotated with memprof metadata with a single
// allocation type get simplified to an attribute.
TEST_F(MemoryProfileInfoTest, SimplifyMIBToAttribute) {
LLVMContext C;
std::unique_ptr<Module> M = makeLLVMModule(C,
R"IR(
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
define i32* @test() {
entry:
%call1 = call noalias dereferenceable_or_null(40) i8* @malloc(i64 noundef 40), !memprof !0
%0 = bitcast i8* %call1 to i32*
%call2 = call noalias dereferenceable_or_null(40) i8* @malloc(i64 noundef 40), !memprof !3
%1 = bitcast i8* %call2 to i32*
ret i32* %1
}
declare dso_local noalias noundef i8* @malloc(i64 noundef)
!0 = !{!1}
!1 = !{!2, !"cold"}
!2 = !{i64 1, i64 2, i64 3}
!3 = !{!4}
!4 = !{!5, !"notcold"}
!5 = !{i64 4, i64 5, i64 6, i64 7}
)IR");
Function *Func = M->getFunction("test");
// First call has all cold contexts.
CallStackTrie Trie1;
CallBase *Call1 = findCall(*Func, "call1");
MDNode *MemProfMD1 = Call1->getMetadata(LLVMContext::MD_memprof);
ASSERT_EQ(MemProfMD1->getNumOperands(), 1u);
MDNode *MIB1 = dyn_cast<MDNode>(MemProfMD1->getOperand(0));
Trie1.addCallStack(MIB1);
Trie1.buildAndAttachMIBMetadata(Call1);
EXPECT_TRUE(Call1->hasFnAttr("memprof"));
EXPECT_EQ(Call1->getFnAttr("memprof").getValueAsString(), "cold");
// Second call has all non-cold contexts.
CallStackTrie Trie2;
CallBase *Call2 = findCall(*Func, "call2");
MDNode *MemProfMD2 = Call2->getMetadata(LLVMContext::MD_memprof);
ASSERT_EQ(MemProfMD2->getNumOperands(), 1u);
MDNode *MIB2 = dyn_cast<MDNode>(MemProfMD2->getOperand(0));
Trie2.addCallStack(MIB2);
Trie2.buildAndAttachMIBMetadata(Call2);
EXPECT_TRUE(Call2->hasFnAttr("memprof"));
EXPECT_EQ(Call2->getFnAttr("memprof").getValueAsString(), "notcold");
}
// Test CallStackTrie::addCallStack interface taking memprof MIB metadata.
// Test that allocations annotated with memprof metadata with multiple call
// stacks gets new memprof metadata with the contexts trimmed to the minimum
// context required to identify the allocation type.
TEST_F(MemoryProfileInfoTest, ReTrimMIBContext) {
LLVMContext C;
std::unique_ptr<Module> M = makeLLVMModule(C,
R"IR(
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
define i32* @test() {
entry:
%call = call noalias dereferenceable_or_null(40) i8* @malloc(i64 noundef 40), !memprof !0
%0 = bitcast i8* %call to i32*
ret i32* %0
}
declare dso_local noalias noundef i8* @malloc(i64 noundef)
!0 = !{!1, !3, !5, !7}
!1 = !{!2, !"cold"}
!2 = !{i64 1, i64 2, i64 3}
!3 = !{!4, !"cold"}
!4 = !{i64 1, i64 2, i64 4}
!5 = !{!6, !"notcold"}
!6 = !{i64 1, i64 5, i64 6}
!7 = !{!8, !"notcold"}
!8 = !{i64 1, i64 5, i64 7}
)IR");
Function *Func = M->getFunction("test");
CallStackTrie Trie;
ASSERT_TRUE(Trie.empty());
CallBase *Call = findCall(*Func, "call");
MDNode *MemProfMD = Call->getMetadata(LLVMContext::MD_memprof);
for (auto &MIBOp : MemProfMD->operands()) {
MDNode *MIB = dyn_cast<MDNode>(MIBOp);
Trie.addCallStack(MIB);
}
ASSERT_FALSE(Trie.empty());
Trie.buildAndAttachMIBMetadata(Call);
// We should be able to trim the first two and combine into a single MIB
// with the cold context {1, 2}.
// We should be able to trim the second two and combine into a single MIB
// with the non-cold context {1, 5}.
EXPECT_FALSE(Call->hasFnAttr("memprof"));
EXPECT_TRUE(Call->hasMetadata(LLVMContext::MD_memprof));
MemProfMD = Call->getMetadata(LLVMContext::MD_memprof);
ASSERT_EQ(MemProfMD->getNumOperands(), 2u);
for (auto &MIBOp : MemProfMD->operands()) {
MDNode *MIB = dyn_cast<MDNode>(MIBOp);
MDNode *StackMD = getMIBStackNode(MIB);
ASSERT_NE(StackMD, nullptr);
ASSERT_EQ(StackMD->getNumOperands(), 2u);
auto *StackId = mdconst::dyn_extract<ConstantInt>(StackMD->getOperand(0));
EXPECT_EQ(StackId->getZExtValue(), 1u);
StackId = mdconst::dyn_extract<ConstantInt>(StackMD->getOperand(1));
if (StackId->getZExtValue() == 2u)
EXPECT_EQ(getMIBAllocType(MIB), AllocationType::Cold);
else {
ASSERT_EQ(StackId->getZExtValue(), 5u);
EXPECT_EQ(getMIBAllocType(MIB), AllocationType::NotCold);
}
}
}
} // end anonymous namespace