| // Copyright 2016 The Chromium Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "base/debug/thread_heap_usage_tracker.h" | |
| #include <algorithm> | |
| #include <limits> | |
| #include <new> | |
| #include <type_traits> | |
| #include "starboard/types.h" | |
| #include "starboard/memory.h" | |
| #include "base/allocator/allocator_shim.h" | |
| #include "base/allocator/buildflags.h" | |
| #include "base/logging.h" | |
| #include "base/no_destructor.h" | |
| #include "base/threading/thread_local_storage.h" | |
| #include "build/build_config.h" | |
| #if !defined(STARBOARD) | |
| #if defined(OS_MACOSX) || defined(OS_IOS) | |
| #include <malloc/malloc.h> | |
| #else | |
| #include <malloc.h> | |
| #endif | |
| #endif | |
| namespace base { | |
| namespace debug { | |
| namespace { | |
| using base::allocator::AllocatorDispatch; | |
| const uintptr_t kSentinelMask = std::numeric_limits<uintptr_t>::max() - 1; | |
| ThreadHeapUsage* const kInitializationSentinel = | |
| reinterpret_cast<ThreadHeapUsage*>(kSentinelMask); | |
| ThreadHeapUsage* const kTeardownSentinel = | |
| reinterpret_cast<ThreadHeapUsage*>(kSentinelMask | 1); | |
| ThreadLocalStorage::Slot& ThreadAllocationUsage() { | |
| static NoDestructor<ThreadLocalStorage::Slot> thread_allocator_usage( | |
| [](void* thread_heap_usage) { | |
| // This destructor will be called twice. Once to destroy the actual | |
| // ThreadHeapUsage instance and a second time, immediately after, for | |
| // the sentinel. Re-setting the TLS slow (below) does re-initialize the | |
| // TLS slot. The ThreadLocalStorage code is designed to deal with this | |
| // use case and will re-call the destructor with the kTeardownSentinel | |
| // as arg. | |
| if (thread_heap_usage == kTeardownSentinel) | |
| return; | |
| DCHECK_NE(thread_heap_usage, kInitializationSentinel); | |
| // Deleting the ThreadHeapUsage TLS object will re-enter the shim and | |
| // hit RecordFree() (see below). The sentinel prevents RecordFree() from | |
| // re-creating another ThreadHeapUsage object. | |
| ThreadAllocationUsage().Set(kTeardownSentinel); | |
| delete static_cast<ThreadHeapUsage*>(thread_heap_usage); | |
| }); | |
| return *thread_allocator_usage; | |
| } | |
| bool g_heap_tracking_enabled = false; | |
| // Forward declared as it needs to delegate memory allocation to the next | |
| // lower shim. | |
| ThreadHeapUsage* GetOrCreateThreadUsage(); | |
| size_t GetAllocSizeEstimate(const AllocatorDispatch* next, | |
| void* ptr, | |
| void* context) { | |
| if (ptr == nullptr) | |
| return 0U; | |
| return next->get_size_estimate_function(next, ptr, context); | |
| } | |
| void RecordAlloc(const AllocatorDispatch* next, | |
| void* ptr, | |
| size_t size, | |
| void* context) { | |
| ThreadHeapUsage* usage = GetOrCreateThreadUsage(); | |
| if (usage == nullptr) | |
| return; | |
| usage->alloc_ops++; | |
| size_t estimate = GetAllocSizeEstimate(next, ptr, context); | |
| if (size && estimate) { | |
| // Only keep track of the net number of bytes allocated in the scope if the | |
| // size estimate function returns sane values, e.g. non-zero. | |
| usage->alloc_bytes += estimate; | |
| usage->alloc_overhead_bytes += estimate - size; | |
| // Record the max outstanding number of bytes, but only if the difference | |
| // is net positive (e.g. more bytes allocated than freed in the scope). | |
| if (usage->alloc_bytes > usage->free_bytes) { | |
| uint64_t allocated_bytes = usage->alloc_bytes - usage->free_bytes; | |
| if (allocated_bytes > usage->max_allocated_bytes) | |
| usage->max_allocated_bytes = allocated_bytes; | |
| } | |
| } else { | |
| usage->alloc_bytes += size; | |
| } | |
| } | |
| void RecordFree(const AllocatorDispatch* next, void* ptr, void* context) { | |
| ThreadHeapUsage* usage = GetOrCreateThreadUsage(); | |
| if (usage == nullptr) | |
| return; | |
| size_t estimate = GetAllocSizeEstimate(next, ptr, context); | |
| usage->free_ops++; | |
| usage->free_bytes += estimate; | |
| } | |
| void* AllocFn(const AllocatorDispatch* self, size_t size, void* context) { | |
| void* ret = self->next->alloc_function(self->next, size, context); | |
| if (ret != nullptr) | |
| RecordAlloc(self->next, ret, size, context); | |
| return ret; | |
| } | |
| void* AllocZeroInitializedFn(const AllocatorDispatch* self, | |
| size_t n, | |
| size_t size, | |
| void* context) { | |
| void* ret = | |
| self->next->alloc_zero_initialized_function(self->next, n, size, context); | |
| if (ret != nullptr) | |
| RecordAlloc(self->next, ret, size, context); | |
| return ret; | |
| } | |
| void* AllocAlignedFn(const AllocatorDispatch* self, | |
| size_t alignment, | |
| size_t size, | |
| void* context) { | |
| void* ret = | |
| self->next->alloc_aligned_function(self->next, alignment, size, context); | |
| if (ret != nullptr) | |
| RecordAlloc(self->next, ret, size, context); | |
| return ret; | |
| } | |
| void* ReallocFn(const AllocatorDispatch* self, | |
| void* address, | |
| size_t size, | |
| void* context) { | |
| if (address != nullptr) | |
| RecordFree(self->next, address, context); | |
| void* ret = self->next->realloc_function(self->next, address, size, context); | |
| if (ret != nullptr && size != 0) | |
| RecordAlloc(self->next, ret, size, context); | |
| return ret; | |
| } | |
| void FreeFn(const AllocatorDispatch* self, void* address, void* context) { | |
| if (address != nullptr) | |
| RecordFree(self->next, address, context); | |
| self->next->free_function(self->next, address, context); | |
| } | |
| size_t GetSizeEstimateFn(const AllocatorDispatch* self, | |
| void* address, | |
| void* context) { | |
| return self->next->get_size_estimate_function(self->next, address, context); | |
| } | |
| unsigned BatchMallocFn(const AllocatorDispatch* self, | |
| size_t size, | |
| void** results, | |
| unsigned num_requested, | |
| void* context) { | |
| unsigned count = self->next->batch_malloc_function(self->next, size, results, | |
| num_requested, context); | |
| for (unsigned i = 0; i < count; ++i) { | |
| RecordAlloc(self->next, results[i], size, context); | |
| } | |
| return count; | |
| } | |
| void BatchFreeFn(const AllocatorDispatch* self, | |
| void** to_be_freed, | |
| unsigned num_to_be_freed, | |
| void* context) { | |
| for (unsigned i = 0; i < num_to_be_freed; ++i) { | |
| if (to_be_freed[i] != nullptr) { | |
| RecordFree(self->next, to_be_freed[i], context); | |
| } | |
| } | |
| self->next->batch_free_function(self->next, to_be_freed, num_to_be_freed, | |
| context); | |
| } | |
| void FreeDefiniteSizeFn(const AllocatorDispatch* self, | |
| void* ptr, | |
| size_t size, | |
| void* context) { | |
| if (ptr != nullptr) | |
| RecordFree(self->next, ptr, context); | |
| self->next->free_definite_size_function(self->next, ptr, size, context); | |
| } | |
| // The allocator dispatch used to intercept heap operations. | |
| AllocatorDispatch allocator_dispatch = {&AllocFn, | |
| &AllocZeroInitializedFn, | |
| &AllocAlignedFn, | |
| &ReallocFn, | |
| &FreeFn, | |
| &GetSizeEstimateFn, | |
| &BatchMallocFn, | |
| &BatchFreeFn, | |
| &FreeDefiniteSizeFn, | |
| nullptr}; | |
| ThreadHeapUsage* GetOrCreateThreadUsage() { | |
| auto tls_ptr = reinterpret_cast<uintptr_t>(ThreadAllocationUsage().Get()); | |
| if ((tls_ptr & kSentinelMask) == kSentinelMask) | |
| return nullptr; // Re-entrancy case. | |
| auto* allocator_usage = reinterpret_cast<ThreadHeapUsage*>(tls_ptr); | |
| if (allocator_usage == nullptr) { | |
| // Prevent reentrancy due to the allocation below. | |
| ThreadAllocationUsage().Set(kInitializationSentinel); | |
| allocator_usage = new ThreadHeapUsage(); | |
| static_assert(std::is_pod<ThreadHeapUsage>::value, | |
| "AllocatorDispatch must be POD"); | |
| memset(allocator_usage, 0, sizeof(*allocator_usage)); | |
| ThreadAllocationUsage().Set(allocator_usage); | |
| } | |
| return allocator_usage; | |
| } | |
| } // namespace | |
| ThreadHeapUsageTracker::ThreadHeapUsageTracker() : thread_usage_(nullptr) { | |
| static_assert(std::is_pod<ThreadHeapUsage>::value, "Must be POD."); | |
| } | |
| ThreadHeapUsageTracker::~ThreadHeapUsageTracker() { | |
| DCHECK(thread_checker_.CalledOnValidThread()); | |
| if (thread_usage_ != nullptr) { | |
| // If this tracker wasn't stopped, make it inclusive so that the | |
| // usage isn't lost. | |
| Stop(false); | |
| } | |
| } | |
| void ThreadHeapUsageTracker::Start() { | |
| DCHECK(thread_checker_.CalledOnValidThread()); | |
| thread_usage_ = GetOrCreateThreadUsage(); | |
| usage_ = *thread_usage_; | |
| // Reset the stats for our current scope. | |
| // The per-thread usage instance now tracks this scope's usage, while this | |
| // instance persists the outer scope's usage stats. On destruction, this | |
| // instance will restore the outer scope's usage stats with this scope's | |
| // usage added. | |
| memset(thread_usage_, 0, sizeof(*thread_usage_)); | |
| } | |
| void ThreadHeapUsageTracker::Stop(bool usage_is_exclusive) { | |
| DCHECK(thread_checker_.CalledOnValidThread()); | |
| DCHECK_NE(nullptr, thread_usage_); | |
| ThreadHeapUsage current = *thread_usage_; | |
| if (usage_is_exclusive) { | |
| // Restore the outer scope. | |
| *thread_usage_ = usage_; | |
| } else { | |
| // Update the outer scope with the accrued inner usage. | |
| if (thread_usage_->max_allocated_bytes) { | |
| uint64_t outer_net_alloc_bytes = usage_.alloc_bytes - usage_.free_bytes; | |
| thread_usage_->max_allocated_bytes = | |
| std::max(usage_.max_allocated_bytes, | |
| outer_net_alloc_bytes + thread_usage_->max_allocated_bytes); | |
| } | |
| thread_usage_->alloc_ops += usage_.alloc_ops; | |
| thread_usage_->alloc_bytes += usage_.alloc_bytes; | |
| thread_usage_->alloc_overhead_bytes += usage_.alloc_overhead_bytes; | |
| thread_usage_->free_ops += usage_.free_ops; | |
| thread_usage_->free_bytes += usage_.free_bytes; | |
| } | |
| thread_usage_ = nullptr; | |
| usage_ = current; | |
| } | |
| ThreadHeapUsage ThreadHeapUsageTracker::GetUsageSnapshot() { | |
| ThreadHeapUsage* usage = GetOrCreateThreadUsage(); | |
| DCHECK_NE(nullptr, usage); | |
| return *usage; | |
| } | |
| void ThreadHeapUsageTracker::EnableHeapTracking() { | |
| EnsureTLSInitialized(); | |
| CHECK_EQ(false, g_heap_tracking_enabled) << "No double-enabling."; | |
| g_heap_tracking_enabled = true; | |
| #if BUILDFLAG(USE_ALLOCATOR_SHIM) | |
| base::allocator::InsertAllocatorDispatch(&allocator_dispatch); | |
| #else | |
| CHECK(false) << "Can't enable heap tracking without the shim."; | |
| #endif // BUILDFLAG(USE_ALLOCATOR_SHIM) | |
| } | |
| bool ThreadHeapUsageTracker::IsHeapTrackingEnabled() { | |
| return g_heap_tracking_enabled; | |
| } | |
| void ThreadHeapUsageTracker::DisableHeapTrackingForTesting() { | |
| #if BUILDFLAG(USE_ALLOCATOR_SHIM) | |
| base::allocator::RemoveAllocatorDispatchForTesting(&allocator_dispatch); | |
| #else | |
| CHECK(false) << "Can't disable heap tracking without the shim."; | |
| #endif // BUILDFLAG(USE_ALLOCATOR_SHIM) | |
| DCHECK_EQ(true, g_heap_tracking_enabled) << "Heap tracking not enabled."; | |
| g_heap_tracking_enabled = false; | |
| } | |
| base::allocator::AllocatorDispatch* | |
| ThreadHeapUsageTracker::GetDispatchForTesting() { | |
| return &allocator_dispatch; | |
| } | |
| void ThreadHeapUsageTracker::EnsureTLSInitialized() { | |
| ignore_result(ThreadAllocationUsage()); | |
| } | |
| } // namespace debug | |
| } // namespace base |