chromium/base/debug/allocation_trace.h

// 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_