blob: 281ab5ef4ae61aa1199cc38914fb61e35d8daa2b [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include <algorithm>
#include <cstdlib>
#include <limits>
#include <random>
#include <string>
#include <vector>
#include <boost/scoped_ptr.hpp>
#include "common/object-pool.h"
#include "runtime/bufferpool/reservation-tracker.h"
#include "runtime/bufferpool/suballocator.h"
#include "runtime/test-env.h"
#include "service/fe-support.h"
#include "testutil/death-test-util.h"
#include "testutil/gtest-util.h"
#include "testutil/rand-util.h"
#include "util/bit-util.h"
#include "common/names.h"
using std::lognormal_distribution;
using std::mt19937;
using std::shuffle;
using std::uniform_int_distribution;
namespace impala {
class SuballocatorTest : public ::testing::Test {
public:
virtual void SetUp() override {
test_env_.reset(new TestEnv);
test_env_->DisableBufferPool();
ASSERT_OK(test_env_->Init());
RandTestUtil::SeedRng("SUBALLOCATOR_TEST_SEED", &rng_);
profile_ = RuntimeProfile::Create(&obj_pool_, "test profile");
}
virtual void TearDown() override {
for (unique_ptr<BufferPool::ClientHandle>& client : clients_) {
buffer_pool_->DeregisterClient(client.get());
}
clients_.clear();
buffer_pool_.reset();
global_reservation_.Close();
obj_pool_.Clear();
}
/// The minimum buffer size used in most tests. Chosen so that the buffer is split
/// at least several ways.
const static int64_t TEST_BUFFER_LEN = Suballocator::MIN_ALLOCATION_BYTES * 16;
protected:
/// Initialize 'buffer_pool_' and 'global_reservation_' with a limit of 'total_mem'
/// bytes of buffers of minimum length 'min_buffer_len'.
void InitPool(int64_t min_buffer_len, int total_mem) {
global_reservation_.InitRootTracker(nullptr, total_mem);
buffer_pool_.reset(
new BufferPool(test_env_->metrics(), min_buffer_len, total_mem, 0));
}
/// Register a client with 'buffer_pool_'. The client is automatically deregistered
/// and freed at the end of the test.
void RegisterClient(
ReservationTracker* parent_reservation, BufferPool::ClientHandle** client) {
clients_.push_back(make_unique<BufferPool::ClientHandle>());
*client = clients_.back().get();
ASSERT_OK(buffer_pool_->RegisterClient("test client", NULL, parent_reservation, NULL,
numeric_limits<int64_t>::max(), profile_, *client));
}
/// Assert that the memory for all of the suballocations is writable and disjoint by
/// writing a distinct value to each suballocation and reading it back. Only works for
/// suballocations at least 8 bytes in size.
void AssertMemoryValid(const vector<unique_ptr<Suballocation>>& allocs);
/// Free all the suballocations and clear the vector.
static void FreeAllocations(
Suballocator* allocator, vector<unique_ptr<Suballocation>>* allocs) {
for (auto& alloc : *allocs) allocator->Free(move(alloc));
allocs->clear();
}
static void ExpectReservationUnused(BufferPool::ClientHandle* client) {
EXPECT_EQ(client->GetUsedReservation(), 0) << client->DebugString();
}
BufferPool* buffer_pool() { return buffer_pool_.get(); }
/// Pool for objects with per-test lifetime. Cleared after every test.
ObjectPool obj_pool_;
/// The top-level global reservation. Initialized in InitPool() and closed after every
/// test.
ReservationTracker global_reservation_;
/// The buffer pool. Initialized in InitPool() and reset after every test.
scoped_ptr<BufferPool> buffer_pool_;
/// Clients for the buffer pool. Deregistered and freed after every test.
vector<unique_ptr<BufferPool::ClientHandle>> clients_;
boost::scoped_ptr<TestEnv> test_env_;
/// Global profile - recreated for every test.
RuntimeProfile* profile_;
/// Per-test random number generator. Seeded before every test.
mt19937 rng_;
};
const int64_t SuballocatorTest::TEST_BUFFER_LEN;
/// Basic test to make sure that we can make multiple suballocations of the same size
/// while using the expected number of buffers.
TEST_F(SuballocatorTest, SameSizeAllocations) {
const int64_t TOTAL_MEM = TEST_BUFFER_LEN * 100;
InitPool(TEST_BUFFER_LEN, TOTAL_MEM);
BufferPool::ClientHandle* client;
RegisterClient(&global_reservation_, &client);
Suballocator allocator(buffer_pool(), client, TEST_BUFFER_LEN);
vector<unique_ptr<Suballocation>> allocs;
// Make suballocations smaller than the buffer size.
const int64_t ALLOC_SIZE = TEST_BUFFER_LEN / 4;
int64_t allocated_mem = 0;
while (allocated_mem < TOTAL_MEM) {
allocs.emplace_back();
ASSERT_OK(allocator.Allocate(ALLOC_SIZE, &allocs.back()));
ASSERT_TRUE(allocs.back() != nullptr) << ALLOC_SIZE << " " << allocated_mem << " "
<< global_reservation_.DebugString();
allocated_mem += ALLOC_SIZE;
}
// Attempts to allocate more memory should fail gracefully.
const int64_t MAX_ALLOC_SIZE = 1L << 24;
for (int alloc_size = 1; alloc_size <= MAX_ALLOC_SIZE; alloc_size *= 2) {
unique_ptr<Suballocation> failed_alloc;
ASSERT_OK(allocator.Allocate(alloc_size, &failed_alloc));
ASSERT_TRUE(failed_alloc == nullptr) << alloc_size << " " << allocated_mem << " "
<< global_reservation_.DebugString();
}
AssertMemoryValid(allocs);
// Check that reservation usage matches the amount allocated.
EXPECT_EQ(client->GetUsedReservation(), allocated_mem)
<< global_reservation_.DebugString();
FreeAllocations(&allocator, &allocs);
ExpectReservationUnused(client);
}
/// Check behaviour of zero-length allocation.
TEST_F(SuballocatorTest, ZeroLengthAllocation) {
const int64_t TOTAL_MEM = TEST_BUFFER_LEN * 100;
InitPool(TEST_BUFFER_LEN, TOTAL_MEM);
BufferPool::ClientHandle* client;
RegisterClient(&global_reservation_, &client);
Suballocator allocator(buffer_pool(), client, TEST_BUFFER_LEN);
unique_ptr<Suballocation> alloc;
// Zero-length allocations are allowed and rounded up to the minimum size.
ASSERT_OK(allocator.Allocate(0, &alloc));
ASSERT_TRUE(alloc != nullptr) << global_reservation_.DebugString();
EXPECT_EQ(alloc->len(), Suballocator::MIN_ALLOCATION_BYTES);
allocator.Free(move(alloc));
ExpectReservationUnused(client);
}
/// Check behaviour of out-of-range allocation.
TEST_F(SuballocatorTest, OutOfRangeAllocations) {
const int64_t TOTAL_MEM = TEST_BUFFER_LEN * 100;
InitPool(TEST_BUFFER_LEN, TOTAL_MEM);
BufferPool::ClientHandle* client;
RegisterClient(&global_reservation_, &client);
Suballocator allocator(buffer_pool(), client, TEST_BUFFER_LEN);
unique_ptr<Suballocation> alloc;
// Negative allocations are not allowed and cause a DCHECK.
IMPALA_ASSERT_DEBUG_DEATH(allocator.Allocate(-1, &alloc), "");
// Too-large allocations fail gracefully.
ASSERT_FALSE(allocator.Allocate(Suballocator::MAX_ALLOCATION_BYTES + 1, &alloc).ok())
<< global_reservation_.DebugString();
ExpectReservationUnused(client);
}
/// Basic test to make sure that non-power-of-two suballocations are handled as expected
/// by rounding up.
TEST_F(SuballocatorTest, NonPowerOfTwoAllocations) {
const int64_t TOTAL_MEM = TEST_BUFFER_LEN * 128;
InitPool(TEST_BUFFER_LEN, TOTAL_MEM);
BufferPool::ClientHandle* client;
RegisterClient(&global_reservation_, &client);
Suballocator allocator(buffer_pool(), client, TEST_BUFFER_LEN);
vector<int64_t> alloc_sizes;
// Multiply by 7 to get some unusual sizes.
for (int64_t alloc_size = 7; BitUtil::RoundUpToPowerOfTwo(alloc_size) <= TOTAL_MEM;
alloc_size *= 7) {
alloc_sizes.push_back(alloc_size);
}
// Test edge cases around power-of-two-sizes.
for (int64_t power_of_two = 2; power_of_two <= TOTAL_MEM; power_of_two *= 2) {
alloc_sizes.push_back(power_of_two - 1);
if (power_of_two != TOTAL_MEM) alloc_sizes.push_back(power_of_two + 1);
}
for (int64_t alloc_size : alloc_sizes) {
unique_ptr<Suballocation> alloc;
ASSERT_OK(allocator.Allocate(alloc_size, &alloc));
ASSERT_TRUE(alloc != nullptr) << alloc_size << " "
<< global_reservation_.DebugString();
// Check that it was rounded up to a power-of-two.
EXPECT_EQ(alloc->len(), max(Suballocator::MIN_ALLOCATION_BYTES,
BitUtil::RoundUpToPowerOfTwo(alloc_size)));
EXPECT_EQ(max(TEST_BUFFER_LEN, alloc->len()), client->GetUsedReservation())
<< global_reservation_.DebugString();
memset(alloc->data(), 0, alloc->len()); // Check memory is writable.
allocator.Free(move(alloc));
}
ExpectReservationUnused(client);
}
/// Test that simulates hash table's patterns of doubling suballocations and validates
/// that memory does not become fragmented.
TEST_F(SuballocatorTest, DoublingAllocations) {
const int64_t TOTAL_MEM = TEST_BUFFER_LEN * 100;
InitPool(TEST_BUFFER_LEN, TOTAL_MEM);
BufferPool::ClientHandle* client;
RegisterClient(&global_reservation_, &client);
Suballocator allocator(buffer_pool(), client, TEST_BUFFER_LEN);
const int NUM_ALLOCS = 16;
vector<unique_ptr<Suballocation>> allocs(NUM_ALLOCS);
// Start with suballocations smaller than the page.
for (int64_t curr_alloc_size = TEST_BUFFER_LEN / 8;
curr_alloc_size * NUM_ALLOCS < TOTAL_MEM; curr_alloc_size *= 2) {
// Randomise the order of suballocations so that coalescing happens in different ways.
shuffle(allocs.begin(), allocs.end(), rng_);
for (unique_ptr<Suballocation>& alloc : allocs) {
unique_ptr<Suballocation> old_alloc = move(alloc);
ASSERT_OK(allocator.Allocate(curr_alloc_size, &alloc));
if (old_alloc != nullptr) allocator.Free(move(old_alloc));
}
AssertMemoryValid(allocs);
// Test that the memory isn't fragmented more than expected. In the worst case, the
// suballocations should require an extra page.
//
// If curr_alloc_size is at least the buffer size, there is no fragmentation because
// all previous suballocations are coalesced and freed, and all new suballocations
// are backed by a newly-allocated buffer.
//
// If curr_alloc_size is less than the buffer size, we lose at most a buffer to
// fragmentation because previous suballocations are incrementally freed in a way
// such that they can always be coalesced and reused. At least N/2 out of N of the
// Free() calls in an iteration result in the free memory being coalesced. This is
// because either the buddy is freed earlier or later, and the coalescing must happen
// either in the current Free() call or a later Free() call. Therefore at least
// N/2 - 1 out of N Allocate() calls follow a Free() call that coalesced memory
// and can therefore alway recycle a coalesced suballocation instead of allocating
// additional buffers.
//
// In the worst case we end up with two buffers with gaps: one buffer carried over
// from the previous iteration with a single curr_alloc_size gap (if the last Free()
// coalesced two buddies of curr_alloc_size / 2) and one buffer with only
// 'curr_alloc_size' bytes in use (if an Allocate() call couldn't recycle memory and
// had to allocate a new buffer).
EXPECT_LE(client->GetUsedReservation(),
TEST_BUFFER_LEN + max(TEST_BUFFER_LEN, curr_alloc_size * NUM_ALLOCS));
}
// Check that reservation usage behaves as expected.
FreeAllocations(&allocator, &allocs);
ExpectReservationUnused(client);
}
/// Do some randomised testing of the allocator. Simulate some interesting patterns with
/// a mix of long and short runs of suballocations of variable size. Try to ensure that we
/// spend some time with the allocator near its upper limit, where most suballocations
/// will fail, and also in other parts of its range.
TEST_F(SuballocatorTest, RandomAllocations) {
const int64_t TOTAL_MEM = TEST_BUFFER_LEN * 1000;
InitPool(TEST_BUFFER_LEN, TOTAL_MEM);
BufferPool::ClientHandle* client;
RegisterClient(&global_reservation_, &client);
Suballocator allocator(buffer_pool(), client, TEST_BUFFER_LEN);
vector<unique_ptr<Suballocation>> allocs;
int64_t allocated_mem = 0;
for (int iter = 0; iter < 1000; ++iter) {
// We want to make runs of suballocations and frees. Use lognormal distribution so
// that runs are mostly short, but there are some long runs mixed in.
int num_allocs = max(1, static_cast<int>(lognormal_distribution<double>(3, 1)(rng_)));
const bool alloc = uniform_int_distribution<int>(0, 1)(rng_);
if (alloc) {
const int64_t remaining_mem_per_alloc = (TOTAL_MEM - allocated_mem) / num_allocs;
// Fraction is ~0.12 on average but sometimes ranges above 1.0 so that we'll hit the
// max reservation and suballocations will fail.
double fraction_to_alloc = lognormal_distribution<double>(2, 1)(rng_) / 100.;
int64_t alloc_size = max(8L, BitUtil::RoundUpToPowerOfTwo(static_cast<int64_t>(
fraction_to_alloc * remaining_mem_per_alloc)));
for (int i = 0; i < num_allocs; ++i) {
unique_ptr<Suballocation> alloc;
ASSERT_OK(allocator.Allocate(alloc_size, &alloc));
if (alloc != nullptr) {
EXPECT_EQ(alloc->len(), max(alloc_size, Suballocator::MIN_ALLOCATION_BYTES));
allocated_mem += alloc->len();
allocs.push_back(move(alloc));
} else {
LOG(INFO) << "Failed to alloc " << alloc_size << " consumed " << allocated_mem
<< "/" << TOTAL_MEM;
}
}
} else {
// Free a random subset of suballocations.
num_allocs = min<int>(num_allocs, allocs.size());
shuffle(allocs.end() - num_allocs, allocs.end(), rng_);
for (int i = 0; i < num_allocs; ++i) {
allocated_mem -= allocs.back()->len();
allocator.Free(move(allocs.back()));
allocs.pop_back();
}
}
// Occasionally check that the suballocations are valid.
if (iter % 50 == 0) AssertMemoryValid(allocs);
}
// Check that memory is released when suballocations are freed.
FreeAllocations(&allocator, &allocs);
ExpectReservationUnused(client);
}
void SuballocatorTest::AssertMemoryValid(
const vector<unique_ptr<Suballocation>>& allocs) {
for (int64_t i = 0; i < allocs.size(); ++i) {
const unique_ptr<Suballocation>& alloc = allocs[i];
ASSERT_GE(alloc->len(), 8);
// Memory should be 8-byte aligned.
ASSERT_EQ(0, reinterpret_cast<uint64_t>(alloc->data()) % 8) << alloc->data();
for (int64_t offset = 0; offset < alloc->len(); offset += 8) {
*reinterpret_cast<int64_t*>(alloc->data() + offset) = i;
}
}
for (int64_t i = 0; i < allocs.size(); ++i) {
const unique_ptr<Suballocation>& alloc = allocs[i];
for (int64_t offset = 0; offset < alloc->len(); offset += 8) {
ASSERT_EQ(*reinterpret_cast<int64_t*>(alloc->data() + offset), i)
<< i << " " << alloc->data() << " " << offset;
}
}
}
}