blob: 62307024c47ed42b0174ee0b30311c244c1d1ea9 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#include <lib/fit/defer.h>
#include <lib/object_cache.h>
#include <lib/unittest/unittest.h>
#include <zircon/errors.h>
#include <zircon/time.h>
#include <fbl/alloc_checker.h>
#include <fbl/ref_counted.h>
#include <fbl/ref_ptr.h>
#include <fbl/vector.h>
#include <kernel/auto_preempt_disabler.h>
#include <kernel/event.h>
#include <kernel/thread.h>
#include <ktl/algorithm.h>
#include <ktl/array.h>
#include <ktl/atomic.h>
#include <ktl/move.h>
#include <ktl/optional.h>
namespace {
using object_cache::DefaultAllocator;
using object_cache::Deletable;
using object_cache::ObjectCache;
using object_cache::Option;
using object_cache::UniquePtr;
struct TestAllocator {
static constexpr size_t kSlabSize = DefaultAllocator::kSlabSize;
static zx::status<void*> Allocate() {
allocated_slabs.fetch_add(1, ktl::memory_order_relaxed);
return DefaultAllocator::Allocate();
}
static void Release(void* slab) {
freed_slabs.fetch_add(1, ktl::memory_order_relaxed);
DefaultAllocator::Release(slab);
}
static void CountObjectAllocation() { DefaultAllocator::CountObjectAllocation(); }
static void CountObjectFree() { DefaultAllocator::CountObjectFree(); }
static void CountSlabAllocation() { DefaultAllocator::CountSlabAllocation(); }
static void CountSlabFree() { DefaultAllocator::CountSlabFree(); }
static void ResetCounts() {
allocated_slabs = 0;
freed_slabs = 0;
}
inline static ktl::atomic<int> allocated_slabs{0};
inline static ktl::atomic<int> freed_slabs{0};
};
struct TestObject {
TestObject() { constructor_count++; }
explicit TestObject(int32_t data) : data{data} { constructor_count++; }
~TestObject() { destructor_count++; }
TestObject(const TestObject&) = default;
TestObject& operator=(const TestObject&) = default;
static constexpr int32_t kDefaultDataValue = 0xdeadbeef;
int32_t data{kDefaultDataValue};
uint8_t extra[256 - sizeof(data)];
static void ResetCounts() {
constructor_count = 0;
destructor_count = 0;
}
inline static volatile int constructor_count{0};
inline static volatile int destructor_count{0};
};
struct TestParent : fbl::RefCounted<TestParent> {
TestParent() = default;
~TestParent() { destructor_count++; }
struct Child : fbl::RefCounted<Child>, Deletable<Child, TestAllocator> {
explicit Child(fbl::RefPtr<TestParent> parent) : parent{ktl::move(parent)} {}
fbl::RefPtr<TestParent> parent;
};
zx::status<fbl::RefPtr<Child>> Allocate() {
auto result = allocator.Allocate(fbl::RefPtr{this});
if (result.is_error()) {
return result.take_error();
}
return zx::ok(fbl::AdoptRef(result.value().release()));
}
using Allocator = ObjectCache<Child, Option::Single, TestAllocator>;
static constexpr size_t kObjectsPerSlab = Allocator::objects_per_slab();
static constexpr size_t kReserveSlabs = 1;
Allocator allocator{kReserveSlabs};
static void ResetCounts() { destructor_count = 0; }
inline static volatile int destructor_count{0};
};
template <int retain_slabs, int slab_count, Option option>
bool ObjectCacheTests() {
BEGIN_TEST;
TestObject::ResetCounts();
ASSERT_EQ(0, TestObject::constructor_count);
ASSERT_EQ(0, TestObject::destructor_count);
TestAllocator::ResetCounts();
ASSERT_EQ(0, TestAllocator::allocated_slabs);
ASSERT_EQ(0, TestAllocator::freed_slabs);
const int objects_per_slab = ObjectCache<TestObject>::objects_per_slab();
const int object_count = objects_per_slab * slab_count;
fbl::Vector<UniquePtr<TestObject, TestAllocator>> objects;
fbl::AllocChecker checker;
objects.reserve(object_count, &checker);
ASSERT_TRUE(checker.check());
{
ktl::optional<ObjectCache<TestObject, option, TestAllocator>> object_cache;
if constexpr (option == Option::Single) {
object_cache.emplace(retain_slabs);
} else {
auto result = ObjectCache<TestObject, Option::PerCpu, TestAllocator>::Create(retain_slabs);
ASSERT_TRUE(result.is_ok());
object_cache.emplace(ktl::move(result.value()));
}
// Stay on one CPU during the following tests to verify numeric properties of a single per-CPU
// cache. Accounting for CPU migration during the tests would make them overly complicated for
// little value.
Thread* const current_thread = Thread::Current::Get();
const cpu_mask_t original_affinity_mask = current_thread->GetCpuAffinity();
const auto restore_affinity = fit::defer([original_affinity_mask, current_thread]() {
current_thread->SetCpuAffinity(original_affinity_mask);
});
{
AutoPreemptDisabler preempt_disable;
const cpu_num_t current_cpu = arch_curr_cpu_num();
current_thread->SetCpuAffinity(cpu_num_to_mask(current_cpu));
}
EXPECT_EQ(0u, object_cache->slab_count());
EXPECT_EQ(0, TestObject::constructor_count);
EXPECT_EQ(0, TestObject::destructor_count);
for (int i = 0; i < object_count; i++) {
auto result = object_cache->Allocate(i);
ASSERT_TRUE(result.is_ok());
EXPECT_EQ(i + 1, TestObject::constructor_count);
EXPECT_EQ(0, TestObject::destructor_count);
EXPECT_EQ(i, result->data);
objects.push_back(ktl::move(result.value()), &checker);
ASSERT_TRUE(checker.check());
}
EXPECT_EQ(slab_count, static_cast<int>(object_cache->slab_count()));
EXPECT_EQ(slab_count, TestAllocator::allocated_slabs);
EXPECT_EQ(0, TestAllocator::freed_slabs);
// Release the first slab worth of objects.
for (int i = 0; i < objects_per_slab; i++) {
objects[i] = nullptr;
EXPECT_EQ(object_count, TestObject::constructor_count);
EXPECT_EQ(i + 1, TestObject::destructor_count);
}
EXPECT_EQ(slab_count, TestAllocator::allocated_slabs);
EXPECT_EQ(slab_count <= retain_slabs ? 0 : 1, TestAllocator::freed_slabs);
EXPECT_EQ(TestAllocator::allocated_slabs - TestAllocator::freed_slabs,
static_cast<int>(object_cache->slab_count()));
objects.reset();
EXPECT_EQ(TestObject::constructor_count, TestObject::destructor_count);
EXPECT_EQ(object_count, TestObject::destructor_count);
}
EXPECT_EQ(TestObject::constructor_count, TestObject::destructor_count);
EXPECT_EQ(TestAllocator::allocated_slabs, TestAllocator::freed_slabs);
END_TEST;
}
bool BackreferenceLifetimeTests() {
BEGIN_TEST;
TestParent::ResetCounts();
EXPECT_EQ(0, TestParent::destructor_count);
TestAllocator::ResetCounts();
ASSERT_EQ(0, TestAllocator::allocated_slabs);
ASSERT_EQ(0, TestAllocator::freed_slabs);
fbl::AllocChecker checker;
fbl::RefPtr parent = fbl::AdoptRef(new (&checker) TestParent{});
ASSERT_TRUE(checker.check());
auto result1 = parent->Allocate();
auto result2 = parent->Allocate();
auto result3 = parent->Allocate();
parent.reset();
EXPECT_EQ(0, TestParent::destructor_count);
result1.value().reset();
EXPECT_EQ(0, TestParent::destructor_count);
result2.value().reset();
EXPECT_EQ(0, TestParent::destructor_count);
result3.value().reset();
EXPECT_EQ(1, TestParent::destructor_count);
EXPECT_EQ(TestAllocator::allocated_slabs, TestAllocator::freed_slabs);
END_TEST;
}
bool BackreferenceLifetimeStressTests() {
BEGIN_TEST;
const size_t kStressTestIterations = 1000;
for (size_t iteration = 0; iteration < kStressTestIterations; iteration++) {
TestParent::ResetCounts();
EXPECT_EQ(0, TestParent::destructor_count);
TestAllocator::ResetCounts();
ASSERT_EQ(0, TestAllocator::allocated_slabs);
ASSERT_EQ(0, TestAllocator::freed_slabs);
fbl::AllocChecker checker;
fbl::RefPtr parent = fbl::AdoptRef(new (&checker) TestParent{});
ASSERT_TRUE(checker.check());
struct Control {
TestParent* parent;
AutounsignalEvent allocation_event{};
Event deallocaton_event{};
AutounsignalEvent finished_event{};
ktl::atomic<bool> failed{false};
ktl::atomic<int> count{0};
};
const size_t object_count = TestParent::kObjectsPerSlab;
Control control{.parent = parent.get()};
const auto thread_body = +[](void* arg) -> int {
Control& control = *static_cast<Control*>(arg);
fbl::AllocChecker checker;
fbl::Vector<fbl::RefPtr<TestParent::Child>> objects;
objects.reserve(object_count, &checker);
if (!checker.check()) {
control.failed = true;
return ZX_ERR_NO_MEMORY;
}
for (size_t i = 0; i < object_count; i++) {
auto result = control.parent->Allocate();
if (result.is_error()) {
control.failed = true;
return result.error_value();
}
objects.push_back(ktl::move(result.value()), &checker);
if (!checker.check()) {
control.failed = true;
return ZX_ERR_NO_MEMORY;
}
control.count++;
Thread::Current::Yield();
}
control.allocation_event.Signal();
control.deallocaton_event.Wait();
for (size_t i = 0; i < object_count; i++) {
objects[i] = nullptr;
control.count--;
Thread::Current::Yield();
}
control.finished_event.Signal();
return ZX_OK;
};
const int thread_count = 8;
ktl::array<Thread*, thread_count> threads;
for (Thread*& thread : threads) {
thread = Thread::Create("ObjectCacheRace", thread_body, &control, DEFAULT_PRIORITY);
}
// Resume threads in quick succession to get maximum overlap in the allocation
// phase.
for (Thread* thread : threads) {
thread->Resume();
}
// Wait for each worker to finish allocating children.
while (control.count != thread_count * object_count && !control.failed) {
control.allocation_event.Wait();
}
EXPECT_FALSE(control.failed);
EXPECT_EQ(thread_count, TestAllocator::allocated_slabs);
EXPECT_EQ(0, TestAllocator::freed_slabs);
// Workers should not touch the parent object after they finish allocating
// children.
control.parent = nullptr;
EXPECT_EQ(0, TestParent::destructor_count);
// Children should maintain the lifetime of the parent.
parent = nullptr;
EXPECT_EQ(0, TestParent::destructor_count);
control.deallocaton_event.Signal();
// Wait for each worker to finish freeing children.
while (control.count != 0 && !control.failed) {
control.finished_event.Wait();
}
EXPECT_FALSE(control.failed);
EXPECT_EQ(1, TestParent::destructor_count);
EXPECT_EQ(thread_count, TestAllocator::allocated_slabs);
EXPECT_EQ(thread_count, TestAllocator::freed_slabs);
for (Thread* thread : threads) {
int retcode;
thread->Join(&retcode, ZX_TIME_INFINITE);
EXPECT_EQ(ZX_OK, retcode);
}
}
END_TEST;
}
} // namespace
UNITTEST_START_TESTCASE(object_cache_tests)
UNITTEST("object_cache_tests<0, 2, Single>", (ObjectCacheTests<0, 2, Option::Single>))
UNITTEST("object_cache_tests<1, 2, Single>", (ObjectCacheTests<1, 2, Option::Single>))
UNITTEST("object_cache_tests<2, 2, Single>", (ObjectCacheTests<2, 2, Option::Single>))
UNITTEST("object_cache_tests<0, 2, PerCpu>", (ObjectCacheTests<0, 2, Option::PerCpu>))
UNITTEST("object_cache_tests<1, 2, PerCpu>", (ObjectCacheTests<1, 2, Option::PerCpu>))
UNITTEST("object_cache_tests<2, 2, PerCpu>", (ObjectCacheTests<2, 2, Option::PerCpu>))
UNITTEST("backreference_lifetime_tests", BackreferenceLifetimeTests)
UNITTEST("backreference_lifetime_stress_tests", BackreferenceLifetimeStressTests)
UNITTEST_END_TESTCASE(object_cache_tests, "object_cache", "object_cache tests")