| /* |
| * Copyright 2016 Google Inc. 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 "nb/analytics/memory_tracker_impl.h" |
| #include "nb/memory_scope.h" |
| #include "nb/scoped_ptr.h" |
| #include "nb/test_thread.h" |
| #include "starboard/configuration.h" |
| #include "starboard/system.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #define STRESS_TEST_DURATION_SECONDS 1 |
| #define NUM_STRESS_TEST_THREADS 3 |
| |
| // The following is necessary to prevent operator new from being optimized |
| // out using some compilers. This is required because we rely on operator new |
| // to report memory usage. Overly aggressive optimizing compilers will |
| // eliminate the call to operator new, even though it causes a lost of side |
| // effects. This will therefore break the memory reporting mechanism. This is |
| // a bug in the compiler. |
| // |
| // The solution here is to use macro-replacement to substitute calls to global |
| // new to instead be delegated to our custom new, which prevents elimination |
| // by using a temporay volatile. |
| namespace { |
| struct CustomObject { |
| static CustomObject Make() { |
| CustomObject o; |
| return o; |
| } |
| }; |
| } // namespace |
| |
| void* operator new(std::size_t size, CustomObject ignored) { |
| // Volatile prevents optimization and elimination of operator new by the |
| // optimizing compiler. |
| volatile void* ptr = ::operator new(size); |
| return const_cast<void*>(ptr); |
| } |
| |
| #define NEW_NO_OPTIMIZER_ELIMINATION new (CustomObject::Make()) |
| #define new NEW_NO_OPTIMIZER_ELIMINATION |
| |
| namespace nb { |
| namespace analytics { |
| namespace { |
| |
| class NoReportAllocator { |
| public: |
| NoReportAllocator() {} |
| NoReportAllocator(const NoReportAllocator&) {} |
| static void* Allocate(size_t n) { return SbMemoryAllocateNoReport(n); } |
| // Second argument can be used for accounting, but is otherwise optional. |
| static void Deallocate(void* ptr, size_t /* not used*/) { |
| SbMemoryDeallocateNoReport(ptr); |
| } |
| }; |
| |
| // Some platforms will allocate memory for empty vectors. Therefore |
| // use MyVector to prevent empty vectors from reporting memory. |
| template <typename T> |
| using MyVector = std::vector<T, StdAllocator<T, NoReportAllocator>>; |
| |
| MemoryTrackerImpl* s_memory_tracker_ = NULL; |
| |
| struct NoMemTracking { |
| bool prev_val; |
| NoMemTracking() : prev_val(false) { |
| if (s_memory_tracker_) { |
| prev_val = s_memory_tracker_->IsMemoryTrackingEnabled(); |
| s_memory_tracker_->SetMemoryTrackingEnabled(false); |
| } |
| } |
| ~NoMemTracking() { |
| if (s_memory_tracker_) { |
| s_memory_tracker_->SetMemoryTrackingEnabled(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_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_TRUE_NO_TRACKING(A) \ |
| { \ |
| NoMemTracking no_memory_tracking_in_this_scope; \ |
| ASSERT_TRUE(A); \ |
| } |
| |
| // !! converts int -> bool. |
| bool FlipCoin() { |
| return !!(SbSystemGetRandomUInt64() & 0x1); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Stress testing the Allocation Tracker. |
| class MemoryScopeThread : public nb::TestThread { |
| public: |
| typedef nb::TestThread Super; |
| |
| explicit MemoryScopeThread(MemoryTrackerImpl* memory_tracker) |
| : memory_tracker_(memory_tracker) { |
| static int s_counter = 0; |
| |
| std::stringstream ss; |
| ss << "MemoryScopeThread_" << s_counter++; |
| unique_name_ = ss.str(); |
| } |
| virtual ~MemoryScopeThread() {} |
| |
| // Overridden so that the thread can exit gracefully. |
| virtual void Join() { |
| finished_ = true; |
| Super::Join(); |
| } |
| virtual void Run() { |
| while (!finished_) { |
| TRACK_MEMORY_SCOPE_DYNAMIC(unique_name_.c_str()); |
| AllocationGroup* group = memory_tracker_->PeekAllocationGroup(); |
| |
| const int cmp_result = group->name().compare(unique_name_); |
| if (cmp_result != 0) { |
| GTEST_FAIL() << "unique name mismatch"; |
| return; |
| } |
| } |
| } |
| |
| private: |
| MemoryTrackerImpl* memory_tracker_; |
| bool finished_; |
| std::string unique_name_; |
| int do_delete_counter_; |
| int do_malloc_counter_; |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Stress testing the Allocation Tracker. |
| class AllocationStressThread : public nb::TestThread { |
| public: |
| explicit AllocationStressThread(MemoryTrackerImpl* memory_tracker); |
| virtual ~AllocationStressThread(); |
| |
| // Overridden so that the thread can exit gracefully. |
| virtual void Join(); |
| virtual void Run(); |
| |
| private: |
| typedef std::map<const void*, AllocationRecord> AllocMap; |
| |
| void CheckPointers(); |
| bool RemoveRandomAllocation(std::pair<const void*, AllocationRecord>* output); |
| bool DoDelete(); |
| void DoMalloc(); |
| |
| MemoryTrackerImpl* memory_tracker_; |
| bool finished_; |
| std::map<const void*, AllocationRecord> allocated_pts_; |
| std::string unique_name_; |
| int do_delete_counter_; |
| int do_malloc_counter_; |
| }; |
| |
| class AddAllocationStressThread : public nb::TestThread { |
| public: |
| typedef std::map<const void*, AllocationRecord> AllocMap; |
| |
| AddAllocationStressThread(MemoryTracker* memory_tracker, |
| int num_elements_add, |
| AllocMap* destination_map, |
| starboard::Mutex* destination_map_mutex) |
| : memory_tracker_(memory_tracker), |
| num_elements_to_add_(num_elements_add), |
| destination_map_(destination_map), |
| destination_map_mutex_(destination_map_mutex) {} |
| |
| virtual void Run() { |
| for (int i = 0; i < num_elements_to_add_; ++i) { |
| const int alloc_size = std::rand() % 100 + 8; |
| void* ptr = SbMemoryAllocate(alloc_size); |
| |
| AllocationRecord record; |
| if (memory_tracker_->GetMemoryTracking(ptr, &record)) { |
| NoMemTracking no_mem_tracking; // simplifies test. |
| |
| starboard::ScopedLock lock(*destination_map_mutex_); |
| destination_map_->insert(std::make_pair(ptr, record)); |
| } else { |
| ADD_FAILURE_AT(__FILE__, __LINE__) << "Could not add pointer."; |
| } |
| if (FlipCoin()) { |
| SbThreadYield(); // Give other threads a chance to run. |
| } |
| } |
| } |
| |
| private: |
| MemoryTracker* memory_tracker_; |
| AllocMap* destination_map_; |
| starboard::Mutex* destination_map_mutex_; |
| int num_elements_to_add_; |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Framework which initializes the MemoryTracker once and installs it |
| // for the first test and the removes the MemoryTracker after the |
| // the last test finishes. |
| class MemoryTrackerImplTest : public ::testing::Test { |
| public: |
| typedef MemoryTrackerImpl::AllocationMapType AllocationMapType; |
| MemoryTrackerImplTest() {} |
| |
| MemoryTrackerImpl* memory_tracker() { return s_memory_tracker_; } |
| |
| bool GetAllocRecord(void* alloc_memory, AllocationRecord* output) { |
| return memory_tracker()->GetMemoryTracking(alloc_memory, output); |
| } |
| |
| AllocationMapType* pointer_map() { return memory_tracker()->pointer_map(); } |
| |
| size_t NumberOfAllocations() { |
| AllocationMapType* map = pointer_map(); |
| return map->Size(); |
| } |
| |
| int64_t TotalAllocationBytes() { |
| return memory_tracker()->GetTotalAllocationBytes(); |
| } |
| |
| bool MemoryTrackerEnabled() const { return s_memory_tracker_enabled_; } |
| |
| protected: |
| static void SetUpTestCase() { |
| if (!s_memory_tracker_) { |
| s_memory_tracker_ = new MemoryTrackerImpl; |
| } |
| // There are obligatory background threads for nb_test suite. This filter |
| // makes sure that they don't intercept this test. |
| s_memory_tracker_->SetThreadFilter(SbThreadGetId()); |
| s_memory_tracker_enabled_ = s_memory_tracker_->InstallGlobalTrackingHooks(); |
| } |
| static void TearDownTestCase() { |
| s_memory_tracker_->RemoveGlobalTrackingHooks(); |
| // Give time for threads to sync. We don't use locks on the reporter |
| // for performance reasons. |
| SbThreadSleep(250 * kSbTimeMillisecond); |
| } |
| |
| virtual void SetUp() { memory_tracker()->Clear(); } |
| |
| virtual void TearDown() { memory_tracker()->Clear(); } |
| static bool s_memory_tracker_enabled_; |
| }; |
| bool MemoryTrackerImplTest::s_memory_tracker_enabled_ = false; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // MemoryTrackerImplTest |
| TEST_F(MemoryTrackerImplTest, NoMemTracking) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| ASSERT_EQ_NO_TRACKING(0, NumberOfAllocations()); |
| scoped_ptr<int> dummy(new int()); |
| EXPECT_EQ_NO_TRACKING(1, NumberOfAllocations()); |
| { |
| // Now that memory allocation is disabled, no more allocations should |
| // be recorded. |
| NoMemTracking no_memory_tracking_in_this_scope; |
| int* dummy2 = new int(); |
| EXPECT_EQ_NO_TRACKING(1, NumberOfAllocations()); |
| delete dummy2; |
| EXPECT_EQ_NO_TRACKING(1, NumberOfAllocations()); |
| } |
| scoped_ptr<int> dummy2(new int()); |
| EXPECT_EQ_NO_TRACKING(2, NumberOfAllocations()); |
| dummy.reset(NULL); |
| EXPECT_EQ_NO_TRACKING(1, NumberOfAllocations()); |
| dummy2.reset(NULL); |
| EXPECT_EQ_NO_TRACKING(0, NumberOfAllocations()); |
| } |
| |
| TEST_F(MemoryTrackerImplTest, RemovePointerOnNoMemoryTracking) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| |
| int* int_ptr = new int(); |
| { |
| NoMemTracking no_memory_tracking_in_this_scope; |
| delete int_ptr; |
| } |
| EXPECT_FALSE_NO_TRACKING(pointer_map()->Get(int_ptr, NULL)); |
| } |
| |
| TEST_F(MemoryTrackerImplTest, NewDeleteOverriddenTest) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| EXPECT_EQ_NO_TRACKING(0, NumberOfAllocations()); |
| int* int_a = new int(0); |
| EXPECT_EQ_NO_TRACKING(1, NumberOfAllocations()); |
| delete int_a; |
| EXPECT_EQ_NO_TRACKING(0, NumberOfAllocations()); |
| } |
| |
| TEST_F(MemoryTrackerImplTest, TotalAllocationBytes) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| int32_t* int_a = new int32_t(0); |
| EXPECT_EQ_NO_TRACKING(1, NumberOfAllocations()); |
| EXPECT_EQ_NO_TRACKING(4, TotalAllocationBytes()); |
| delete int_a; |
| EXPECT_EQ_NO_TRACKING(0, NumberOfAllocations()); |
| } |
| |
| // Tests the expectation that a lot of allocations can be executed and that |
| // internal data structures won't overflow. |
| TEST_F(MemoryTrackerImplTest, NoStackOverflow) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| static const int kNumAllocations = 1000; |
| MyVector<int*> allocations; |
| |
| // Also it turns out that this test is great for catching |
| // background threads pushing allocations through the allocator. |
| // This is supposed to be filtered, but if it's not then this test will |
| // fail. |
| // SbThreadYield() is used to give other threads a chance to enter into our |
| // allocator and catch a test failure. |
| SbThreadSleep(1); |
| size_t previously_existing_allocs = NumberOfAllocations(); |
| ASSERT_EQ_NO_TRACKING(0, previously_existing_allocs); |
| |
| for (int i = 0; i < kNumAllocations; ++i) { |
| SbThreadYield(); |
| EXPECT_EQ_NO_TRACKING(i, NumberOfAllocations()); |
| int* val = new int(0); |
| NoMemTracking no_tracking_in_scope; |
| allocations.push_back(val); |
| } |
| |
| EXPECT_EQ_NO_TRACKING(kNumAllocations, NumberOfAllocations()); |
| |
| for (int i = 0; i < kNumAllocations; ++i) { |
| SbThreadYield(); |
| EXPECT_EQ_NO_TRACKING(kNumAllocations - i, NumberOfAllocations()); |
| delete allocations[i]; |
| } |
| |
| EXPECT_EQ_NO_TRACKING(0, NumberOfAllocations()); |
| } |
| |
| // Tests the expectation that the macros will push/pop the memory scope. |
| TEST_F(MemoryTrackerImplTest, MacrosPushPop) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| scoped_ptr<int> dummy; |
| { |
| TRACK_MEMORY_SCOPE("TestAllocations"); |
| dummy.reset(new int()); |
| } |
| |
| scoped_ptr<int> dummy2(new int()); |
| |
| AllocationRecord alloc_rec; |
| pointer_map()->Get(dummy.get(), &alloc_rec); |
| |
| ASSERT_TRUE_NO_TRACKING(alloc_rec.allocation_group); |
| EXPECT_EQ_NO_TRACKING(std::string("TestAllocations"), |
| alloc_rec.allocation_group->name()); |
| |
| pointer_map()->Get(dummy2.get(), &alloc_rec); |
| |
| ASSERT_TRUE_NO_TRACKING(alloc_rec.allocation_group); |
| EXPECT_EQ_NO_TRACKING(std::string("Unaccounted"), |
| alloc_rec.allocation_group->name()); |
| } |
| |
| // Tests the expectation that if the cached flag on the NbMemoryScopeInfo is |
| // set to false that the caching of the handle is not performed. |
| TEST_F(MemoryTrackerImplTest, RespectsNonCachedHandle) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| const bool kCaching = false; |
| NbMemoryScopeInfo memory_scope = { |
| NULL, "MyName", __FILE__, |
| __LINE__, __FUNCTION__, false}; // false to disallow caching. |
| |
| // Pushing the memory scope should trigger the caching operation to be |
| // attempted. However, because caching was explicitly disabled this handle |
| // should retain the value of 0. |
| NbPushMemoryScope(&memory_scope); |
| EXPECT_TRUE_NO_TRACKING(memory_scope.cached_handle_ == NULL); |
| |
| // ... and still assert that the group was created with the expected name. |
| AllocationGroup* group = memory_tracker()->GetAllocationGroup("MyName"); |
| // Equality check. |
| EXPECT_EQ_NO_TRACKING(0, group->name().compare("MyName")); |
| NbPopMemoryScope(); |
| } |
| |
| // Tests the expectation that if the cached flag on the NbMemoryScopeInfo is |
| // set to true that the caching will be applied for the cached_handle of the |
| // memory scope. |
| TEST_F(MemoryTrackerImplTest, PushAllocGroupCachedHandle) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| NbMemoryScopeInfo memory_scope = { |
| NULL, // Cached handle. |
| "MyName", // Memory scope name. |
| __FILE__, __LINE__, __FUNCTION__, |
| true // Allows caching. |
| }; |
| |
| NbPushMemoryScope(&memory_scope); |
| EXPECT_TRUE_NO_TRACKING(memory_scope.cached_handle_ != NULL); |
| AllocationGroup* group = memory_tracker()->GetAllocationGroup("MyName"); |
| |
| EXPECT_EQ_NO_TRACKING(memory_scope.cached_handle_, static_cast<void*>(group)); |
| NbPopMemoryScope(); |
| } |
| |
| // Tests the expectation that the macro TRACK_MEMORY_SCOPE will capture the |
| // allocation in the MemoryTrackerImpl. |
| TEST_F(MemoryTrackerImplTest, MacrosGroupAccounting) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| MemoryTrackerImpl* track_alloc = memory_tracker(); // Debugging. |
| track_alloc->Clear(); |
| |
| memory_tracker()->Clear(); |
| const AllocationGroup* group_a = |
| memory_tracker()->GetAllocationGroup("MemoryTrackerTest-ScopeA"); |
| |
| const AllocationGroup* group_b = |
| memory_tracker()->GetAllocationGroup("MemoryTrackerTest-ScopeB"); |
| |
| ASSERT_TRUE_NO_TRACKING(group_a); |
| ASSERT_TRUE_NO_TRACKING(group_b); |
| |
| int32_t num_allocations = -1; |
| int64_t allocation_bytes = -1; |
| |
| // Expect that both groups have no allocations in them. |
| group_a->GetAggregateStats(&num_allocations, &allocation_bytes); |
| EXPECT_EQ_NO_TRACKING(0, num_allocations); |
| EXPECT_EQ_NO_TRACKING(0, allocation_bytes); |
| |
| group_b->GetAggregateStats(&num_allocations, &allocation_bytes); |
| EXPECT_EQ_NO_TRACKING(0, num_allocations); |
| EXPECT_EQ_NO_TRACKING(0, allocation_bytes); |
| |
| scoped_ptr<int> alloc_a, alloc_b, alloc_b2; |
| { |
| TRACK_MEMORY_SCOPE("MemoryTrackerTest-ScopeA"); |
| alloc_a.reset(new int()); |
| { |
| TRACK_MEMORY_SCOPE("MemoryTrackerTest-ScopeB"); |
| alloc_b.reset(new int()); |
| alloc_b2.reset(new int()); |
| |
| group_a->GetAggregateStats(&num_allocations, &allocation_bytes); |
| EXPECT_EQ_NO_TRACKING(1, num_allocations); |
| EXPECT_EQ_NO_TRACKING(4, allocation_bytes); |
| alloc_a.reset(NULL); |
| group_a->GetAggregateStats(&num_allocations, &allocation_bytes); |
| EXPECT_EQ_NO_TRACKING(0, num_allocations); |
| EXPECT_EQ_NO_TRACKING(0, allocation_bytes); |
| |
| allocation_bytes = num_allocations = -1; |
| group_b->GetAggregateStats(&num_allocations, &allocation_bytes); |
| EXPECT_EQ_NO_TRACKING(2, num_allocations); |
| EXPECT_EQ_NO_TRACKING(8, allocation_bytes); |
| |
| alloc_b2.reset(NULL); |
| group_b->GetAggregateStats(&num_allocations, &allocation_bytes); |
| EXPECT_EQ_NO_TRACKING(1, num_allocations); |
| EXPECT_EQ_NO_TRACKING(4, allocation_bytes); |
| |
| alloc_b.reset(NULL); |
| group_b->GetAggregateStats(&num_allocations, &allocation_bytes); |
| EXPECT_EQ_NO_TRACKING(0, num_allocations); |
| EXPECT_EQ_NO_TRACKING(0, allocation_bytes); |
| } |
| } |
| } |
| |
| // Tests the expectation that the MemoryTrackerDebugCallback works as expected |
| // to notify of incoming allocations. |
| TEST_F(MemoryTrackerImplTest, MemoryTrackerDebugCallback) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| |
| // Impl of the callback. Copies the allocation information so that we can |
| // ensure it produces expected values. |
| class MemoryTrackerDebugCallbackTest : public MemoryTrackerDebugCallback { |
| public: |
| MemoryTrackerDebugCallbackTest() { Reset(); } |
| void OnMemoryAllocation(const void* memory_block, |
| const AllocationRecord& record, |
| const CallStack& callstack) override { |
| last_memory_block_allocated_ = memory_block; |
| last_allocation_record_allocated_ = record; |
| } |
| void OnMemoryDeallocation(const void* memory_block, |
| const AllocationRecord& record, |
| const CallStack& callstack) override { |
| last_memory_block_deallocated_ = memory_block; |
| last_allocation_record_deallocated_ = record; |
| } |
| void Reset() { |
| last_memory_block_allocated_ = NULL; |
| last_memory_block_deallocated_ = NULL; |
| last_allocation_record_allocated_ = AllocationRecord::Empty(); |
| last_allocation_record_deallocated_ = AllocationRecord::Empty(); |
| } |
| const void* last_memory_block_allocated_; |
| const void* last_memory_block_deallocated_; |
| AllocationRecord last_allocation_record_allocated_; |
| AllocationRecord last_allocation_record_deallocated_; |
| }; |
| |
| // Needs to be static due to concurrent and lockless nature of object. |
| static MemoryTrackerDebugCallbackTest s_debug_callback; |
| s_debug_callback.Reset(); |
| |
| memory_tracker()->SetMemoryTrackerDebugCallback(&s_debug_callback); |
| void* memory_block = SbMemoryAllocate(8); |
| EXPECT_EQ_NO_TRACKING(memory_block, |
| s_debug_callback.last_memory_block_allocated_); |
| EXPECT_EQ_NO_TRACKING( |
| 8, s_debug_callback.last_allocation_record_allocated_.size); |
| // ... and no memory should have been deallocated. |
| EXPECT_TRUE_NO_TRACKING(s_debug_callback.last_memory_block_deallocated_ == |
| NULL); |
| |
| // After this call we check that the callback for deallocation was used. |
| SbMemoryDeallocate(memory_block); |
| EXPECT_EQ_NO_TRACKING(memory_block, |
| s_debug_callback.last_memory_block_deallocated_); |
| |
| EXPECT_EQ_NO_TRACKING( |
| s_debug_callback.last_allocation_record_deallocated_.size, 8); |
| } |
| |
| // Tests the expectation that the visitor can access the allocations. |
| TEST_F(MemoryTrackerImplTest, VisitorAccess) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| class SimpleVisitor : public AllocationVisitor { |
| public: |
| SimpleVisitor() : num_memory_allocs_(0) {} |
| virtual bool Visit(const void* memory, |
| const AllocationRecord& alloc_record) { |
| num_memory_allocs_++; |
| return true; // Keep traversing. |
| } |
| |
| size_t num_memory_allocs_; |
| }; |
| |
| SimpleVisitor visitor; |
| scoped_ptr<int> int_ptr(new int); |
| |
| // Should see the int_ptr allocation. |
| memory_tracker()->Accept(&visitor); |
| EXPECT_EQ_NO_TRACKING(1, visitor.num_memory_allocs_); |
| visitor.num_memory_allocs_ = 0; |
| |
| int_ptr.reset(NULL); |
| // Now no allocations should be available. |
| memory_tracker()->Accept(&visitor); |
| EXPECT_EQ_NO_TRACKING(0, visitor.num_memory_allocs_); |
| } |
| |
| // A stress test that rapidly adds allocations, but saves all deletions |
| // for the main thread. This test will catch concurrency errors related |
| // to reporting new allocations. |
| TEST_F(MemoryTrackerImplTest, MultiThreadedStressAddTest) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| // Disable allocation filtering. |
| memory_tracker()->SetThreadFilter(kSbThreadInvalidId); |
| |
| MyVector<nb::TestThread*> threads; |
| |
| const int kNumObjectsToAdd = 10000 / NUM_STRESS_TEST_THREADS; |
| AddAllocationStressThread::AllocMap map; |
| starboard::Mutex map_mutex; |
| |
| for (int i = 0; i < NUM_STRESS_TEST_THREADS; ++i) { |
| nb::TestThread* thread = new AddAllocationStressThread( |
| memory_tracker(), kNumObjectsToAdd, &map, &map_mutex); |
| |
| threads.push_back(thread); |
| } |
| |
| for (int i = 0; i < NUM_STRESS_TEST_THREADS; ++i) { |
| threads[i]->Start(); |
| } |
| |
| for (int i = 0; i < NUM_STRESS_TEST_THREADS; ++i) { |
| threads[i]->Join(); |
| } |
| for (int i = 0; i < NUM_STRESS_TEST_THREADS; ++i) { |
| delete threads[i]; |
| } |
| |
| while (!map.empty()) { |
| const void* ptr = map.begin()->first; |
| map.erase(map.begin()); |
| |
| if (!memory_tracker()->GetMemoryTracking(ptr, NULL)) { |
| ADD_FAILURE_AT(__FILE__, __LINE__) << "No tracking?!"; |
| } |
| |
| SbMemoryDeallocate(const_cast<void*>(ptr)); |
| if (memory_tracker()->GetMemoryTracking(ptr, NULL)) { |
| ADD_FAILURE_AT(__FILE__, __LINE__) << "Tracking?!"; |
| } |
| } |
| } |
| |
| // Tests the expectation that memory scopes are multi-threaded safe. |
| TEST_F(MemoryTrackerImplTest, MultiThreadedMemoryScope) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| memory_tracker()->SetThreadFilter(kSbThreadInvalidId); |
| TRACK_MEMORY_SCOPE("MultiThreadedStressUseTest"); |
| |
| MyVector<MemoryScopeThread*> threads; |
| |
| for (int i = 0; i < NUM_STRESS_TEST_THREADS; ++i) { |
| threads.push_back(new MemoryScopeThread(memory_tracker())); |
| } |
| |
| for (int i = 0; i < threads.size(); ++i) { |
| threads[i]->Start(); |
| } |
| |
| SbThreadSleep(STRESS_TEST_DURATION_SECONDS * 1000 * 1000); |
| |
| for (int i = 0; i < threads.size(); ++i) { |
| threads[i]->Join(); |
| } |
| |
| for (int i = 0; i < threads.size(); ++i) { |
| delete threads[i]; |
| } |
| |
| threads.clear(); |
| } |
| |
| // Tests the expectation that new/delete can be done by different threads. |
| TEST_F(MemoryTrackerImplTest, MultiThreadedStressUseTest) { |
| // Memory tracker is not enabled for this build. |
| if (!MemoryTrackerEnabled()) { |
| return; |
| } |
| // Disable allocation filtering. |
| memory_tracker()->SetThreadFilter(kSbThreadInvalidId); |
| TRACK_MEMORY_SCOPE("MultiThreadedStressUseTest"); |
| |
| MyVector<AllocationStressThread*> threads; |
| |
| for (int i = 0; i < NUM_STRESS_TEST_THREADS; ++i) { |
| threads.push_back(new AllocationStressThread(memory_tracker())); |
| } |
| |
| for (int i = 0; i < threads.size(); ++i) { |
| threads[i]->Start(); |
| } |
| |
| SbThreadSleep(STRESS_TEST_DURATION_SECONDS * 1000 * 1000); |
| |
| for (int i = 0; i < threads.size(); ++i) { |
| threads[i]->Join(); |
| } |
| |
| for (int i = 0; i < threads.size(); ++i) { |
| delete threads[i]; |
| } |
| |
| threads.clear(); |
| } |
| |
| //////////////////////////// Implementation /////////////////////////////////// |
| /// Impl of AllocationStressThread |
| AllocationStressThread::AllocationStressThread(MemoryTrackerImpl* tracker) |
| : memory_tracker_(tracker), finished_(false) { |
| static int counter = 0; |
| std::stringstream ss; |
| ss << "AllocStressThread-" << counter++; |
| unique_name_ = ss.str(); |
| } |
| |
| AllocationStressThread::~AllocationStressThread() { |
| if (!allocated_pts_.empty()) { |
| ADD_FAILURE_AT(__FILE__, __LINE__) << "allocated pointers still exist"; |
| } |
| } |
| |
| void AllocationStressThread::Join() { |
| finished_ = true; |
| nb::TestThread::Join(); |
| } |
| |
| void AllocationStressThread::CheckPointers() { |
| typedef AllocMap::iterator Iter; |
| |
| for (Iter it = allocated_pts_.begin(); it != allocated_pts_.end(); ++it) { |
| const void* ptr = it->first; |
| const bool found = memory_tracker_->GetMemoryTracking(ptr, NULL); |
| if (!found) { |
| NoMemTracking no_tracking_in_scope; |
| ADD_FAILURE_AT(__FILE__, __LINE__) << "Not found"; |
| } |
| } |
| } |
| |
| void AllocationStressThread::Run() { |
| while (!finished_) { |
| const bool do_delete = FlipCoin(); |
| if (FlipCoin()) { |
| DoDelete(); |
| } else { |
| DoMalloc(); |
| } |
| CheckPointers(); |
| |
| // Randomly give other threads the opportunity run. |
| if (FlipCoin()) { |
| SbThreadYield(); |
| } |
| } |
| |
| // Clear out all memory. |
| while (DoDelete()) { |
| ; |
| } |
| } |
| |
| bool AllocationStressThread::RemoveRandomAllocation( |
| std::pair<const void*, AllocationRecord>* output) { |
| if (allocated_pts_.empty()) { |
| return false; |
| } |
| |
| // Select a random pointer to delete. |
| int idx = std::rand() % allocated_pts_.size(); |
| AllocMap::iterator iter = allocated_pts_.begin(); |
| while (idx > 0) { |
| idx--; |
| iter++; |
| } |
| output->first = iter->first; |
| output->second = iter->second; |
| allocated_pts_.erase(iter); |
| return true; |
| } |
| |
| bool AllocationStressThread::DoDelete() { |
| NoMemTracking no_memory_tracking_in_this_scope; |
| ++do_delete_counter_; |
| |
| std::pair<const void*, AllocationRecord> alloc; |
| if (!RemoveRandomAllocation(&alloc)) { |
| return false; |
| } |
| |
| const void* ptr = alloc.first; |
| const AllocationRecord expected_alloc_record = alloc.second; |
| |
| TRACK_MEMORY_SCOPE_DYNAMIC(unique_name_.c_str()); |
| AllocationGroup* current_group = memory_tracker_->PeekAllocationGroup(); |
| |
| // Expect that the name of the current allocation group name is the same as |
| // what we expect. |
| if (current_group->name() != unique_name_) { |
| NoMemTracking no_memory_tracking_in_this_scope; |
| ADD_FAILURE_AT(__FILE__, __LINE__) |
| << " " << current_group->name() << " != " << unique_name_; |
| } |
| |
| MemoryTrackerImpl::AllocationMapType* internal_alloc_map = |
| memory_tracker_->pointer_map(); |
| |
| AllocationRecord existing_alloc_record; |
| |
| const bool found_existing_record = |
| memory_tracker_->GetMemoryTracking(ptr, &existing_alloc_record); |
| |
| if (!found_existing_record) { |
| ADD_FAILURE_AT(__FILE__, __LINE__) |
| << "expected to find existing record, but did not"; |
| } else if (current_group != existing_alloc_record.allocation_group) { |
| ADD_FAILURE_AT(__FILE__, __LINE__) |
| << "group allocation mismatch: " << current_group->name() |
| << " != " << existing_alloc_record.allocation_group->name() << "\n"; |
| } |
| SbMemoryDeallocate(const_cast<void*>(ptr)); |
| return true; |
| } |
| |
| void AllocationStressThread::DoMalloc() { |
| ++do_malloc_counter_; |
| if (allocated_pts_.size() > 10000) { |
| return; |
| } |
| |
| TRACK_MEMORY_SCOPE_DYNAMIC(unique_name_.c_str()); |
| AllocationGroup* current_group = memory_tracker_->PeekAllocationGroup(); |
| |
| // Sanity check, make sure that the current_group name is the same as |
| // our unique name. |
| if (current_group->name() != unique_name_) { |
| NoMemTracking no_tracking_in_scope; |
| ADD_FAILURE_AT(__FILE__, __LINE__) |
| << " " << current_group->name() << " != " << unique_name_; |
| } |
| |
| if (!memory_tracker_->IsMemoryTrackingEnabled()) { |
| NoMemTracking no_tracking_in_scope; |
| ADD_FAILURE_AT(__FILE__, __LINE__) |
| << " memory tracking state was disabled."; |
| } |
| |
| const int alloc_size = std::rand() % 100 + 8; |
| |
| void* memory = SbMemoryAllocate(alloc_size); |
| |
| AllocationRecord record; |
| bool found = memory_tracker_->GetMemoryTracking(memory, &record); |
| if (!found) { |
| NoMemTracking no_tracking_in_scope; |
| ADD_FAILURE_AT(__FILE__, __LINE__) |
| << "Violated expectation, malloc counter: " << do_malloc_counter_; |
| } |
| AllocMap::iterator found_it = allocated_pts_.find(memory); |
| |
| if (found_it != allocated_pts_.end()) { |
| NoMemTracking no_tracking_in_scope; |
| ADD_FAILURE_AT(__FILE__, __LINE__) |
| << "This pointer should not be in the map."; |
| } |
| |
| NoMemTracking no_tracking_in_scope; |
| allocated_pts_[memory] = AllocationRecord(alloc_size, current_group); |
| } |
| |
| } // namespace |
| } // namespace analytics |
| } // namespace nb |