// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef BASE_DEBUG_ALLOCATION_TRACE_H_
#define BASE_DEBUG_ALLOCATION_TRACE_H_
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
#include <cstdint>
#include "base/allocator/dispatcher/notification_data.h"
#include "base/base_export.h"
#include "base/compiler_specific.h"
#include "base/debug/debugging_buildflags.h"
#include "base/debug/stack_trace.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "build/build_config.h"
namespace base::debug::tracer {
// Number of traces that can be stored. This number must be a power of two to
// allow for fast computation of modulo.
constexpr size_t kMaximumNumberOfMemoryOperationTraces = (1 << 15);
// Number of frames stored for each operation. Probably the lower frames
// represent the memory allocation system. Hence, we store more frames to
// increase chances of having a meaningful trace of the path that caused the
// allocation or free.
constexpr size_t kStackTraceSize = 16;
// The type of an operation stored in the recorder.
enum class OperationType {
// The state of an operation record before calling any of the initialization
// functions.
kNone = 0,
// The record represents an allocation operation.
kAllocation,
// The record represents a free operation.
kFree,
};
using StackTraceContainer = std::array<const void*, kStackTraceSize>;
// The record for a single operation. A record can represent any type of
// operation, allocation or free, but not at the same time.
//
// A record protects itself from concurrent initializations. If a thread B calls
// any of the Initialize*-functions while another thread A is currently
// initializing, B's invocations shall immediately return |false| without
// interfering with thread A.
class BASE_EXPORT OperationRecord {
public:
constexpr OperationRecord() = default;
OperationRecord(const OperationRecord&) = delete;
OperationRecord& operator=(const OperationRecord&) = delete;
// Is the record currently being taken?
bool IsRecording() const;
OperationType GetOperationType() const;
// The address allocated or freed.
const void* GetAddress() const;
// Number of allocated bytes. Returns 0 for free operations.
size_t GetSize() const;
// The stacktrace as taken by the Initialize*-functions.
const StackTraceContainer& GetStackTrace() const;
// Initialize the record with data for another operation. Data from any
// previous operation will be silently overwritten. These functions are
// declared ALWAYS_INLINE to minimize pollution of the recorded stack trace.
//
// Both functions return false in case no record was taken, i.e. if another
// thread is capturing.
ALWAYS_INLINE bool InitializeFree(const void* freed_address) {
return InitializeOperationRecord(freed_address, 0, OperationType::kFree);
}
ALWAYS_INLINE bool InitializeAllocation(const void* allocated_address,
size_t allocated_size) {
return InitializeOperationRecord(allocated_address, allocated_size,
OperationType::kAllocation);
}
private:
// Initialize a record with the given data. Return true if the record was
// initialized successfully, false if no record was taken, i.e. if another
// thread is capturing.
ALWAYS_INLINE bool InitializeOperationRecord(const void* address,
size_t size,
OperationType operation_type);
ALWAYS_INLINE void StoreStackTrace();
// The stack trace taken in one of the Initialize* functions.
StackTraceContainer stack_trace_ = {};
// The number of allocated bytes.
size_t size_ = 0;
// The address that was allocated or freed.
// We use a raw C++ pointer instead of base::raw_ptr for performance
// reasons.
// - In the recorder we only store pointers, we never allocate or free on
// our own.
// - Storing is the hot path. base::raw_ptr::operator== may perform sanity
// checks which do not make sense in our case (otherwise the allocated
// address would have been quirky)
RAW_PTR_EXCLUSION const void* address_ = nullptr;
// The type of the operation that was performed. In the course of making a
// record, this value is reset to |OperationType::kNone| and later set to
// the operation type specific value, so if the process crashes whilst writing
// the record, it's marked as empty. To prevent the compiler from optimizing
// away the initial reset, this value is marked as volatile.
volatile OperationType operation_type_ = OperationType::kNone;
// Is the record currently being taken from another thread? Used to prevent
// concurrent writes to the same record.
//
// The value is mutable since pre C++20 there is no const getter in
// atomic_flag. All ways to get the value involve setting it.
// TODO(crbug.com/42050406): Remove mutable and make IsRecording() use
// atomic_flag::test();
mutable std::atomic_flag is_recording_ = ATOMIC_FLAG_INIT;
};
ALWAYS_INLINE bool OperationRecord::InitializeOperationRecord(
const void* address,
size_t size,
OperationType operation_type) {
if (is_recording_.test_and_set(std::memory_order_acquire)) {
return false;
}
operation_type_ = operation_type;
StoreStackTrace();
address_ = address;
size_ = size;
is_recording_.clear(std::memory_order_release);
return true;
}
ALWAYS_INLINE void OperationRecord::StoreStackTrace() {
stack_trace_.fill(nullptr);
#if BUILDFLAG(CAN_UNWIND_WITH_FRAME_POINTERS)
// Currently we limit ourselves to use TraceStackFramePointers. We know that
// TraceStackFramePointers has an acceptable performance impact on Android.
base::debug::TraceStackFramePointers(stack_trace_, 0);
#elif BUILDFLAG(IS_LINUX)
// Use base::debug::CollectStackTrace as an alternative for tests on Linux. We
// still have a check in /base/debug/debug.gni to prevent that
// AllocationStackTraceRecorder is enabled accidentally on Linux.
base::debug::CollectStackTrace(stack_trace_);
#else
#error "No supported stack tracer found."
#endif
}
struct BASE_EXPORT AllocationTraceRecorderStatistics {
#if BUILDFLAG(ENABLE_ALLOCATION_TRACE_RECORDER_FULL_REPORTING)
AllocationTraceRecorderStatistics(size_t total_number_of_allocations,
size_t total_number_of_collisions);
#else
AllocationTraceRecorderStatistics(size_t total_number_of_allocations);
#endif
// The total number of allocations that have been recorded.
size_t total_number_of_allocations;
#if BUILDFLAG(ENABLE_ALLOCATION_TRACE_RECORDER_FULL_REPORTING)
// The total number of collisions that have been encountered. A collision
// happens when two threads concurrently try to record using the same slot.
size_t total_number_of_collisions;
#endif
};
// The recorder which holds entries for past memory operations.
//
// The memory image of the recorder will be copied into the crash-handler.
// Therefore, it must not hold any references to external data which are vital
// for proper functioning.
//
// It is important that the recorder itself does not allocate to prevent
// recursive calls and save as much runtime overhead as possible.
//
// Therefore, records are stored in a preallocated buffer with a compile time
// constant maximum size, see |kMaximumNumberOfMemoryOperationTraces|. Once all
// records have been used, old records will be overwritten (fifo-style).
//
// The recorder works in an multithreaded environment without external locking.
// Concurrent writes are prevented by two means:
// 1 - We atomically increment and calculate the effective index of the record
// to be written.
// 2 - If this entry is still being used (the recording thread didn't finish
// yet), we go back to step 1
// Currently we do not enforce separate cache lines for each entry, which means
// false sharing can occur. On the other hand, with 64 byte cachelines a clean
// separation would introduce some 3*64 - sizeof(OperationRecord) = 40 bytes of
// padding per entry.
//
// Note: As a process might be terminated for whatever reason while stack
// traces are being written, the recorded data may contain some garbage.
//
// TODO(crbug.com/40258550): Evaluate the impact of the shared cache
// lines between entries.
class BASE_EXPORT AllocationTraceRecorder {
public:
constexpr AllocationTraceRecorder() = default;
AllocationTraceRecorder(const AllocationTraceRecorder&) = delete;
AllocationTraceRecorder& operator=(const AllocationTraceRecorder&) = delete;
// The allocation event observer interface. See the dispatcher for further
// details. The functions are marked NO_INLINE. All other functions called but
// the one taking the call stack are marked ALWAYS_INLINE. This way we ensure
// the number of frames recorded from these functions is fixed.
inline void OnAllocation(
const base::allocator::dispatcher::AllocationNotificationData&
allocation_data);
// Handle all free events.
inline void OnFree(
const base::allocator::dispatcher::FreeNotificationData& free_data);
// Access functions to retrieve the current content of the recorder.
// Note: Since the recorder is usually updated upon each allocation or free,
// it is important to prevent updates if you want to read the entries at any
// point.
// Get the current number of entries stored in the recorder. When the
// recorder has reached its maximum capacity, it always returns
// |GetMaximumNumberOfTraces()|.
size_t size() const;
// Access the record of an operation by index. Oldest operation is always
// accessible at index 0, latest operation at |size()-1|.
// Note: Since a process might have crashed while a trace is being written,
// especially the last records might be corrupted.
const OperationRecord& operator[](size_t idx) const;
constexpr size_t GetMaximumNumberOfTraces() const {
return kMaximumNumberOfMemoryOperationTraces;
}
AllocationTraceRecorderStatistics GetRecorderStatistics() const;
private:
// Handle all allocation events.
NOINLINE void OnAllocation(const void* allocated_address,
size_t allocated_size);
// Handle all free events.
NOINLINE void OnFree(const void* freed_address);
ALWAYS_INLINE size_t GetNextIndex();
ALWAYS_INLINE static constexpr size_t WrapIdxIfNeeded(size_t idx);
// The actual container.
std::array<OperationRecord, kMaximumNumberOfMemoryOperationTraces>
alloc_trace_buffer_ = {};
// The total number of records that have been taken so far. Note that this
// might be greater than |kMaximumNumberOfMemoryOperationTraces| since we
// overwrite oldest items.
std::atomic<size_t> total_number_of_records_ = 0;
#if BUILDFLAG(ENABLE_ALLOCATION_TRACE_RECORDER_FULL_REPORTING)
std::atomic<size_t> total_number_of_collisions_ = 0;
#endif
};
inline void AllocationTraceRecorder::OnAllocation(
const base::allocator::dispatcher::AllocationNotificationData&
allocation_data) {
OnAllocation(allocation_data.address(), allocation_data.size());
}
// Handle all free events.
inline void AllocationTraceRecorder::OnFree(
const base::allocator::dispatcher::FreeNotificationData& free_data) {
OnFree(free_data.address());
}
ALWAYS_INLINE constexpr size_t AllocationTraceRecorder::WrapIdxIfNeeded(
size_t idx) {
// Wrapping around counter, e.g. for BUFFER_SIZE = 256, the counter will
// wrap around when reaching 256. To enable the compiler to emit more
// optimized code we assert |kMaximumNumberOfMemoryOperationTraces| is a power
// of two .
static_assert(
std::has_single_bit(kMaximumNumberOfMemoryOperationTraces),
"kMaximumNumberOfMemoryOperationTraces should be a power of 2 to "
"allow for fast modulo operation.");
return idx % kMaximumNumberOfMemoryOperationTraces;
}
ALWAYS_INLINE size_t AllocationTraceRecorder::GetNextIndex() {
const auto raw_idx =
total_number_of_records_.fetch_add(1, std::memory_order_relaxed);
return WrapIdxIfNeeded(raw_idx);
}
} // namespace base::debug::tracer
#endif // BASE_DEBUG_ALLOCATION_TRACE_H_