Summary: This change builds upon D54989, which removes memory allocation from the critical path of the profiling implementation. This also changes the API for the profile collection service, to take ownership of the memory and associated data structures per-thread. The consolidation of the memory allocation allows us to do two things: - Limits the amount of memory used by the profiling implementation, associating preallocated buffers instead of allocating memory on-demand. - Consolidate the memory initialisation and cleanup by relying on the buffer queue's reference counting implementation. We find a number of places which also display some problematic behaviour, including: - Off-by-factor bug in the allocator implementation. - Unrolling semantics in cases of "memory exhausted" situations, when managing the state of the function call trie. We also add a few test cases which verify our understanding of the behaviour of the system, with important edge-cases (especially for memory-exhausted cases) in the segmented array and profile collector unit tests. Depends on D54989. Reviewers: mboerger Subscribers: dschuff, mgorny, dmgreen, jfb, llvm-commits Differential Revision: https://reviews.llvm.org/D55249 llvm-svn: 348568
350 lines
10 KiB
C++
350 lines
10 KiB
C++
#include "test_helpers.h"
|
|
#include "xray_segmented_array.h"
|
|
#include "gmock/gmock.h"
|
|
#include "gtest/gtest.h"
|
|
#include <algorithm>
|
|
#include <numeric>
|
|
#include <vector>
|
|
|
|
namespace __xray {
|
|
namespace {
|
|
|
|
using ::testing::SizeIs;
|
|
|
|
struct TestData {
|
|
s64 First;
|
|
s64 Second;
|
|
|
|
// Need a constructor for emplace operations.
|
|
TestData(s64 F, s64 S) : First(F), Second(S) {}
|
|
};
|
|
|
|
void PrintTo(const TestData &D, std::ostream *OS) {
|
|
*OS << "{ " << D.First << ", " << D.Second << " }";
|
|
}
|
|
|
|
TEST(SegmentedArrayTest, ConstructWithAllocators) {
|
|
using AllocatorType = typename Array<TestData>::AllocatorType;
|
|
AllocatorType A(1 << 4);
|
|
Array<TestData> Data(A);
|
|
(void)Data;
|
|
}
|
|
|
|
TEST(SegmentedArrayTest, ConstructAndPopulate) {
|
|
using AllocatorType = typename Array<TestData>::AllocatorType;
|
|
AllocatorType A(1 << 4);
|
|
Array<TestData> data(A);
|
|
ASSERT_NE(data.Append(TestData{0, 0}), nullptr);
|
|
ASSERT_NE(data.Append(TestData{1, 1}), nullptr);
|
|
ASSERT_EQ(data.size(), 2u);
|
|
}
|
|
|
|
TEST(SegmentedArrayTest, ConstructPopulateAndLookup) {
|
|
using AllocatorType = typename Array<TestData>::AllocatorType;
|
|
AllocatorType A(1 << 4);
|
|
Array<TestData> data(A);
|
|
ASSERT_NE(data.Append(TestData{0, 1}), nullptr);
|
|
ASSERT_EQ(data.size(), 1u);
|
|
ASSERT_EQ(data[0].First, 0);
|
|
ASSERT_EQ(data[0].Second, 1);
|
|
}
|
|
|
|
TEST(SegmentedArrayTest, PopulateWithMoreElements) {
|
|
using AllocatorType = typename Array<TestData>::AllocatorType;
|
|
AllocatorType A(1 << 24);
|
|
Array<TestData> data(A);
|
|
static const auto kMaxElements = 100u;
|
|
for (auto I = 0u; I < kMaxElements; ++I) {
|
|
ASSERT_NE(data.Append(TestData{I, I + 1}), nullptr);
|
|
}
|
|
ASSERT_EQ(data.size(), kMaxElements);
|
|
for (auto I = 0u; I < kMaxElements; ++I) {
|
|
ASSERT_EQ(data[I].First, I);
|
|
ASSERT_EQ(data[I].Second, I + 1);
|
|
}
|
|
}
|
|
|
|
TEST(SegmentedArrayTest, AppendEmplace) {
|
|
using AllocatorType = typename Array<TestData>::AllocatorType;
|
|
AllocatorType A(1 << 4);
|
|
Array<TestData> data(A);
|
|
ASSERT_NE(data.AppendEmplace(1, 1), nullptr);
|
|
ASSERT_EQ(data[0].First, 1);
|
|
ASSERT_EQ(data[0].Second, 1);
|
|
}
|
|
|
|
TEST(SegmentedArrayTest, AppendAndTrim) {
|
|
using AllocatorType = typename Array<TestData>::AllocatorType;
|
|
AllocatorType A(1 << 4);
|
|
Array<TestData> data(A);
|
|
ASSERT_NE(data.AppendEmplace(1, 1), nullptr);
|
|
ASSERT_EQ(data.size(), 1u);
|
|
data.trim(1);
|
|
ASSERT_EQ(data.size(), 0u);
|
|
ASSERT_TRUE(data.empty());
|
|
}
|
|
|
|
TEST(SegmentedArrayTest, IteratorAdvance) {
|
|
using AllocatorType = typename Array<TestData>::AllocatorType;
|
|
AllocatorType A(1 << 4);
|
|
Array<TestData> data(A);
|
|
ASSERT_TRUE(data.empty());
|
|
ASSERT_EQ(data.begin(), data.end());
|
|
auto I0 = data.begin();
|
|
ASSERT_EQ(I0++, data.begin());
|
|
ASSERT_NE(I0, data.begin());
|
|
for (const auto &D : data) {
|
|
(void)D;
|
|
FAIL();
|
|
}
|
|
ASSERT_NE(data.AppendEmplace(1, 1), nullptr);
|
|
ASSERT_EQ(data.size(), 1u);
|
|
ASSERT_NE(data.begin(), data.end());
|
|
auto &D0 = *data.begin();
|
|
ASSERT_EQ(D0.First, 1);
|
|
ASSERT_EQ(D0.Second, 1);
|
|
}
|
|
|
|
TEST(SegmentedArrayTest, IteratorRetreat) {
|
|
using AllocatorType = typename Array<TestData>::AllocatorType;
|
|
AllocatorType A(1 << 4);
|
|
Array<TestData> data(A);
|
|
ASSERT_TRUE(data.empty());
|
|
ASSERT_EQ(data.begin(), data.end());
|
|
ASSERT_NE(data.AppendEmplace(1, 1), nullptr);
|
|
ASSERT_EQ(data.size(), 1u);
|
|
ASSERT_NE(data.begin(), data.end());
|
|
auto &D0 = *data.begin();
|
|
ASSERT_EQ(D0.First, 1);
|
|
ASSERT_EQ(D0.Second, 1);
|
|
|
|
auto I0 = data.end();
|
|
ASSERT_EQ(I0--, data.end());
|
|
ASSERT_NE(I0, data.end());
|
|
ASSERT_EQ(I0, data.begin());
|
|
ASSERT_EQ(I0->First, 1);
|
|
ASSERT_EQ(I0->Second, 1);
|
|
}
|
|
|
|
TEST(SegmentedArrayTest, IteratorTrimBehaviour) {
|
|
using AllocatorType = typename Array<TestData>::AllocatorType;
|
|
AllocatorType A(1 << 20);
|
|
Array<TestData> Data(A);
|
|
ASSERT_TRUE(Data.empty());
|
|
auto I0Begin = Data.begin(), I0End = Data.end();
|
|
// Add enough elements in Data to have more than one chunk.
|
|
constexpr auto Segment = Array<TestData>::SegmentSize;
|
|
constexpr auto SegmentX2 = Segment * 2;
|
|
for (auto i = SegmentX2; i > 0u; --i) {
|
|
Data.AppendEmplace(static_cast<s64>(i), static_cast<s64>(i));
|
|
}
|
|
ASSERT_EQ(Data.size(), SegmentX2);
|
|
{
|
|
auto &Back = Data.back();
|
|
ASSERT_EQ(Back.First, 1);
|
|
ASSERT_EQ(Back.Second, 1);
|
|
}
|
|
|
|
// Trim one chunk's elements worth.
|
|
Data.trim(Segment);
|
|
ASSERT_EQ(Data.size(), Segment);
|
|
|
|
// Check that we are still able to access 'back' properly.
|
|
{
|
|
auto &Back = Data.back();
|
|
ASSERT_EQ(Back.First, static_cast<s64>(Segment + 1));
|
|
ASSERT_EQ(Back.Second, static_cast<s64>(Segment + 1));
|
|
}
|
|
|
|
// Then trim until it's empty.
|
|
Data.trim(Segment);
|
|
ASSERT_TRUE(Data.empty());
|
|
|
|
// Here our iterators should be the same.
|
|
auto I1Begin = Data.begin(), I1End = Data.end();
|
|
EXPECT_EQ(I0Begin, I1Begin);
|
|
EXPECT_EQ(I0End, I1End);
|
|
|
|
// Then we ensure that adding elements back works just fine.
|
|
for (auto i = SegmentX2; i > 0u; --i) {
|
|
Data.AppendEmplace(static_cast<s64>(i), static_cast<s64>(i));
|
|
}
|
|
EXPECT_EQ(Data.size(), SegmentX2);
|
|
}
|
|
|
|
TEST(SegmentedArrayTest, HandleExhaustedAllocator) {
|
|
using AllocatorType = typename Array<TestData>::AllocatorType;
|
|
constexpr auto Segment = Array<TestData>::SegmentSize;
|
|
constexpr auto MaxElements = Array<TestData>::ElementsPerSegment;
|
|
AllocatorType A(Segment);
|
|
Array<TestData> Data(A);
|
|
for (auto i = MaxElements; i > 0u; --i)
|
|
EXPECT_NE(Data.AppendEmplace(static_cast<s64>(i), static_cast<s64>(i)),
|
|
nullptr);
|
|
EXPECT_EQ(Data.AppendEmplace(0, 0), nullptr);
|
|
EXPECT_THAT(Data, SizeIs(MaxElements));
|
|
|
|
// Trimming more elements than there are in the container should be fine.
|
|
Data.trim(MaxElements + 1);
|
|
EXPECT_THAT(Data, SizeIs(0u));
|
|
}
|
|
|
|
struct ShadowStackEntry {
|
|
uint64_t EntryTSC = 0;
|
|
uint64_t *NodePtr = nullptr;
|
|
ShadowStackEntry(uint64_t T, uint64_t *N) : EntryTSC(T), NodePtr(N) {}
|
|
};
|
|
|
|
TEST(SegmentedArrayTest, SimulateStackBehaviour) {
|
|
using AllocatorType = typename Array<ShadowStackEntry>::AllocatorType;
|
|
AllocatorType A(1 << 10);
|
|
Array<ShadowStackEntry> Data(A);
|
|
static uint64_t Dummy = 0;
|
|
constexpr uint64_t Max = 9;
|
|
|
|
for (uint64_t i = 0; i < Max; ++i) {
|
|
auto P = Data.Append({i, &Dummy});
|
|
ASSERT_NE(P, nullptr);
|
|
ASSERT_EQ(P->NodePtr, &Dummy);
|
|
auto &Back = Data.back();
|
|
ASSERT_EQ(Back.NodePtr, &Dummy);
|
|
ASSERT_EQ(Back.EntryTSC, i);
|
|
}
|
|
|
|
// Simulate a stack by checking the data from the end as we're trimming.
|
|
auto Counter = Max;
|
|
ASSERT_EQ(Data.size(), size_t(Max));
|
|
while (!Data.empty()) {
|
|
const auto &Top = Data.back();
|
|
uint64_t *TopNode = Top.NodePtr;
|
|
EXPECT_EQ(TopNode, &Dummy) << "Counter = " << Counter;
|
|
Data.trim(1);
|
|
--Counter;
|
|
ASSERT_EQ(Data.size(), size_t(Counter));
|
|
}
|
|
}
|
|
|
|
TEST(SegmentedArrayTest, PlacementNewOnAlignedStorage) {
|
|
using AllocatorType = typename Array<ShadowStackEntry>::AllocatorType;
|
|
typename std::aligned_storage<sizeof(AllocatorType),
|
|
alignof(AllocatorType)>::type AllocatorStorage;
|
|
new (&AllocatorStorage) AllocatorType(1 << 10);
|
|
auto *A = reinterpret_cast<AllocatorType *>(&AllocatorStorage);
|
|
typename std::aligned_storage<sizeof(Array<ShadowStackEntry>),
|
|
alignof(Array<ShadowStackEntry>)>::type
|
|
ArrayStorage;
|
|
new (&ArrayStorage) Array<ShadowStackEntry>(*A);
|
|
auto *Data = reinterpret_cast<Array<ShadowStackEntry> *>(&ArrayStorage);
|
|
|
|
static uint64_t Dummy = 0;
|
|
constexpr uint64_t Max = 9;
|
|
|
|
for (uint64_t i = 0; i < Max; ++i) {
|
|
auto P = Data->Append({i, &Dummy});
|
|
ASSERT_NE(P, nullptr);
|
|
ASSERT_EQ(P->NodePtr, &Dummy);
|
|
auto &Back = Data->back();
|
|
ASSERT_EQ(Back.NodePtr, &Dummy);
|
|
ASSERT_EQ(Back.EntryTSC, i);
|
|
}
|
|
|
|
// Simulate a stack by checking the data from the end as we're trimming.
|
|
auto Counter = Max;
|
|
ASSERT_EQ(Data->size(), size_t(Max));
|
|
while (!Data->empty()) {
|
|
const auto &Top = Data->back();
|
|
uint64_t *TopNode = Top.NodePtr;
|
|
EXPECT_EQ(TopNode, &Dummy) << "Counter = " << Counter;
|
|
Data->trim(1);
|
|
--Counter;
|
|
ASSERT_EQ(Data->size(), size_t(Counter));
|
|
}
|
|
|
|
// Once the stack is exhausted, we re-use the storage.
|
|
for (uint64_t i = 0; i < Max; ++i) {
|
|
auto P = Data->Append({i, &Dummy});
|
|
ASSERT_NE(P, nullptr);
|
|
ASSERT_EQ(P->NodePtr, &Dummy);
|
|
auto &Back = Data->back();
|
|
ASSERT_EQ(Back.NodePtr, &Dummy);
|
|
ASSERT_EQ(Back.EntryTSC, i);
|
|
}
|
|
|
|
// We re-initialize the storage, by calling the destructor and
|
|
// placement-new'ing again.
|
|
Data->~Array();
|
|
A->~AllocatorType();
|
|
new (A) AllocatorType(1 << 10);
|
|
new (Data) Array<ShadowStackEntry>(*A);
|
|
|
|
// Then re-do the test.
|
|
for (uint64_t i = 0; i < Max; ++i) {
|
|
auto P = Data->Append({i, &Dummy});
|
|
ASSERT_NE(P, nullptr);
|
|
ASSERT_EQ(P->NodePtr, &Dummy);
|
|
auto &Back = Data->back();
|
|
ASSERT_EQ(Back.NodePtr, &Dummy);
|
|
ASSERT_EQ(Back.EntryTSC, i);
|
|
}
|
|
|
|
// Simulate a stack by checking the data from the end as we're trimming.
|
|
Counter = Max;
|
|
ASSERT_EQ(Data->size(), size_t(Max));
|
|
while (!Data->empty()) {
|
|
const auto &Top = Data->back();
|
|
uint64_t *TopNode = Top.NodePtr;
|
|
EXPECT_EQ(TopNode, &Dummy) << "Counter = " << Counter;
|
|
Data->trim(1);
|
|
--Counter;
|
|
ASSERT_EQ(Data->size(), size_t(Counter));
|
|
}
|
|
|
|
// Once the stack is exhausted, we re-use the storage.
|
|
for (uint64_t i = 0; i < Max; ++i) {
|
|
auto P = Data->Append({i, &Dummy});
|
|
ASSERT_NE(P, nullptr);
|
|
ASSERT_EQ(P->NodePtr, &Dummy);
|
|
auto &Back = Data->back();
|
|
ASSERT_EQ(Back.NodePtr, &Dummy);
|
|
ASSERT_EQ(Back.EntryTSC, i);
|
|
}
|
|
}
|
|
|
|
TEST(SegmentedArrayTest, ArrayOfPointersIteratorAccess) {
|
|
using PtrArray = Array<int *>;
|
|
PtrArray::AllocatorType Alloc(16384);
|
|
Array<int *> A(Alloc);
|
|
static constexpr size_t Count = 100;
|
|
std::vector<int> Integers(Count);
|
|
std::iota(Integers.begin(), Integers.end(), 0);
|
|
for (auto &I : Integers)
|
|
ASSERT_NE(A.Append(&I), nullptr);
|
|
int V = 0;
|
|
ASSERT_EQ(A.size(), Count);
|
|
for (auto P : A) {
|
|
ASSERT_NE(P, nullptr);
|
|
ASSERT_EQ(*P, V++);
|
|
}
|
|
}
|
|
|
|
TEST(SegmentedArrayTest, ArrayOfPointersIteratorAccessExhaustion) {
|
|
using PtrArray = Array<int *>;
|
|
PtrArray::AllocatorType Alloc(4096);
|
|
Array<int *> A(Alloc);
|
|
static constexpr size_t Count = 1000;
|
|
std::vector<int> Integers(Count);
|
|
std::iota(Integers.begin(), Integers.end(), 0);
|
|
for (auto &I : Integers)
|
|
if (A.Append(&I) == nullptr)
|
|
break;
|
|
int V = 0;
|
|
ASSERT_LT(A.size(), Count);
|
|
for (auto P : A) {
|
|
ASSERT_NE(P, nullptr);
|
|
ASSERT_EQ(*P, V++);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace __xray
|