mirror of
https://github.com/klzgrad/naiveproxy.git
synced 2024-11-29 00:36:10 +03:00
306 lines
12 KiB
C
306 lines
12 KiB
C
|
// 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_
|