current commit
260406.4 · quic: use arena allocation for packets
#pragma once

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include <memory_tracker-inl.h>
#include <util.h>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <new>
#include <utility>
#include <vector>

namespace node::quic {

// ArenaPool<T> is a block-based arena allocator for fixed-size objects.
//
// Objects are allocated from contiguous memory blocks ("slabs"), reducing
// heap fragmentation and allocation overhead. Each block contains a fixed
// number of equally-sized slots. Free slots form an intrusive singly-linked
// list for O(1) acquire/release.
//
// Each slot can optionally hold extra bytes after T (e.g., a trailing data
// buffer). The extra_bytes parameter is set at pool construction time and
// applies uniformly to all slots. All slots are allocated at the same size
// regardless of how much of the extra space is actually used, to prevent
// fragmentation.
//
// ArenaPool<T>::Ptr is a move-only RAII smart pointer that releases slots
// back to the pool on destruction. Ptr::release() detaches ownership for
// handoff to external systems (e.g., libuv), after which the caller must
// eventually call ArenaPool<T>::Release(raw_ptr) to return the slot.
//
// The pool supports lazy GC: when the ratio of free slots to total slots
// exceeds a threshold, fully-unused blocks are reclaimed. At least one
// block is always retained.
template <typename T>
class ArenaPool final : public MemoryRetainer {
 public:
  class Ptr;

  // extra_bytes: additional memory available after T in each slot.
  //              All slots are sized identically regardless of how
  //              much extra space is actually used.
  // slots_per_block: number of T slots per allocation block.
  explicit ArenaPool(size_t extra_bytes = 0,
                     size_t slots_per_block = kDefaultSlotsPerBlock);
  ~ArenaPool();

  ArenaPool(const ArenaPool&) = delete;
  ArenaPool& operator=(const ArenaPool&) = delete;
  ArenaPool(ArenaPool&&) = delete;
  ArenaPool& operator=(ArenaPool&&) = delete;

  // Construct T in an acquired slot with forwarded args.
  // Returns an empty Ptr only on allocation failure.
  template <typename... Args>
  [[nodiscard]] Ptr Acquire(Args&&... args);

  // Construct T with (extra_data_ptr, extra_bytes, ...args).
  // Use this for types whose constructor accepts a trailing data
  // buffer as its first two parameters.
  template <typename... Args>
  [[nodiscard]] Ptr AcquireExtra(Args&&... args);

  // Release a raw T* previously detached via Ptr::release().
  // Calls ~T() and returns the slot to the pool's free list.
  // Recovers the pool instance from T*'s slot metadata.
  static void Release(T* obj);

  // Attempt to reclaim fully-unused blocks. Called automatically
  // from Release/ReleaseSlot when the pool is over-provisioned.
  void MaybeGC();

  // Free all unused blocks immediately, keeping at least one.
  void Flush();

  size_t extra_bytes() const { return extra_bytes_; }
  size_t slot_size() const { return slot_size_; }
  size_t total_slots() const { return total_slots_; }
  size_t in_use_count() const { return in_use_count_; }
  size_t block_count() const { return blocks_.size(); }

  void MemoryInfo(MemoryTracker* tracker) const override;
  SET_MEMORY_INFO_NAME(ArenaPool)
  SET_SELF_SIZE(ArenaPool)

  static constexpr size_t kDefaultSlotsPerBlock = 128;

 private:
  // -------------------------------------------------------------------
  // Slot layout in memory:
  //
  //   [0, kObjectOffset)                        SlotHeader
  //   [kObjectOffset, kObjectOffset+sizeof(T))   T object
  //   [kObjectOffset+sizeof(T), slot_size_)      extra bytes + padding
  //
  // SlotHeader is only used when the slot is on the free list.
  // When the slot is acquired, the T object occupies its storage
  // and the header fields are not accessed.
  // -------------------------------------------------------------------

  struct Block;

  struct SlotHeader {
    SlotHeader* next_free;
    Block* block;
  };

  struct Block {
    std::unique_ptr<char[]> memory;
    ArenaPool* pool;
    size_t capacity;
    size_t in_use_count;
  };

  static constexpr size_t kObjectOffset =
      (sizeof(SlotHeader) + alignof(T) - 1) & ~(alignof(T) - 1);

  // Slot ↔ T* conversion using the compile-time offset.
  static T* ObjectFromSlot(SlotHeader* slot) {
    return reinterpret_cast<T*>(reinterpret_cast<char*>(slot) + kObjectOffset);
  }

  static SlotHeader* SlotFromObject(T* obj) {
    return reinterpret_cast<SlotHeader*>(reinterpret_cast<char*>(obj) -
                                         kObjectOffset);
  }

  static uint8_t* ExtraDataFromSlot(SlotHeader* slot) {
    return reinterpret_cast<uint8_t*>(reinterpret_cast<char*>(slot) +
                                      kObjectOffset + sizeof(T));
  }

  SlotHeader* SlotAt(Block* block, size_t index) {
    return reinterpret_cast<SlotHeader*>(block->memory.get() +
                                         index * slot_size_);
  }

  SlotHeader* AcquireSlot();
  void ReleaseSlot(SlotHeader* slot);
  bool Grow();
  void FreeEmptyBlocks();
  void RemoveBlockFromFreeList(Block* block);

  const size_t extra_bytes_;
  const size_t slots_per_block_;
  const size_t slot_size_;

  SlotHeader* free_list_ = nullptr;
  std::vector<std::unique_ptr<Block>> blocks_;
  size_t total_slots_ = 0;
  size_t in_use_count_ = 0;
};

// =====================================================================
// ArenaPool<T>::Ptr — Move-only RAII smart pointer
// =====================================================================

template <typename T>
class ArenaPool<T>::Ptr final {
 public:
  Ptr() = default;
  ~Ptr() { reset(); }

  Ptr(Ptr&& other) noexcept : slot_(other.slot_) { other.slot_ = nullptr; }

  Ptr& operator=(Ptr&& other) noexcept {
    if (this != &other) {
      reset();
      slot_ = other.slot_;
      other.slot_ = nullptr;
    }
    return *this;
  }

  Ptr(const Ptr&) = delete;
  Ptr& operator=(const Ptr&) = delete;

  T* get() const { return slot_ ? ObjectFromSlot(slot_) : nullptr; }
  T* operator->() const {
    DCHECK(slot_);
    return ObjectFromSlot(slot_);
  }
  T& operator*() const {
    DCHECK(slot_);
    return *ObjectFromSlot(slot_);
  }
  explicit operator bool() const { return slot_ != nullptr; }

  // Access the extra data region after T in the slot.
  uint8_t* extra_data() const {
    return slot_ ? ExtraDataFromSlot(slot_) : nullptr;
  }
  size_t extra_bytes() const {
    return slot_ ? slot_->block->pool->extra_bytes_ : 0;
  }

  // Detach ownership. The caller takes responsibility for eventually
  // calling ArenaPool<T>::Release(ptr) to destruct T and return
  // the slot to the pool.
  [[nodiscard]] T* release() noexcept {
    if (!slot_) return nullptr;
    T* obj = ObjectFromSlot(slot_);
    slot_ = nullptr;
    return obj;
  }

  // Destruct T and return the slot to the pool. Ptr becomes empty.
  void reset() {
    if (slot_) {
      ObjectFromSlot(slot_)->~T();
      slot_->block->pool->ReleaseSlot(slot_);
      slot_ = nullptr;
    }
  }

 private:
  friend class ArenaPool<T>;
  explicit Ptr(SlotHeader* slot) : slot_(slot) {}
  SlotHeader* slot_ = nullptr;
};

// =====================================================================
// ArenaPool<T> implementation
// =====================================================================

template <typename T>
ArenaPool<T>::ArenaPool(size_t extra_bytes, size_t slots_per_block)
    : extra_bytes_(extra_bytes),
      slots_per_block_(slots_per_block),
      slot_size_(((kObjectOffset + sizeof(T) + extra_bytes) +
                  alignof(std::max_align_t) - 1) &
                 ~(alignof(std::max_align_t) - 1)) {
  DCHECK_GT(slots_per_block, 0);
}

template <typename T>
ArenaPool<T>::~ArenaPool() {
  DCHECK_EQ(in_use_count_, 0);
}

template <typename T>
template <typename... Args>
typename ArenaPool<T>::Ptr ArenaPool<T>::Acquire(Args&&... args) {
  SlotHeader* slot = AcquireSlot();
  if (!slot) return Ptr();
  T* obj = new (ObjectFromSlot(slot)) T(std::forward<Args>(args)...);
  CHECK_EQ(obj, ObjectFromSlot(slot));
  return Ptr(slot);
}

template <typename T>
template <typename... Args>
typename ArenaPool<T>::Ptr ArenaPool<T>::AcquireExtra(Args&&... args) {
  SlotHeader* slot = AcquireSlot();
  if (!slot) return Ptr();
  T* obj = new (ObjectFromSlot(slot))
      T(ExtraDataFromSlot(slot), extra_bytes_, std::forward<Args>(args)...);
  CHECK_EQ(obj, ObjectFromSlot(slot));
  return Ptr(slot);
}

template <typename T>
void ArenaPool<T>::Release(T* obj) {
  DCHECK_NOT_NULL(obj);
  SlotHeader* slot = SlotFromObject(obj);
  DCHECK_NOT_NULL(slot->block);
  DCHECK_NOT_NULL(slot->block->pool);
  obj->~T();
  slot->block->pool->ReleaseSlot(slot);
}

template <typename T>
typename ArenaPool<T>::SlotHeader* ArenaPool<T>::AcquireSlot() {
  if (!free_list_) {
    if (!Grow()) return nullptr;
  }
  DCHECK_NOT_NULL(free_list_);
  SlotHeader* slot = free_list_;
  free_list_ = slot->next_free;
  slot->next_free = nullptr;
  slot->block->in_use_count++;
  in_use_count_++;
  return slot;
}

template <typename T>
void ArenaPool<T>::ReleaseSlot(SlotHeader* slot) {
  DCHECK_NOT_NULL(slot);
  DCHECK_NOT_NULL(slot->block);
  DCHECK_GT(slot->block->in_use_count, 0);
  DCHECK_GT(in_use_count_, 0);

  slot->block->in_use_count--;
  in_use_count_--;
  slot->next_free = free_list_;
  free_list_ = slot;

  MaybeGC();
}

template <typename T>
bool ArenaPool<T>::Grow() {
  auto block = std::make_unique<Block>();
  block->pool = this;
  block->capacity = slots_per_block_;
  block->in_use_count = 0;
  block->memory.reset(new char[slots_per_block_ * slot_size_]());

  // Initialize slot headers and chain onto free list.
  for (size_t i = 0; i < slots_per_block_; i++) {
    SlotHeader* slot = SlotAt(block.get(), i);
    slot->block = block.get();
    slot->next_free = free_list_;
    free_list_ = slot;
  }

  total_slots_ += slots_per_block_;
  blocks_.push_back(std::move(block));
  return true;
}

template <typename T>
void ArenaPool<T>::MaybeGC() {
  // Only GC when we have excess capacity: more than one block and
  // less than half the slots are in use.
  if (blocks_.size() <= 1 || in_use_count_ >= total_slots_ / 2) return;
  FreeEmptyBlocks();
}

template <typename T>
void ArenaPool<T>::Flush() {
  FreeEmptyBlocks();
}

template <typename T>
void ArenaPool<T>::FreeEmptyBlocks() {
  for (auto it = blocks_.begin(); it != blocks_.end();) {
    if ((*it)->in_use_count == 0 && blocks_.size() > 1) {
      RemoveBlockFromFreeList(it->get());
      total_slots_ -= (*it)->capacity;
      it = blocks_.erase(it);
    } else {
      ++it;
    }
  }
}

template <typename T>
void ArenaPool<T>::RemoveBlockFromFreeList(Block* block) {
  char* block_start = block->memory.get();
  char* block_end = block_start + block->capacity * slot_size_;

  SlotHeader** pp = &free_list_;
  while (*pp) {
    char* addr = reinterpret_cast<char*>(*pp);
    if (addr >= block_start && addr < block_end) {
      *pp = (*pp)->next_free;
    } else {
      pp = &(*pp)->next_free;
    }
  }
}

template <typename T>
void ArenaPool<T>::MemoryInfo(MemoryTracker* tracker) const {
  tracker->TrackFieldWithSize("blocks", total_slots_ * slot_size_);
}

}  // namespace node::quic

#endif  // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS