blob: 8f9f83da4351385d4bd22c33abbd9d72bc46453d [file] [log] [blame]
/*
* 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 "starboard/client_porting/poem/string_leaks_poem.h"
#include "nb/analytics/memory_tracker_impl.h"
#include <algorithm>
#include <cstring>
#include <functional>
#include <iomanip>
#include <iterator>
#include <sstream>
#include "nb/concurrent_map.h"
#include "starboard/atomic.h"
#include "starboard/common/flat_map.h"
#include "starboard/log.h"
#include "starboard/time.h"
namespace nb {
namespace analytics {
namespace {
// This class allows std containers to bypass memory reporting.
template <typename T>
class RawAllocator : public std::allocator<T> {
public:
typedef typename std::allocator<T>::pointer pointer;
typedef typename std::allocator<T>::const_pointer const_pointer;
typedef typename std::allocator<T>::reference reference;
typedef typename std::allocator<T>::const_reference const_reference;
typedef typename std::allocator<T>::size_type size_type;
typedef typename std::allocator<T>::value_type value_type;
typedef typename std::allocator<T>::difference_type difference_type;
RawAllocator() {}
// Constructor used for rebinding
template <typename U>
RawAllocator(const RawAllocator<U>& x) {}
pointer allocate(size_type n,
std::allocator<void>::const_pointer hint = NULL) {
void* ptr = SbMemoryAllocateNoReport(n * sizeof(value_type));
return static_cast<pointer>(ptr);
}
void deallocate(pointer p, size_type n) { SbMemoryDeallocateNoReport(p); }
template <typename U>
struct rebind {
typedef RawAllocator<U> other;
};
};
} // namespace.
// ThreadLocalBool_NoReport class guarantees that any memory allocated will NOT
// go through any memory reporting mechanism. Otherwise certain platforms will
// encounter a stack overflow condition as execution re-enters the allocation
// function.
class MemoryTrackerImpl::ThreadLocalBool_NoReport {
public:
void Set(bool val) {
if (val) {
thread_map_.Set(SbThreadGetId(), val);
} else {
// Rather than set to false, remove the element.
thread_map_.Remove(SbThreadGetId());
}
}
bool Get() {
ThreadMap::EntryHandle entry;
bool has_key = thread_map_.Get(SbThreadGetId(), &entry);
if (!has_key) {
return false;
}
return entry.Value();
}
private:
// Create a concurrent map that supports fast deletion of elements
// (no memory release).
//
using PairType = std::vector<std::pair<SbThreadId, bool>>::value_type;
// This vector bypasses reporting allocators.
using VectorPair = std::vector<PairType, RawAllocator<PairType>>;
// The InnerMap is backed by the vector. The FlatMap transforms the vector
// into a map interface.
using InnerMap = starboard::FlatMap<SbThreadId, bool,
std::less<SbThreadId>,
VectorPair>;
// Concurrent map uses distributed locking to achieve a highly concurrent
// unsorted map.
using ThreadMap = ConcurrentMap<SbThreadId,
bool,
std::hash<SbThreadId>,
InnerMap>;
ThreadMap thread_map_;
};
SbMemoryReporter* MemoryTrackerImpl::GetMemoryReporter() {
return &sb_memory_tracker_;
}
NbMemoryScopeReporter* MemoryTrackerImpl::GetMemoryScopeReporter() {
return &nb_memory_scope_reporter_;
}
int64_t MemoryTrackerImpl::GetTotalAllocationBytes() {
return total_bytes_allocated_.load();
}
AllocationGroup* MemoryTrackerImpl::GetAllocationGroup(const char* name) {
DisableMemoryTrackingInScope no_tracking(this);
AllocationGroup* alloc_group = alloc_group_map_.Ensure(name);
return alloc_group;
}
void MemoryTrackerImpl::PushAllocationGroupByName(const char* group_name) {
AllocationGroup* group = GetAllocationGroup(group_name);
PushAllocationGroup(group);
}
void MemoryTrackerImpl::PushAllocationGroup(AllocationGroup* alloc_group) {
if (alloc_group == NULL) {
alloc_group = alloc_group_map_.GetDefaultUnaccounted();
}
DisableMemoryTrackingInScope no_tracking(this);
allocation_group_stack_tls_.GetOrCreate()->Push(alloc_group);
}
AllocationGroup* MemoryTrackerImpl::PeekAllocationGroup() {
DisableMemoryTrackingInScope no_tracking(this);
AllocationGroup* out =
allocation_group_stack_tls_.GetOrCreate()->Peek();
if (out == NULL) {
out = alloc_group_map_.GetDefaultUnaccounted();
}
return out;
}
void MemoryTrackerImpl::PopAllocationGroup() {
DisableMemoryTrackingInScope no_tracking(this);
AllocationGroupStack* alloc_tls = allocation_group_stack_tls_.GetOrCreate();
alloc_tls->Pop();
AllocationGroup* group = alloc_tls->Peek();
// We don't allow null, so if this is encountered then push the
// "default unaccounted" alloc group.
if (group == NULL) {
alloc_tls->Push(alloc_group_map_.GetDefaultUnaccounted());
}
}
void MemoryTrackerImpl::GetAllocationGroups(
std::vector<const AllocationGroup*>* output) {
DisableMemoryTrackingInScope no_allocation_tracking(this);
output->reserve(100);
alloc_group_map_.GetAll(output);
}
void MemoryTrackerImpl::GetAllocationGroups(
std::map<std::string, const AllocationGroup*>* output) {
output->clear();
DisableMemoryTrackingInScope no_tracking(this);
std::vector<const AllocationGroup*> tmp;
GetAllocationGroups(&tmp);
for (size_t i = 0; i < tmp.size(); ++i) {
output->insert(std::make_pair(tmp[i]->name(), tmp[i]));
}
}
void MemoryTrackerImpl::Accept(AllocationVisitor* visitor) {
DisableMemoryTrackingInScope no_mem_allocation_reporting(this);
DisableDeletionInScope no_mem_deletion_reporting(this);
atomic_allocation_map_.Accept(visitor);
}
void MemoryTrackerImpl::Clear() {
// Prevent clearing of the tree from triggering a re-entrant
// memory deallocation.
atomic_allocation_map_.Clear();
total_bytes_allocated_.store(0);
}
void MemoryTrackerImpl::Debug_PushAllocationGroupBreakPointByName(
const char* group_name) {
DisableMemoryTrackingInScope no_tracking(this);
SB_DCHECK(group_name != NULL);
AllocationGroup* group = alloc_group_map_.Ensure(group_name);
Debug_PushAllocationGroupBreakPoint(group);
}
void MemoryTrackerImpl::Debug_PushAllocationGroupBreakPoint(
AllocationGroup* alloc_group) {
DisableMemoryTrackingInScope no_tracking(this);
allocation_group_stack_tls_.GetOrCreate()->Push_DebugBreak(alloc_group);
}
void MemoryTrackerImpl::Debug_PopAllocationGroupBreakPoint() {
DisableMemoryTrackingInScope no_tracking(this);
allocation_group_stack_tls_.GetOrCreate()->Pop_DebugBreak();
}
void MemoryTrackerImpl::OnMalloc(void* context,
const void* memory,
size_t size) {
MemoryTrackerImpl* t = static_cast<MemoryTrackerImpl*>(context);
t->AddMemoryTracking(memory, size);
}
void MemoryTrackerImpl::OnDealloc(void* context, const void* memory) {
MemoryTrackerImpl* t = static_cast<MemoryTrackerImpl*>(context);
t->RemoveMemoryTracking(memory);
}
void MemoryTrackerImpl::OnMapMem(void* context,
const void* memory,
size_t size) {
// We might do something more interesting with MapMemory calls later.
OnMalloc(context, memory, size);
}
void MemoryTrackerImpl::OnUnMapMem(void* context,
const void* memory,
size_t size) {
// We might do something more interesting with UnMapMemory calls later.
OnDealloc(context, memory);
}
void MemoryTrackerImpl::OnPushAllocationGroup(
void* context,
NbMemoryScopeInfo* memory_scope_info) {
MemoryTrackerImpl* t = static_cast<MemoryTrackerImpl*>(context);
void** cached_handle = &(memory_scope_info->cached_handle_);
const bool allows_caching = memory_scope_info->allows_caching_;
const char* group_name = memory_scope_info->memory_scope_name_;
AllocationGroup* group = NULL;
if (allows_caching && *cached_handle != NULL) {
group = static_cast<AllocationGroup*>(*cached_handle);
} else {
group = t->GetAllocationGroup(group_name);
if (allows_caching) {
// Flush all pending writes so that the the pointee is well formed
// by the time the pointer becomes visible to other threads.
SbAtomicMemoryBarrier();
*cached_handle = static_cast<void*>(group);
}
}
DisableMemoryTrackingInScope no_memory_tracking_in_scope(t);
t->PushAllocationGroup(group);
CallStack* callstack = t->callstack_tls_.GetOrCreate();
callstack->push_back(memory_scope_info);
}
void MemoryTrackerImpl::OnPopAllocationGroup(void* context) {
MemoryTrackerImpl* t = static_cast<MemoryTrackerImpl*>(context);
t->PopAllocationGroup();
CallStack* callstack = t->callstack_tls_.GetOrCreate();
// Callstack can be empty when the memory tracker binds to the callback
// system while the program is in the middle of the execution.
if (!callstack->empty()) {
callstack->pop_back();
}
}
void MemoryTrackerImpl::Initialize(
SbMemoryReporter* sb_memory_reporter,
NbMemoryScopeReporter* memory_scope_reporter) {
SbMemoryReporter mem_reporter = {
MemoryTrackerImpl::OnMalloc, MemoryTrackerImpl::OnDealloc,
MemoryTrackerImpl::OnMapMem, MemoryTrackerImpl::OnUnMapMem,
this};
NbMemoryScopeReporter mem_scope_reporter = {
MemoryTrackerImpl::OnPushAllocationGroup,
MemoryTrackerImpl::OnPopAllocationGroup,
this,
};
*sb_memory_reporter = mem_reporter;
*memory_scope_reporter = mem_scope_reporter;
}
void MemoryTrackerImpl::SetThreadFilter(SbThreadId tid) {
thread_filter_id_ = tid;
}
bool MemoryTrackerImpl::IsCurrentThreadAllowedToReport() const {
if (thread_filter_id_ == kSbThreadInvalidId) {
return true;
}
return SbThreadGetId() == thread_filter_id_;
}
void MemoryTrackerImpl::SetMemoryTrackerDebugCallback(
MemoryTrackerDebugCallback* cb) {
debug_callback_.swap(cb);
}
MemoryTrackerImpl::DisableDeletionInScope::DisableDeletionInScope(
MemoryTrackerImpl* owner)
: owner_(owner) {
prev_state_ = owner->MemoryDeletionEnabled();
owner_->SetMemoryDeletionEnabled(false);
}
MemoryTrackerImpl::DisableDeletionInScope::~DisableDeletionInScope() {
owner_->SetMemoryDeletionEnabled(prev_state_);
}
MemoryTrackerImpl::MemoryTrackerImpl()
: thread_filter_id_(kSbThreadInvalidId), debug_callback_(nullptr) {
memory_deletion_enabled_tls_.reset(new ThreadLocalBool_NoReport);
memory_tracking_disabled_tls_.reset(new ThreadLocalBool_NoReport);
total_bytes_allocated_.store(0);
global_hooks_installed_ = false;
Initialize(&sb_memory_tracker_, &nb_memory_scope_reporter_);
// Push the default region so that stats can be accounted for.
PushAllocationGroup(alloc_group_map_.GetDefaultUnaccounted());
}
MemoryTrackerImpl::~MemoryTrackerImpl() {
// If we are currently hooked into allocation tracking...
if (global_hooks_installed_) {
RemoveGlobalTrackingHooks();
// For performance reasons no locking is used on the tracker.
// Therefore give enough time for other threads to exit this tracker
// before fully destroying this object.
SbThreadSleep(250 * kSbTimeMillisecond); // 250 millisecond wait.
}
}
bool MemoryTrackerImpl::AddMemoryTracking(const void* memory, size_t size) {
// Vars are stored to assist in debugging.
const bool thread_allowed_to_report = IsCurrentThreadAllowedToReport();
const bool valid_memory_request = (memory != NULL) && (size != 0);
const bool mem_track_enabled = IsMemoryTrackingEnabled();
const bool tracking_enabled =
mem_track_enabled && valid_memory_request && thread_allowed_to_report;
if (!tracking_enabled) {
return false;
}
// End all memory tracking in subsequent data structures.
DisableMemoryTrackingInScope no_memory_tracking(this);
AllocationGroupStack* alloc_stack =
allocation_group_stack_tls_.GetOrCreate();
AllocationGroup* group = alloc_stack->Peek();
if (!group) {
group = alloc_group_map_.GetDefaultUnaccounted();
}
#ifndef NDEBUG
// This section of the code is designed to allow a developer to break
// execution whenever the debug allocation stack is in scope, so that the
// allocations can be stepped through. This is a THREAD LOCAL operation.
// Example:
// Debug_PushAllocationGroupBreakPointByName("Javascript");
// ...now set a break point below at "static int i = 0"
if (group && (group == alloc_stack->Peek_DebugBreak()) &&
alloc_stack->Peek_DebugBreak()) {
static int i = 0; // This static is here to allow an
++i; // easy breakpoint in the debugger
}
#endif
AllocationRecord alloc_record(size, group);
bool added = atomic_allocation_map_.Add(memory, alloc_record);
if (added) {
AddAllocationBytes(size);
group->AddAllocation(size);
ConcurrentPtr<MemoryTrackerDebugCallback>::Access access_ptr =
debug_callback_.access_ptr(SbThreadId());
// If access_ptr is valid then it is guaranteed to be alive for the
// duration of the scope.
if (access_ptr) {
const CallStack& callstack = *(callstack_tls_.GetOrCreate());
DisableDeletionInScope disable_deletion_tracking(this);
access_ptr->OnMemoryAllocation(memory, alloc_record, callstack);
}
} else {
// Handles the case where the memory hasn't been properly been reported
// released. This is less serious than you would think because the memory
// allocator in the system will recycle the memory and it will come back.
AllocationRecord unexpected_alloc;
atomic_allocation_map_.Get(memory, &unexpected_alloc);
AllocationGroup* prev_group = unexpected_alloc.allocation_group;
std::string prev_group_name;
if (prev_group) {
prev_group_name = unexpected_alloc.allocation_group->name();
} else {
prev_group_name = "none";
}
if (!added) {
std::stringstream ss;
ss << "\nUnexpected condition, previous allocation was not removed:\n"
<< "\tprevious alloc group: " << prev_group_name << "\n"
<< "\tnew alloc group: " << group->name() << "\n"
<< "\tprevious size: " << unexpected_alloc.size << "\n"
<< "\tnew size: " << size << "\n";
SbLogRaw(ss.str().c_str());
}
}
return added;
}
size_t MemoryTrackerImpl::RemoveMemoryTracking(const void* memory) {
const bool do_remove = memory && MemoryDeletionEnabled();
if (!do_remove) {
return 0;
}
AllocationRecord alloc_record;
bool removed = false;
ConcurrentPtr<MemoryTrackerDebugCallback>::Access access_ptr =
debug_callback_.access_ptr(SbThreadId());
if (access_ptr) {
if (atomic_allocation_map_.Get(memory, &alloc_record)) {
DisableMemoryTrackingInScope no_memory_tracking(this);
const CallStack& callstack = (*callstack_tls_.GetOrCreate());
access_ptr->OnMemoryDeallocation(memory, alloc_record, callstack);
}
}
// Prevent a map::erase() from causing an endless stack overflow by
// disabling memory deletion for the very limited scope.
{
DisableDeletionInScope no_memory_deletion(this);
removed = atomic_allocation_map_.Remove(memory, &alloc_record);
}
if (!removed) {
return 0;
} else {
const int64_t alloc_size = (static_cast<int64_t>(alloc_record.size));
AllocationGroup* group = alloc_record.allocation_group;
if (group) {
group->AddAllocation(-alloc_size);
}
AddAllocationBytes(-alloc_size);
return alloc_record.size;
}
}
bool MemoryTrackerImpl::GetMemoryTracking(const void* memory,
AllocationRecord* record) const {
const bool exists = atomic_allocation_map_.Get(memory, record);
return exists;
}
void MemoryTrackerImpl::SetMemoryTrackingEnabled(bool on) {
memory_tracking_disabled_tls_->Set(!on);
}
bool MemoryTrackerImpl::IsMemoryTrackingEnabled() const {
const bool enabled = !memory_tracking_disabled_tls_->Get();
return enabled;
}
void MemoryTrackerImpl::AddAllocationBytes(int64_t val) {
total_bytes_allocated_.fetch_add(val);
}
bool MemoryTrackerImpl::MemoryDeletionEnabled() const {
return !memory_deletion_enabled_tls_->Get();
}
void MemoryTrackerImpl::SetMemoryDeletionEnabled(bool on) {
memory_deletion_enabled_tls_->Set(!on);
}
MemoryTrackerImpl::DisableMemoryTrackingInScope::DisableMemoryTrackingInScope(
MemoryTrackerImpl* t)
: owner_(t) {
prev_value_ = owner_->IsMemoryTrackingEnabled();
owner_->SetMemoryTrackingEnabled(false);
}
MemoryTrackerImpl::DisableMemoryTrackingInScope::
~DisableMemoryTrackingInScope() {
owner_->SetMemoryTrackingEnabled(prev_value_);
}
} // namespace analytics
} // namespace nb