blob: 1633cdcdca27c5459929d20db0746f02b6cf22cb [file] [log] [blame]
// Copyright 2017 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 "chrome/common/profiling/memlog_allocator_shim.h"
#include "base/allocator/allocator_shim.h"
#include "base/allocator/buildflags.h"
#include "base/allocator/partition_allocator/partition_alloc.h"
#include "base/atomicops.h"
#include "base/compiler_specific.h"
#include "base/debug/debugging_buildflags.h"
#include "base/debug/stack_trace.h"
#include "base/lazy_instance.h"
#include "base/no_destructor.h"
#include "base/numerics/safe_conversions.h"
#include "base/rand_util.h"
#include "base/synchronization/lock.h"
#include "base/threading/thread_id_name_manager.h"
#include "base/threading/thread_local.h"
#include "base/trace_event/heap_profiler_allocation_context_tracker.h"
#include "base/trace_event/heap_profiler_allocation_register.h"
#include "base/trace_event/heap_profiler_event_filter.h"
#include "base/trace_event/memory_dump_manager.h"
#include "build/build_config.h"
#include "chrome/common/profiling/memlog_stream.h"
#if defined(OS_POSIX)
#include <limits.h>
#endif
#if defined(OS_LINUX) || defined(OS_ANDROID)
#include <sys/prctl.h>
#endif
using base::trace_event::AllocationContext;
using base::trace_event::AllocationContextTracker;
using CaptureMode = base::trace_event::AllocationContextTracker::CaptureMode;
namespace profiling {
namespace {
using base::allocator::AllocatorDispatch;
base::LazyInstance<base::OnceClosure>::Leaky g_on_init_allocator_shim_callback_;
base::LazyInstance<scoped_refptr<base::TaskRunner>>::Leaky
g_on_init_allocator_shim_task_runner_;
MemlogSenderPipe* g_sender_pipe = nullptr;
// In NATIVE stack mode, whether to insert stack names into the backtraces.
bool g_include_thread_names = false;
// Whether to sample allocations.
bool g_sample_allocations = false;
// Sampling rate describes the probability of sampling small allocations.
// Probability = MIN((size of allocation) / g_sampling_rate, 1).
uint32_t g_sampling_rate = 0;
// Prime since this is used like a hash table. Numbers of this magnitude seemed
// to provide sufficient parallelism to avoid lock overhead in ad-hoc testing.
constexpr int kNumSendBuffers = 17;
// If writing to the MemlogSenderPipe ever takes longer than 10s, just give up.
constexpr int kTimeoutMs = 10000;
// Functions set by a callback if the GC heap exists in the current process.
// This function pointers can be used to hook or unhook the oilpan allocations.
// It will be null in the browser process.
SetGCAllocHookFunction g_hook_gc_alloc = nullptr;
SetGCFreeHookFunction g_hook_gc_free = nullptr;
// In the very unlikely scenario where a thread has grabbed the SendBuffer lock,
// and then performs a heap allocation/free, ignore the allocation. Failing to
// do so will cause non-deterministic deadlock, depending on whether the
// allocation is dispatched to the same SendBuffer.
//
// On macOS, this flag is also used to prevent double-counting during sampling.
// The implementation of libmalloc will sometimes call malloc [from
// one zone to another] - without this flag, the allocation would get two
// chances of being sampled.
base::LazyInstance<base::ThreadLocalBoolean>::Leaky g_prevent_reentrancy =
LAZY_INSTANCE_INITIALIZER;
// The allocator shim needs to retain some additional state for each thread.
struct ShimState {
// The pointer must be valid for the lifetime of the process.
const char* thread_name = nullptr;
// If we are using pseudo stacks, we need to inform the profiling service of
// the address to string mapping. To avoid a global lock, we keep a
// thread-local unordered_set of every address that has been sent from the
// thread in question.
std::unordered_set<const void*> sent_strings;
// When we are sampling, each allocation's size is subtracted from
// |interval_to_next_sample|. When |interval_to_next_sample| is 0 or lower,
// the allocation is sampled, and |interval_to_next_sample| is reset.
int32_t interval_to_next_sample = 0;
};
// This algorithm is copied from "v8/src/profiler/sampling-heap-profiler.cc".
// We sample with a Poisson process, with constant average sampling interval.
// This follows the exponential probability distribution with parameter
// λ = 1/rate where rate is the average number of bytes between samples.
//
// Let u be a uniformly distributed random number between 0 and 1, then
// next_sample = (- ln u) / λ
int32_t GetNextSampleInterval(uint32_t rate) {
double u = base::RandDouble(); // Random value in [0, 1)
double v = 1 - u; // Random value in (0, 1]
double next = (-std::log(v)) * rate;
int32_t next_int = static_cast<int32_t>(next);
if (next_int < 1)
return 1;
return next_int;
}
// This function is added to the TLS slot to clean up the instance when the
// thread exits.
void DestructShimState(void* shim_state) {
delete static_cast<ShimState*>(shim_state);
}
base::ThreadLocalStorage::Slot& ShimStateTLS() {
static base::NoDestructor<base::ThreadLocalStorage::Slot> shim_state_tls(
&DestructShimState);
return *shim_state_tls;
}
// We don't need to worry about re-entrancy because g_prevent_reentrancy
// already guards against that.
ShimState* GetShimState() {
ShimState* state = static_cast<ShimState*>(ShimStateTLS().Get());
if (!state) {
state = new ShimState();
ShimStateTLS().Set(state);
}
return state;
}
// Set the thread name, which is a pointer to a leaked string, to ensure
// validity forever.
void SetCurrentThreadName(const char* name) {
GetShimState()->thread_name = name;
}
// Cannot call ThreadIdNameManager::GetName because it holds a lock and causes
// deadlock when lock is already held by ThreadIdNameManager before the current
// allocation. Gets the thread name from kernel if available or returns a string
// with id. This function intentionally leaks the allocated strings since they
// are used to tag allocations even after the thread dies.
const char* GetAndLeakThreadName() {
// prctl requires 16 bytes, snprintf requires 19, pthread_getname_np requires
// 64 on macOS, see PlatformThread::SetName in platform_thread_mac.mm.
constexpr size_t kBufferLen = 64;
char name[kBufferLen];
#if defined(OS_LINUX) || defined(OS_ANDROID)
// If the thread name is not set, try to get it from prctl. Thread name might
// not be set in cases where the thread started before heap profiling was
// enabled.
int err = prctl(PR_GET_NAME, name);
if (!err) {
return strdup(name);
}
#elif defined(OS_MACOSX)
int err = pthread_getname_np(pthread_self(), name, kBufferLen);
if (err == 0 && name[0] != '\0') {
return strdup(name);
}
#endif // defined(OS_LINUX) || defined(OS_ANDROID)
// Use tid if we don't have a thread name.
snprintf(name, sizeof(name), "Thread %lu",
static_cast<unsigned long>(base::PlatformThread::CurrentId()));
return strdup(name);
}
// Returns the thread name, looking it up if necessary.
const char* GetOrSetThreadName() {
const char* thread_name = GetShimState()->thread_name;
if (UNLIKELY(!thread_name)) {
thread_name = GetAndLeakThreadName();
GetShimState()->thread_name = thread_name;
}
return thread_name;
}
class SendBuffer {
public:
SendBuffer() : buffer_(new char[MemlogSenderPipe::kPipeSize]) {}
~SendBuffer() { delete[] buffer_; }
void Send(const void* data, size_t sz) {
base::AutoLock lock(lock_);
if (used_ + sz > MemlogSenderPipe::kPipeSize)
SendCurrentBuffer();
memcpy(&buffer_[used_], data, sz);
used_ += sz;
}
void Flush() {
base::AutoLock lock(lock_);
if (used_ > 0)
SendCurrentBuffer();
}
private:
void SendCurrentBuffer() {
MemlogSenderPipe::Result result =
g_sender_pipe->Send(buffer_, used_, kTimeoutMs);
used_ = 0;
if (result == MemlogSenderPipe::Result::kError)
StopAllocatorShimDangerous();
if (result == MemlogSenderPipe::Result::kTimeout) {
StopAllocatorShimDangerous();
// TODO(erikchen): Emit a histogram. https://crbug.com/777546.
}
}
base::Lock lock_;
char* buffer_;
size_t used_ = 0;
DISALLOW_COPY_AND_ASSIGN(SendBuffer);
};
// It's safe to call Read() before Write(). Read() will either return nullptr or
// a valid SendBuffer.
class AtomicallyConsistentSendBufferArray {
public:
void Write(SendBuffer* buffer) {
base::subtle::Release_Store(
&send_buffers, reinterpret_cast<base::subtle::AtomicWord>(buffer));
}
SendBuffer* Read() {
return reinterpret_cast<SendBuffer*>(
base::subtle::Acquire_Load(&send_buffers));
}
private:
// This class is used as a static global. This will be linker-initialized to
// 0.
base::subtle::AtomicWord send_buffers;
};
// The API guarantees that Read() will either return a valid object or a
// nullptr.
AtomicallyConsistentSendBufferArray g_send_buffers;
// "address" is the address in question, which is used to select which send
// buffer to use.
void DoSend(const void* address,
const void* data,
size_t size,
SendBuffer* send_buffers) {
base::trace_event::AllocationRegister::AddressHasher hasher;
int bin_to_use = hasher(address) % kNumSendBuffers;
send_buffers[bin_to_use].Send(data, size);
}
#if BUILDFLAG(USE_ALLOCATOR_SHIM)
void* HookAlloc(const AllocatorDispatch* self, size_t size, void* context) {
const AllocatorDispatch* const next = self->next;
// If this is our first time passing through, set the reentrancy bit.
bool reentering = g_prevent_reentrancy.Pointer()->Get();
if (LIKELY(!reentering))
g_prevent_reentrancy.Pointer()->Set(true);
void* ptr = next->alloc_function(next, size, context);
if (LIKELY(!reentering)) {
AllocatorShimLogAlloc(AllocatorType::kMalloc, ptr, size, nullptr);
g_prevent_reentrancy.Pointer()->Set(false);
}
return ptr;
}
void* HookZeroInitAlloc(const AllocatorDispatch* self,
size_t n,
size_t size,
void* context) {
const AllocatorDispatch* const next = self->next;
// If this is our first time passing through, set the reentrancy bit.
bool reentering = g_prevent_reentrancy.Pointer()->Get();
if (LIKELY(!reentering))
g_prevent_reentrancy.Pointer()->Set(true);
void* ptr = next->alloc_zero_initialized_function(next, n, size, context);
if (LIKELY(!reentering)) {
AllocatorShimLogAlloc(AllocatorType::kMalloc, ptr, n * size, nullptr);
g_prevent_reentrancy.Pointer()->Set(false);
}
return ptr;
}
void* HookAllocAligned(const AllocatorDispatch* self,
size_t alignment,
size_t size,
void* context) {
const AllocatorDispatch* const next = self->next;
// If this is our first time passing through, set the reentrancy bit.
bool reentering = g_prevent_reentrancy.Pointer()->Get();
if (LIKELY(!reentering))
g_prevent_reentrancy.Pointer()->Set(true);
void* ptr = next->alloc_aligned_function(next, alignment, size, context);
if (LIKELY(!reentering)) {
AllocatorShimLogAlloc(AllocatorType::kMalloc, ptr, size, nullptr);
g_prevent_reentrancy.Pointer()->Set(false);
}
return ptr;
}
void* HookRealloc(const AllocatorDispatch* self,
void* address,
size_t size,
void* context) {
const AllocatorDispatch* const next = self->next;
// If this is our first time passing through, set the reentrancy bit.
bool reentering = g_prevent_reentrancy.Pointer()->Get();
if (LIKELY(!reentering))
g_prevent_reentrancy.Pointer()->Set(true);
void* ptr = next->realloc_function(next, address, size, context);
if (LIKELY(!reentering)) {
AllocatorShimLogFree(address);
if (size > 0) // realloc(size == 0) means free()
AllocatorShimLogAlloc(AllocatorType::kMalloc, ptr, size, nullptr);
g_prevent_reentrancy.Pointer()->Set(false);
}
return ptr;
}
void HookFree(const AllocatorDispatch* self, void* address, void* context) {
// If this is our first time passing through, set the reentrancy bit.
bool reentering = g_prevent_reentrancy.Pointer()->Get();
if (LIKELY(!reentering))
g_prevent_reentrancy.Pointer()->Set(true);
const AllocatorDispatch* const next = self->next;
next->free_function(next, address, context);
if (LIKELY(!reentering)) {
AllocatorShimLogFree(address);
g_prevent_reentrancy.Pointer()->Set(false);
}
}
size_t HookGetSizeEstimate(const AllocatorDispatch* self,
void* address,
void* context) {
const AllocatorDispatch* const next = self->next;
return next->get_size_estimate_function(next, address, context);
}
unsigned HookBatchMalloc(const AllocatorDispatch* self,
size_t size,
void** results,
unsigned num_requested,
void* context) {
// If this is our first time passing through, set the reentrancy bit.
bool reentering = g_prevent_reentrancy.Pointer()->Get();
if (LIKELY(!reentering))
g_prevent_reentrancy.Pointer()->Set(true);
const AllocatorDispatch* const next = self->next;
unsigned count =
next->batch_malloc_function(next, size, results, num_requested, context);
if (LIKELY(!reentering)) {
for (unsigned i = 0; i < count; ++i)
AllocatorShimLogAlloc(AllocatorType::kMalloc, results[i], size, nullptr);
g_prevent_reentrancy.Pointer()->Set(false);
}
return count;
}
void HookBatchFree(const AllocatorDispatch* self,
void** to_be_freed,
unsigned num_to_be_freed,
void* context) {
// If this is our first time passing through, set the reentrancy bit.
bool reentering = g_prevent_reentrancy.Pointer()->Get();
if (LIKELY(!reentering))
g_prevent_reentrancy.Pointer()->Set(true);
const AllocatorDispatch* const next = self->next;
next->batch_free_function(next, to_be_freed, num_to_be_freed, context);
if (LIKELY(!reentering)) {
for (unsigned i = 0; i < num_to_be_freed; ++i)
AllocatorShimLogFree(to_be_freed[i]);
g_prevent_reentrancy.Pointer()->Set(false);
}
}
void HookFreeDefiniteSize(const AllocatorDispatch* self,
void* ptr,
size_t size,
void* context) {
// If this is our first time passing through, set the reentrancy bit.
bool reentering = g_prevent_reentrancy.Pointer()->Get();
if (LIKELY(!reentering))
g_prevent_reentrancy.Pointer()->Set(true);
const AllocatorDispatch* const next = self->next;
next->free_definite_size_function(next, ptr, size, context);
if (LIKELY(!reentering)) {
AllocatorShimLogFree(ptr);
g_prevent_reentrancy.Pointer()->Set(false);
}
}
AllocatorDispatch g_memlog_hooks = {
&HookAlloc, // alloc_function
&HookZeroInitAlloc, // alloc_zero_initialized_function
&HookAllocAligned, // alloc_aligned_function
&HookRealloc, // realloc_function
&HookFree, // free_function
&HookGetSizeEstimate, // get_size_estimate_function
&HookBatchMalloc, // batch_malloc_function
&HookBatchFree, // batch_free_function
&HookFreeDefiniteSize, // free_definite_size_function
nullptr, // next
};
#endif // BUILDFLAG(USE_ALLOCATOR_SHIM)
void HookPartitionAlloc(void* address, size_t size, const char* type) {
// If this is our first time passing through, set the reentrancy bit.
if (LIKELY(!g_prevent_reentrancy.Pointer()->Get())) {
g_prevent_reentrancy.Pointer()->Set(true);
AllocatorShimLogAlloc(AllocatorType::kPartitionAlloc, address, size, type);
g_prevent_reentrancy.Pointer()->Set(false);
}
}
void HookPartitionFree(void* address) {
// If this is our first time passing through, set the reentrancy bit.
if (LIKELY(!g_prevent_reentrancy.Pointer()->Get())) {
g_prevent_reentrancy.Pointer()->Set(true);
AllocatorShimLogFree(address);
g_prevent_reentrancy.Pointer()->Set(false);
}
}
void HookGCAlloc(uint8_t* address, size_t size, const char* type) {
if (LIKELY(!g_prevent_reentrancy.Pointer()->Get())) {
g_prevent_reentrancy.Pointer()->Set(true);
AllocatorShimLogAlloc(AllocatorType::kOilpan, address, size, type);
g_prevent_reentrancy.Pointer()->Set(false);
}
}
void HookGCFree(uint8_t* address) {
if (LIKELY(!g_prevent_reentrancy.Pointer()->Get())) {
g_prevent_reentrancy.Pointer()->Set(true);
AllocatorShimLogFree(address);
g_prevent_reentrancy.Pointer()->Set(false);
}
}
// Updates an existing in_memory buffer with frame data. If a frame contains a
// pointer to a cstring rather than an instruction pointer, and the profiling
// service has not yet been informed of that pointer -> cstring mapping, sends a
// StringMappingPacket.
class FrameSerializer {
public:
FrameSerializer(uint64_t* stack,
const void* address,
size_t initial_buffer_size,
SendBuffer* send_buffers)
: stack_(stack),
address_(address),
remaining_buffer_size_(initial_buffer_size),
send_buffers_(send_buffers) {}
void AddAllFrames(const base::trace_event::Backtrace& backtrace) {
CHECK_LE(backtrace.frame_count, kMaxStackEntries);
size_t required_capacity = backtrace.frame_count * sizeof(uint64_t);
CHECK_LE(required_capacity, remaining_buffer_size_);
remaining_buffer_size_ -= required_capacity;
for (int i = base::checked_cast<int>(backtrace.frame_count) - 1; i >= 0;
--i) {
AddFrame(backtrace.frames[i]);
}
}
void AddAllInstructionPointers(size_t frame_count,
const void* const* frames) {
CHECK_LE(frame_count, kMaxStackEntries);
size_t required_capacity = frame_count * sizeof(uint64_t);
CHECK_LE(required_capacity, remaining_buffer_size_);
remaining_buffer_size_ -= required_capacity;
// If there are too many frames, keep the ones furthest from main().
for (size_t i = 0; i < frame_count; i++)
AddInstructionPointer(frames[i]);
}
void AddCString(const char* c_string) {
// Using a TLS cache of sent_strings avoids lock contention on malloc, which
// would kill performance.
std::unordered_set<const void*>* sent_strings =
&GetShimState()->sent_strings;
if (sent_strings->find(c_string) == sent_strings->end()) {
// No point in allowing arbitrarily long c-strings, which might cause pipe
// max length issues. Pick a reasonable length like 255.
static const size_t kMaxCStringLen = 255;
// length does not include the null terminator.
size_t length = strnlen(c_string, kMaxCStringLen);
char message[sizeof(StringMappingPacket) + kMaxCStringLen];
StringMappingPacket* string_mapping_packet =
new (&message) StringMappingPacket();
string_mapping_packet->address = reinterpret_cast<uint64_t>(c_string);
string_mapping_packet->string_len = length;
memcpy(message + sizeof(StringMappingPacket), c_string, length);
DoSend(address_, message, sizeof(StringMappingPacket) + length,
send_buffers_);
sent_strings->insert(c_string);
}
AddInstructionPointer(c_string);
}
size_t count() { return count_; }
private:
void AddFrame(const base::trace_event::StackFrame& frame) {
if (frame.type == base::trace_event::StackFrame::Type::PROGRAM_COUNTER) {
AddInstructionPointer(frame.value);
return;
}
AddCString(static_cast<const char*>(frame.value));
}
void AddInstructionPointer(const void* value) {
*stack_ = reinterpret_cast<uint64_t>(value);
++stack_;
++count_;
}
// The next frame should be written to this memory location. There are both
// static and runtime checks to prevent buffer overrun.
static_assert(
base::trace_event::Backtrace::kMaxFrameCount < kMaxStackEntries,
"Ensure that pseudo-stack frame count won't exceed OOP HP frame buffer.");
uint64_t* stack_;
// The number of frames that have been written to the stack.
size_t count_ = 0;
const void* address_;
size_t remaining_buffer_size_;
SendBuffer* send_buffers_;
};
} // namespace
void InitTLSSlot() {
ignore_result(g_prevent_reentrancy.Pointer()->Get());
ignore_result(ShimStateTLS());
}
// In order for pseudo stacks to work, trace event filtering must be enabled.
void EnableTraceEventFiltering() {
std::string filter_string = base::JoinString(
{"*", TRACE_DISABLED_BY_DEFAULT("net"), TRACE_DISABLED_BY_DEFAULT("cc"),
base::trace_event::MemoryDumpManager::kTraceCategory},
",");
base::trace_event::TraceConfigCategoryFilter category_filter;
category_filter.InitializeFromString(filter_string);
base::trace_event::TraceConfig::EventFilterConfig heap_profiler_filter_config(
base::trace_event::HeapProfilerEventFilter::kName);
heap_profiler_filter_config.SetCategoryFilter(category_filter);
base::trace_event::TraceConfig::EventFilters filters;
filters.push_back(heap_profiler_filter_config);
base::trace_event::TraceConfig filtering_trace_config;
filtering_trace_config.SetEventFilters(filters);
base::trace_event::TraceLog::GetInstance()->SetEnabled(
filtering_trace_config, base::trace_event::TraceLog::FILTERING_MODE);
}
void InitAllocatorShim(MemlogSenderPipe* sender_pipe,
mojom::ProfilingParamsPtr params) {
// Must be done before hooking any functions that make stack traces.
base::debug::EnableInProcessStackDumping();
g_sample_allocations = params->sampling_rate > 1;
g_sampling_rate = params->sampling_rate;
if (params->stack_mode == mojom::StackMode::NATIVE_WITH_THREAD_NAMES) {
g_include_thread_names = true;
base::ThreadIdNameManager::GetInstance()->InstallSetNameCallback(
base::BindRepeating(&SetCurrentThreadName));
}
switch (params->stack_mode) {
case mojom::StackMode::PSEUDO:
EnableTraceEventFiltering();
AllocationContextTracker::SetCaptureMode(CaptureMode::PSEUDO_STACK);
break;
case mojom::StackMode::MIXED:
EnableTraceEventFiltering();
AllocationContextTracker::SetCaptureMode(CaptureMode::MIXED_STACK);
break;
case mojom::StackMode::NATIVE_WITH_THREAD_NAMES:
case mojom::StackMode::NATIVE_WITHOUT_THREAD_NAMES:
AllocationContextTracker::SetCaptureMode(CaptureMode::DISABLED);
break;
}
g_send_buffers.Write(new SendBuffer[kNumSendBuffers]);
g_sender_pipe = sender_pipe;
#if BUILDFLAG(USE_ALLOCATOR_SHIM)
// Normal malloc allocator shim.
base::allocator::InsertAllocatorDispatch(&g_memlog_hooks);
#endif
// PartitionAlloc allocator shim.
base::PartitionAllocHooks::SetAllocationHook(&HookPartitionAlloc);
base::PartitionAllocHooks::SetFreeHook(&HookPartitionFree);
// GC (Oilpan) allocator shim.
if (g_hook_gc_alloc && g_hook_gc_free) {
g_hook_gc_alloc(&HookGCAlloc);
g_hook_gc_free(&HookGCFree);
}
if (*g_on_init_allocator_shim_callback_.Pointer()) {
(*g_on_init_allocator_shim_task_runner_.Pointer())
->PostTask(FROM_HERE,
std::move(*g_on_init_allocator_shim_callback_.Pointer()));
}
}
void StopAllocatorShimDangerous() {
// This ShareBuffer array is leaked on purpose to avoid races on Stop.
g_send_buffers.Write(nullptr);
base::PartitionAllocHooks::SetAllocationHook(nullptr);
base::PartitionAllocHooks::SetFreeHook(nullptr);
if (g_hook_gc_alloc && g_hook_gc_free) {
g_hook_gc_alloc(nullptr);
g_hook_gc_free(nullptr);
}
if (g_sender_pipe)
g_sender_pipe->Close();
}
void SerializeFramesFromAllocationContext(FrameSerializer* serializer,
const char** context) {
auto* tracker = AllocationContextTracker::GetInstanceForCurrentThread();
if (!tracker)
return;
AllocationContext allocation_context;
if (!tracker->GetContextSnapshot(&allocation_context))
return;
serializer->AddAllFrames(allocation_context.backtrace);
if (!*context)
*context = allocation_context.type_name;
}
void SerializeFramesFromBacktrace(FrameSerializer* serializer) {
#if BUILDFLAG(CAN_UNWIND_WITH_FRAME_POINTERS)
const void* frames[kMaxStackEntries - 1];
size_t frame_count = base::debug::TraceStackFramePointers(
frames, kMaxStackEntries - 1,
1); // exclude this function from the trace.
#else // BUILDFLAG(CAN_UNWIND_WITH_FRAME_POINTERS)
// Fall-back to capturing the stack with base::debug::StackTrace,
// which is likely slower, but more reliable.
base::debug::StackTrace stack_trace(kMaxStackEntries - 1);
size_t frame_count = 0u;
const void* const* frames = stack_trace.Addresses(&frame_count);
#endif // BUILDFLAG(CAN_UNWIND_WITH_FRAME_POINTERS)
serializer->AddAllInstructionPointers(frame_count, frames);
if (g_include_thread_names) {
const char* thread_name = GetOrSetThreadName();
serializer->AddCString(thread_name);
}
}
void AllocatorShimLogAlloc(AllocatorType type,
void* address,
size_t sz,
const char* context) {
SendBuffer* send_buffers = g_send_buffers.Read();
if (!send_buffers)
return;
// When sampling, we divide allocations into two buckets. For allocations
// larger than g_sampling_rate we just skip the sampling logic entirely, since
// we want to record them with probability 1. Allocations smaller than
// g_sampling_rate we use a poisson process to sample. That gives us a
// computationally cheap mechanism to sample allocations with probability P =
// (size) / g_sampling_rate.
if (g_sample_allocations && LIKELY(sz < g_sampling_rate)) {
ShimState* shim_state = GetShimState();
shim_state->interval_to_next_sample -= sz;
// When |interval_to_next_sample| underflows, we record a sample.
if (LIKELY(shim_state->interval_to_next_sample > 0)) {
return;
}
// Very occasionally, when sampling, we'll want to take more than 1 sample
// from the same object. Ideally, we'd have a "count" or "weight" associated
// with the allocation in question. Since the memlog stream format does not
// support that, just use |sz| as a proxy.
int sz_multiplier = 0;
while (shim_state->interval_to_next_sample <= 0) {
shim_state->interval_to_next_sample +=
GetNextSampleInterval(g_sampling_rate);
++sz_multiplier;
}
sz *= sz_multiplier;
}
if (address) {
constexpr size_t max_message_size = sizeof(AllocPacket) +
kMaxStackEntries * sizeof(uint64_t) +
kMaxContextLen;
static_assert(max_message_size < MemlogSenderPipe::kPipeSize,
"We can't have a message size that exceeds the pipe write "
"buffer size.");
char message[max_message_size];
// TODO(ajwong) check that this is technically valid.
AllocPacket* alloc_packet = reinterpret_cast<AllocPacket*>(message);
uint64_t* stack =
reinterpret_cast<uint64_t*>(&message[sizeof(AllocPacket)]);
FrameSerializer serializer(
stack, address, max_message_size - sizeof(AllocPacket), send_buffers);
CaptureMode capture_mode = AllocationContextTracker::capture_mode();
if (capture_mode == CaptureMode::PSEUDO_STACK ||
capture_mode == CaptureMode::MIXED_STACK) {
SerializeFramesFromAllocationContext(&serializer, &context);
} else {
SerializeFramesFromBacktrace(&serializer);
}
size_t context_len = context ? strnlen(context, kMaxContextLen) : 0;
alloc_packet->op = kAllocPacketType;
alloc_packet->allocator = type;
alloc_packet->address = (uint64_t)address;
alloc_packet->size = sz;
alloc_packet->stack_len = static_cast<uint32_t>(serializer.count());
alloc_packet->context_byte_len = static_cast<uint32_t>(context_len);
char* message_end = message + sizeof(AllocPacket) +
alloc_packet->stack_len * sizeof(uint64_t);
if (context_len > 0) {
memcpy(message_end, context, context_len);
message_end += context_len;
}
DoSend(address, message, message_end - message, send_buffers);
}
}
void AllocatorShimLogFree(void* address) {
SendBuffer* send_buffers = g_send_buffers.Read();
if (!send_buffers)
return;
if (address) {
FreePacket free_packet;
free_packet.op = kFreePacketType;
free_packet.address = (uint64_t)address;
DoSend(address, &free_packet, sizeof(FreePacket), send_buffers);
}
}
void AllocatorShimFlushPipe(uint32_t barrier_id) {
SendBuffer* send_buffers = g_send_buffers.Read();
if (!send_buffers)
return;
for (int i = 0; i < kNumSendBuffers; i++)
send_buffers[i].Flush();
BarrierPacket barrier;
barrier.barrier_id = barrier_id;
MemlogSenderPipe::Result result =
g_sender_pipe->Send(&barrier, sizeof(barrier), kTimeoutMs);
if (result != MemlogSenderPipe::Result::kSuccess) {
StopAllocatorShimDangerous();
// TODO(erikchen): Emit a histogram. https://crbug.com/777546.
}
}
void SetGCHeapAllocationHookFunctions(SetGCAllocHookFunction hook_alloc,
SetGCFreeHookFunction hook_free) {
g_hook_gc_alloc = hook_alloc;
g_hook_gc_free = hook_free;
if (g_sender_pipe) {
// If starting the memlog pipe beat Blink initialization, hook the
// functions now.
g_hook_gc_alloc(&HookGCAlloc);
g_hook_gc_free(&HookGCFree);
}
}
void SetOnInitAllocatorShimCallbackForTesting(
base::OnceClosure callback,
scoped_refptr<base::TaskRunner> task_runner) {
*g_on_init_allocator_shim_callback_.Pointer() = std::move(callback);
*g_on_init_allocator_shim_task_runner_.Pointer() = task_runner;
}
} // namespace profiling