Files
clang-p2996/compiler-rt/lib/xray/tests/unit/segmented_array_test.cc
Dean Michael Berris 25d505953a [XRay] Use preallocated memory for XRay profiling
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
2018-12-07 06:23:06 +00:00

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