blob: 8b2604a230deae1328638c92998c07a46c18caef [file] [log] [blame]
// Copyright 2016 The Cobalt Authors. All Rights Reserved.
//
// Licensed 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 "starboard/memory_reporter.h"
#include "starboard/common/mutex.h"
#include "starboard/configuration.h"
#include "starboard/memory.h"
#include "starboard/thread.h"
#include "starboard/time.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace starboard {
namespace nplb {
namespace {
// This thread local boolean is used to filter allocations and
// 1) Prevent allocations from other threads.
// 2) Selectively disable allocations so that unintended allocs from
// gTest (ASSERT_XXX) don't cause the current test to fail.
void InitMemoryTrackingState_ThreadLocal();
bool GetMemoryTrackingEnabled_ThreadLocal();
void SetMemoryTrackingEnabled_ThreadLocal(bool value);
// Scoped object that temporary turns off MemoryTracking. This is needed
// to avoid allocations from gTest being inserted into the memory tracker.
struct NoMemTracking {
bool prev_val;
NoMemTracking() {
prev_val = GetMemoryTrackingEnabled_ThreadLocal();
SetMemoryTrackingEnabled_ThreadLocal(false);
}
~NoMemTracking() { SetMemoryTrackingEnabled_ThreadLocal(prev_val); }
};
// EXPECT_XXX and ASSERT_XXX allocate memory, a big no-no when
// for unit testing allocations. These overrides disable memory
// tracking for the duration of the EXPECT and ASSERT operations.
#define EXPECT_EQ_NO_TRACKING(A, B) \
{ \
NoMemTracking no_memory_tracking_in_this_scope; \
EXPECT_EQ(A, B); \
}
#define EXPECT_NE_NO_TRACKING(A, B) \
{ \
NoMemTracking no_memory_tracking_in_this_scope; \
EXPECT_NE(A, B); \
}
#define EXPECT_TRUE_NO_TRACKING(A) \
{ \
NoMemTracking no_memory_tracking_in_this_scope; \
EXPECT_TRUE(A); \
}
#define EXPECT_FALSE_NO_TRACKING(A) \
{ \
NoMemTracking no_memory_tracking_in_this_scope; \
EXPECT_FALSE(A); \
}
#define ASSERT_EQ_NO_TRACKING(A, B) \
{ \
NoMemTracking no_memory_tracking_in_this_scope; \
ASSERT_EQ(A, B); \
}
#define ASSERT_NE_NO_TRACKING(A, B) \
{ \
NoMemTracking no_memory_tracking_in_this_scope; \
ASSERT_NE(A, B); \
}
#define ASSERT_TRUE_NO_TRACKING(A) \
{ \
NoMemTracking no_memory_tracking_in_this_scope; \
ASSERT_TRUE(A); \
}
#define ASSERT_FALSE_NO_TRACKING(A) \
{ \
NoMemTracking no_memory_tracking_in_this_scope; \
ASSERT_FALSE(A); \
}
// A structure that cannot be allocated because it throws an exception in its
// constructor. This is needed to test the std::nothrow version of delete since
// it is only called when the std::nothrow version of new fails.
struct ThrowConstructor {
// ThrowConstructor() throw(std::exception) { throw std::exception(); }
ThrowConstructor() : foo_(1) { throw std::exception(); }
// Required to prevent the constructor from being inlined and optimized away.
volatile int foo_;
};
///////////////////////////////////////////////////////////////////////////////
// A memory reporter that is used to watch allocations from the system.
class TestMemReporter {
public:
TestMemReporter() { Construct(); }
SbMemoryReporter* memory_reporter() { return &memory_reporter_; }
// Removes this object from listening to allocations.
void RemoveGlobalHooks() {
SbMemorySetReporter(NULL);
// Sleep to allow other threads time to pass through.
SbThreadSleep(250 * kSbTimeMillisecond);
}
// Total number allocations outstanding.
int number_allocs() const { return number_allocs_; }
// Total number memory map outstanding.
int number_map_mem() const { return number_map_mem_; }
void Clear() {
starboard::ScopedLock lock(mutex_);
number_allocs_ = 0;
number_map_mem_ = 0;
last_allocation_ = NULL;
last_deallocation_ = NULL;
last_mem_map_ = NULL;
last_mem_unmap_ = NULL;
}
const void* last_allocation() const { return last_allocation_; }
const void* last_deallocation() const { return last_deallocation_; }
const void* last_mem_map() const { return last_mem_map_; }
const void* last_mem_unmap() const { return last_mem_unmap_; }
private:
// Boilerplate to delegate static function callbacks to the class instance.
static void OnMalloc(void* context, const void* memory, size_t size) {
TestMemReporter* t = static_cast<TestMemReporter*>(context);
t->ReportAlloc(memory, size);
}
static void OnMapMem(void* context, const void* memory, size_t size) {
TestMemReporter* t = static_cast<TestMemReporter*>(context);
t->ReportMapMem(memory, size);
}
static void OnDealloc(void* context, const void* memory) {
TestMemReporter* t = static_cast<TestMemReporter*>(context);
t->ReportDealloc(memory);
}
static void SbUnMapMem(void* context, const void* memory, size_t size) {
TestMemReporter* t = static_cast<TestMemReporter*>(context);
t->ReportUnMapMemory(memory, size);
}
static SbMemoryReporter CreateSbMemoryReporter(TestMemReporter* context) {
SbMemoryReporter cb = {OnMalloc, OnDealloc, OnMapMem, SbUnMapMem, context};
return cb;
}
void ReportAlloc(const void* memory, size_t size) {
if (!GetMemoryTrackingEnabled_ThreadLocal()) {
return;
}
starboard::ScopedLock lock(mutex_);
last_allocation_ = memory;
number_allocs_++;
}
void ReportMapMem(const void* memory, size_t size) {
if (!GetMemoryTrackingEnabled_ThreadLocal()) {
return;
}
starboard::ScopedLock lock(mutex_);
last_mem_map_ = memory;
number_map_mem_++;
}
void ReportDealloc(const void* memory) {
if (!GetMemoryTrackingEnabled_ThreadLocal()) {
return;
}
starboard::ScopedLock lock(mutex_);
last_deallocation_ = memory;
number_allocs_--;
}
void ReportUnMapMemory(const void* memory, size_t size) {
if (!GetMemoryTrackingEnabled_ThreadLocal()) {
return;
}
starboard::ScopedLock lock(mutex_);
last_mem_unmap_ = memory;
number_map_mem_--;
}
void Construct() {
Clear();
memory_reporter_ = CreateSbMemoryReporter(this);
}
SbMemoryReporter memory_reporter_;
starboard::Mutex mutex_;
const void* last_allocation_;
const void* last_deallocation_;
const void* last_mem_map_;
const void* last_mem_unmap_;
int number_allocs_;
int number_map_mem_;
};
///////////////////////////////////////////////////////////////////////////////
// Needed by all tests that require a memory tracker to be installed. On the
// first test the test memory tracker is installed and then after the last test
// the memory tracker is removed.
class MemoryReportingTest : public ::testing::Test {
public:
TestMemReporter* mem_reporter() { return s_test_alloc_tracker_; }
bool MemoryReportingEnabled() const {
return s_memory_reporter_error_enabled_;
}
protected:
// Global setup - runs before the first test in the series.
static void SetUpTestCase() {
InitMemoryTrackingState_ThreadLocal();
// global init test code should run here.
if (s_test_alloc_tracker_ == NULL) {
s_test_alloc_tracker_ = new TestMemReporter;
}
s_memory_reporter_error_enabled_ =
SbMemorySetReporter(s_test_alloc_tracker_->memory_reporter());
}
// Global Teardown after last test has run it's course.
static void TearDownTestCase() { s_test_alloc_tracker_->RemoveGlobalHooks(); }
// Per test setup.
virtual void SetUp() {
mem_reporter()->Clear();
// Allows current thread to capture memory allocations. If this wasn't
// done then background threads spawned by a framework could notify this
// class about allocations and fail the test.
SetMemoryTrackingEnabled_ThreadLocal(true);
}
// Per test teardown.
virtual void TearDown() {
SetMemoryTrackingEnabled_ThreadLocal(false);
if ((mem_reporter()->number_allocs() != 0) ||
(mem_reporter()->number_map_mem() != 0)) {
ADD_FAILURE_AT(__FILE__, __LINE__) << "Memory Leak detected.";
}
mem_reporter()->Clear();
}
static TestMemReporter* s_test_alloc_tracker_;
static bool s_memory_reporter_error_enabled_;
};
TestMemReporter* MemoryReportingTest::s_test_alloc_tracker_ = NULL;
bool MemoryReportingTest::s_memory_reporter_error_enabled_ = false;
///////////////////////////////////////////////////////////////////////////////
// TESTS.
// There are two sets of tests: POSITIVE and NEGATIVE.
// The positive tests are active when STARBOARD_ALLOWS_MEMORY_TRACKING is
// defined and test that memory tracking is enabled.
// NEGATIVE tests ensure that tracking is disabled when
// STARBOARD_ALLOWS_MEMORY_TRACKING is not defined.
// When adding new tests:
// POSITIVE tests are named normally.
// NEGATIVE tets are named with "No" prefixed to the beginning.
// Example:
// TEST_F(MemoryReportingTest, CapturesAllocDealloc) <--- POSITIVE test.
// TEST_F(MemoryReportingTest, NoCapturesAllocDealloc) <- NEGATIVE test.
// All positive & negative tests are grouped together.
///////////////////////////////////////////////////////////////////////////////
#ifdef STARBOARD_ALLOWS_MEMORY_TRACKING
// These are POSITIVE tests, which test the expectation that when the define
// STARBOARD_ALLOWS_MEMORY_TRACKING is active that the memory tracker will
// receive memory allocations notifications.
//
// Tests the assumption that the SbMemoryAllocate and SbMemoryDeallocate
// will report memory allocations.
TEST_F(MemoryReportingTest, CapturesAllocDealloc) {
if (!MemoryReportingEnabled()) {
SbLog(kSbLogPriorityInfo, "Memory reporting is disabled.\n");
return;
}
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
void* memory = SbMemoryAllocate(4);
EXPECT_EQ_NO_TRACKING(1, mem_reporter()->number_allocs());
EXPECT_EQ_NO_TRACKING(memory, mem_reporter()->last_allocation());
SbMemoryDeallocate(memory);
EXPECT_EQ_NO_TRACKING(mem_reporter()->number_allocs(), 0);
// Should equal the last allocation.
EXPECT_EQ_NO_TRACKING(memory, mem_reporter()->last_deallocation());
}
// Tests the assumption that SbMemoryReallocate() will report a
// deallocation and an allocation to the memory tracker.
TEST_F(MemoryReportingTest, CapturesRealloc) {
if (!MemoryReportingEnabled()) {
SbLog(kSbLogPriorityInfo, "Memory reporting is disabled.\n");
return;
}
void* prev_memory = SbMemoryAllocate(4);
void* new_memory = SbMemoryReallocate(prev_memory, 8);
EXPECT_EQ_NO_TRACKING(new_memory, mem_reporter()->last_allocation());
EXPECT_EQ_NO_TRACKING(prev_memory, mem_reporter()->last_deallocation());
EXPECT_EQ_NO_TRACKING(1, mem_reporter()->number_allocs());
SbMemoryDeallocate(new_memory);
EXPECT_EQ_NO_TRACKING(mem_reporter()->number_allocs(), 0);
}
// Tests the assumption that the SbMemoryMap and SbMemoryUnmap
// will report memory allocations.
TEST_F(MemoryReportingTest, CapturesMemMapUnmap) {
if (!MemoryReportingEnabled()) {
SbLog(kSbLogPriorityInfo, "Memory reporting is disabled.\n");
return;
}
const int64_t kMemSize = 4096;
const int kFlags = kSbMemoryMapProtectReadWrite;
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_map_mem());
void* mem_chunk = SbMemoryMap(kMemSize, kFlags, "TestMemMap");
EXPECT_EQ_NO_TRACKING(1, mem_reporter()->number_map_mem());
// Now unmap the memory and confirm that this memory was reported as free.
EXPECT_EQ_NO_TRACKING(mem_chunk, mem_reporter()->last_mem_map());
SbMemoryUnmap(mem_chunk, kMemSize);
EXPECT_EQ_NO_TRACKING(mem_chunk, mem_reporter()->last_mem_unmap());
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_map_mem());
// On some platforms, bookkeeping for memory mapping can cost allocations.
// Call Clear() explicitly before TearDown() checks number_allocs_;
mem_reporter()->Clear();
}
// Tests the assumption that the operator new/delete will report
// memory allocations.
TEST_F(MemoryReportingTest, CapturesOperatorNewDelete) {
if (!MemoryReportingEnabled()) {
SbLog(kSbLogPriorityInfo, "Memory reporting is disabled.\n");
return;
}
EXPECT_TRUE_NO_TRACKING(mem_reporter()->number_allocs() == 0);
int* my_int = new int();
EXPECT_TRUE_NO_TRACKING(mem_reporter()->number_allocs() == 1);
bool is_last_allocation = my_int == mem_reporter()->last_allocation();
EXPECT_TRUE_NO_TRACKING(is_last_allocation);
delete my_int;
EXPECT_TRUE_NO_TRACKING(mem_reporter()->number_allocs() == 0);
// Expect last deallocation to be the expected pointer.
EXPECT_EQ_NO_TRACKING(my_int, mem_reporter()->last_deallocation());
}
// Tests the assumption that the nothrow version of operator new will report
// memory allocations.
TEST_F(MemoryReportingTest, CapturesOperatorNewNothrow) {
if (!MemoryReportingEnabled()) {
SbLog(kSbLogPriorityInfo, "Memory reporting is disabled.\n");
return;
}
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
int* my_int = new (std::nothrow) int();
EXPECT_EQ_NO_TRACKING(1, mem_reporter()->number_allocs());
bool is_last_allocation = my_int == mem_reporter()->last_allocation();
EXPECT_TRUE_NO_TRACKING(is_last_allocation);
delete my_int;
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
// Expect last deallocation to be the expected pointer.
EXPECT_EQ_NO_TRACKING(my_int, mem_reporter()->last_deallocation());
}
// Tests the assumption that the nothrow version of operator delete will report
// memory deallocations.
TEST_F(MemoryReportingTest, CapturesOperatorDeleteNothrow) {
if (!MemoryReportingEnabled()) {
SbLog(kSbLogPriorityInfo, "Memory reporting is disabled.\n");
return;
}
const void* init_alloc = mem_reporter()->last_allocation();
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
void* my_obj = nullptr;
bool caught_exception = false;
try {
my_obj = new (std::nothrow) ThrowConstructor();
} catch (std::exception e) {
caught_exception = true;
}
EXPECT_TRUE(caught_exception);
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
// Expect that an allocation occurred, even though we never got a pointer.
EXPECT_EQ_NO_TRACKING(nullptr, my_obj);
EXPECT_NE_NO_TRACKING(nullptr, mem_reporter()->last_allocation());
EXPECT_NE_NO_TRACKING(init_alloc, mem_reporter()->last_allocation());
// Expect last deallocation to be the allocation we never got.
EXPECT_EQ_NO_TRACKING(mem_reporter()->last_allocation(),
mem_reporter()->last_deallocation());
}
#else // !defined(STARBOARD_ALLOWS_MEMORY_TRACKING)
// These are NEGATIVE tests, which test the expectation that when the
// STARBOARD_ALLOWS_MEMORY_TRACKING is undefined that the memory tracker does
// not receive memory allocations notifications.
TEST_F(MemoryReportingTest, NoCapturesAllocDealloc) {
EXPECT_FALSE_NO_TRACKING(MemoryReportingEnabled());
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
void* memory = SbMemoryAllocate(4);
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
EXPECT_EQ_NO_TRACKING(NULL, mem_reporter()->last_allocation());
SbMemoryDeallocate(memory);
EXPECT_EQ_NO_TRACKING(mem_reporter()->number_allocs(), 0);
EXPECT_EQ_NO_TRACKING(NULL, mem_reporter()->last_deallocation());
}
TEST_F(MemoryReportingTest, NoCapturesRealloc) {
EXPECT_FALSE_NO_TRACKING(MemoryReportingEnabled());
void* prev_memory = SbMemoryAllocate(4);
void* new_memory = SbMemoryReallocate(prev_memory, 8);
EXPECT_EQ_NO_TRACKING(NULL, mem_reporter()->last_allocation());
EXPECT_EQ_NO_TRACKING(NULL, mem_reporter()->last_deallocation());
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
SbMemoryDeallocate(new_memory);
EXPECT_EQ_NO_TRACKING(mem_reporter()->number_allocs(), 0);
}
TEST_F(MemoryReportingTest, NoCapturesMemMapUnmap) {
EXPECT_FALSE_NO_TRACKING(MemoryReportingEnabled());
const int64_t kMemSize = 4096;
const int kFlags = 0;
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
void* mem_chunk = SbMemoryMap(kMemSize, kFlags, "TestMemMap");
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
// Now unmap the memory and confirm that this memory was reported as free.
EXPECT_EQ_NO_TRACKING(NULL, mem_reporter()->last_mem_map());
SbMemoryUnmap(mem_chunk, kMemSize);
EXPECT_EQ_NO_TRACKING(NULL, mem_reporter()->last_mem_unmap());
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_allocs());
}
TEST_F(MemoryReportingTest, NoCapturesOperatorNewDelete) {
EXPECT_FALSE_NO_TRACKING(MemoryReportingEnabled());
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_map_mem());
int* my_int = new int();
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_map_mem());
EXPECT_EQ_NO_TRACKING(NULL, mem_reporter()->last_allocation());
delete my_int;
EXPECT_EQ_NO_TRACKING(0, mem_reporter()->number_map_mem());
EXPECT_EQ_NO_TRACKING(NULL, mem_reporter()->last_deallocation());
}
#endif // !defined(STARBOARD_ALLOWS_MEMORY_TRACKING)
/////////////////////////////// Implementation ////////////////////////////////
// Simple ThreadLocalBool class which allows a default value to be
// true or false.
class ThreadLocalBool {
public:
ThreadLocalBool() {
// NULL is the destructor.
slot_ = SbThreadCreateLocalKey(NULL);
}
~ThreadLocalBool() { SbThreadDestroyLocalKey(slot_); }
void SetEnabled(bool value) { SetEnabledThreadLocal(value); }
bool Enabled() const { return GetEnabledThreadLocal(); }
private:
void SetEnabledThreadLocal(bool value) {
void* bool_as_pointer;
if (value) {
bool_as_pointer = reinterpret_cast<void*>(0x1);
} else {
bool_as_pointer = NULL;
}
SbThreadSetLocalValue(slot_, bool_as_pointer);
}
bool GetEnabledThreadLocal() const {
void* ptr = SbThreadGetLocalValue(slot_);
return ptr != NULL;
}
mutable SbThreadLocalKey slot_;
bool default_value_;
};
void InitMemoryTrackingState_ThreadLocal() {
GetMemoryTrackingEnabled_ThreadLocal();
}
static ThreadLocalBool* GetMemoryTrackingState() {
static ThreadLocalBool* thread_local_bool = new ThreadLocalBool();
return thread_local_bool;
}
bool GetMemoryTrackingEnabled_ThreadLocal() {
return GetMemoryTrackingState()->Enabled();
}
void SetMemoryTrackingEnabled_ThreadLocal(bool value) {
GetMemoryTrackingState()->SetEnabled(value);
}
// No use for these macros anymore.
#undef EXPECT_EQ_NO_TRACKING
#undef EXPECT_TRUE_NO_TRACKING
#undef EXPECT_FALSE_NO_TRACKING
#undef ASSERT_EQ_NO_TRACKING
#undef ASSERT_TRUE_NO_TRACKING
#undef ASSERT_FALSE_NO_TRACKING
} // namespace
} // namespace nplb
} // namespace starboard