use crate::collector::{CollectorAvailability, CollectorCapability, CollectorId};
use std::alloc::{GlobalAlloc, Layout, System};
pub const STAGE_SLOTS: usize = crate::runtime::STAGE_COUNT + 1;
pub const ROOT_SLOT: usize = crate::runtime::STAGE_COUNT;
#[cfg(feature = "allocation-tracking")]
mod tracked {
use super::*;
use std::cell::{Cell, UnsafeCell};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
const MAX_STAGE_DEPTH: usize = 64;
const HEADER_BYTES: usize = 16;
const HEADER_MAGIC: u8 = 0xA5;
pub(super) static INSTALLED: AtomicBool = AtomicBool::new(false);
static ACTIVE_ALLOC_SESSIONS: AtomicUsize = AtomicUsize::new(0);
static ALLOC_SESSION_OVERLAP: AtomicBool = AtomicBool::new(false);
static ALLOCATIONS: AtomicU64 = AtomicU64::new(0);
static DEALLOCATIONS: AtomicU64 = AtomicU64::new(0);
static ALLOCATION_BYTES: AtomicU64 = AtomicU64::new(0);
static DEALLOCATION_BYTES: AtomicU64 = AtomicU64::new(0);
static LIVE_BYTES: AtomicU64 = AtomicU64::new(0);
static PEAK_LIVE_BYTES: AtomicU64 = AtomicU64::new(0);
static SLOT_ALLOCATIONS: [AtomicU64; STAGE_SLOTS] = [const { AtomicU64::new(0) }; STAGE_SLOTS];
static SLOT_ALLOCATION_BYTES: [AtomicU64; STAGE_SLOTS] =
[const { AtomicU64::new(0) }; STAGE_SLOTS];
static SLOT_DEALLOCATION_BYTES: [AtomicU64; STAGE_SLOTS] =
[const { AtomicU64::new(0) }; STAGE_SLOTS];
static SLOT_LIVE_BYTES: [AtomicU64; STAGE_SLOTS] = [const { AtomicU64::new(0) }; STAGE_SLOTS];
static SLOT_PEAK_LIVE_BYTES: [AtomicU64; STAGE_SLOTS] =
[const { AtomicU64::new(0) }; STAGE_SLOTS];
thread_local! {
static STAGE_STACK_DEPTH: Cell<u16> = const { Cell::new(0) };
static STAGE_STACK: UnsafeCell<[u8; MAX_STAGE_DEPTH]> =
const { UnsafeCell::new([0; MAX_STAGE_DEPTH]) };
}
pub(crate) fn stage_context_push(stage: crate::Stage) {
STAGE_STACK_DEPTH.with(|depth| {
let current = depth.get();
if (current as usize) < MAX_STAGE_DEPTH {
STAGE_STACK.with(|stack| {
unsafe { (*stack.get())[current as usize] = stage.index() as u8 };
});
}
depth.set(current.saturating_add(1));
});
}
pub(crate) fn stage_context_pop() {
STAGE_STACK_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
}
fn current_slot() -> usize {
STAGE_STACK_DEPTH.with(|depth| {
let current = depth.get();
if current == 0 || current as usize > MAX_STAGE_DEPTH {
return ROOT_SLOT;
}
STAGE_STACK.with(|stack| {
usize::from(unsafe { (*stack.get())[current as usize - 1] })
})
})
}
#[inline]
fn record_alloc(slot: usize, bytes: u64) {
INSTALLED.store(true, Ordering::Relaxed);
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
ALLOCATION_BYTES.fetch_add(bytes, Ordering::Relaxed);
let live = LIVE_BYTES.fetch_add(bytes, Ordering::Relaxed) + bytes;
PEAK_LIVE_BYTES.fetch_max(live, Ordering::Relaxed);
SLOT_ALLOCATIONS[slot].fetch_add(1, Ordering::Relaxed);
SLOT_ALLOCATION_BYTES[slot].fetch_add(bytes, Ordering::Relaxed);
let slot_live = SLOT_LIVE_BYTES[slot].fetch_add(bytes, Ordering::Relaxed) + bytes;
SLOT_PEAK_LIVE_BYTES[slot].fetch_max(slot_live, Ordering::Relaxed);
}
#[inline]
fn record_dealloc(slot: usize, bytes: u64) {
if slot >= STAGE_SLOTS {
return;
}
DEALLOCATIONS.fetch_add(1, Ordering::Relaxed);
DEALLOCATION_BYTES.fetch_add(bytes, Ordering::Relaxed);
saturating_fetch_sub(&LIVE_BYTES, bytes);
SLOT_DEALLOCATION_BYTES[slot].fetch_add(bytes, Ordering::Relaxed);
saturating_fetch_sub(&SLOT_LIVE_BYTES[slot], bytes);
}
#[inline]
fn saturating_fetch_sub(cell: &AtomicU64, bytes: u64) {
let mut current = cell.load(Ordering::Relaxed);
loop {
let next = current.saturating_sub(bytes);
match cell.compare_exchange_weak(current, next, Ordering::Relaxed, Ordering::Relaxed) {
Ok(_) => break,
Err(observed) => current = observed,
}
}
}
pub(super) fn snapshot_totals() -> (u64, u64, u64, u64, u64, u64) {
(
ALLOCATIONS.load(Ordering::Relaxed),
DEALLOCATIONS.load(Ordering::Relaxed),
ALLOCATION_BYTES.load(Ordering::Relaxed),
DEALLOCATION_BYTES.load(Ordering::Relaxed),
LIVE_BYTES.load(Ordering::Relaxed),
PEAK_LIVE_BYTES.load(Ordering::Relaxed),
)
}
pub(super) fn snapshot_slot(slot: usize) -> super::AllocationSlotV2 {
let allocated = SLOT_ALLOCATION_BYTES[slot].load(Ordering::Relaxed);
let deallocated = SLOT_DEALLOCATION_BYTES[slot].load(Ordering::Relaxed);
super::AllocationSlotV2 {
allocations: SLOT_ALLOCATIONS[slot].load(Ordering::Relaxed),
allocated_bytes: allocated,
live_bytes: allocated.saturating_sub(deallocated),
peak_live_bytes: SLOT_PEAK_LIVE_BYTES[slot].load(Ordering::Relaxed),
}
}
pub(super) fn reset_peaks() {
PEAK_LIVE_BYTES.store(LIVE_BYTES.load(Ordering::Relaxed), Ordering::Relaxed);
for slot in 0..STAGE_SLOTS {
SLOT_PEAK_LIVE_BYTES[slot].store(
SLOT_LIVE_BYTES[slot].load(Ordering::Relaxed),
Ordering::Relaxed,
);
}
}
pub(super) fn enter_session() -> (bool, bool) {
let prev = ACTIVE_ALLOC_SESSIONS.fetch_add(1, Ordering::AcqRel);
if prev == 0 {
ALLOC_SESSION_OVERLAP.store(false, Ordering::Release);
reset_peaks();
(true, false)
} else {
ALLOC_SESSION_OVERLAP.store(true, Ordering::Release);
(true, true)
}
}
pub(super) fn leave_session() {
ACTIVE_ALLOC_SESSIONS.fetch_sub(1, Ordering::AcqRel);
}
pub(super) fn session_evidence_reliable(joined_overlapped: bool) -> bool {
if joined_overlapped {
return false;
}
if ALLOC_SESSION_OVERLAP.load(Ordering::Acquire) {
return false;
}
ACTIVE_ALLOC_SESSIONS.load(Ordering::Acquire) == 1
}
#[repr(C)]
struct AllocationHeader {
stage: u8,
magic: u8,
reserved: [u8; 6],
bytes: u64,
}
const _: () = assert!(std::mem::size_of::<AllocationHeader>() == HEADER_BYTES);
pub(super) unsafe fn tracked_alloc(layout: Layout) -> *mut u8 {
let offset = layout.align().max(HEADER_BYTES);
let Some(total) = layout.size().checked_add(offset) else {
return std::ptr::null_mut();
};
let Ok(real) = Layout::from_size_align(total, offset) else {
return std::ptr::null_mut();
};
let base = unsafe { System.alloc(real) };
if base.is_null() {
return base;
}
let slot = current_slot();
let header = AllocationHeader {
stage: slot as u8,
magic: HEADER_MAGIC,
reserved: [0; 6],
bytes: layout.size() as u64,
};
unsafe { base.cast::<AllocationHeader>().write(header) };
record_alloc(slot, layout.size() as u64);
unsafe { base.add(offset) }
}
pub(super) unsafe fn tracked_dealloc(ptr: *mut u8, layout: Layout) {
let offset = layout.align().max(HEADER_BYTES);
let base = unsafe { ptr.sub(offset) };
let header = unsafe { base.cast::<AllocationHeader>().read() };
let stage = usize::from(header.stage);
let header_ok = header.magic == HEADER_MAGIC
&& header.bytes == layout.size() as u64
&& stage < STAGE_SLOTS;
let user_bytes = if header_ok {
record_dealloc(stage, header.bytes);
header.bytes as usize
} else {
layout.size()
};
let real = Layout::from_size_align(user_bytes.saturating_add(offset), offset)
.unwrap_or_else(|_| {
unsafe { Layout::from_size_align_unchecked(layout.size() + offset, offset) }
});
unsafe { System.dealloc(base, real) };
}
}
#[cfg(not(feature = "allocation-tracking"))]
mod untracked {
pub(super) fn snapshot_totals() -> (u64, u64, u64, u64, u64, u64) {
(0, 0, 0, 0, 0, 0)
}
pub(super) fn snapshot_slot(_slot: usize) -> super::AllocationSlotV2 {
super::AllocationSlotV2 {
allocations: 0,
allocated_bytes: 0,
live_bytes: 0,
peak_live_bytes: 0,
}
}
pub(super) fn reset_peaks() {}
}
#[cfg(feature = "allocation-tracking")]
use tracked as backend;
#[cfg(not(feature = "allocation-tracking"))]
use untracked as backend;
#[cfg(feature = "allocation-tracking")]
pub(crate) use backend::{stage_context_pop, stage_context_push};
#[cfg(not(feature = "allocation-tracking"))]
#[inline(always)]
pub(crate) fn stage_context_push(_stage: crate::Stage) {}
#[cfg(not(feature = "allocation-tracking"))]
#[inline(always)]
pub(crate) fn stage_context_pop() {}
pub struct TrackingAllocator;
impl TrackingAllocator {
pub const fn new() -> Self {
Self
}
}
impl Default for TrackingAllocator {
fn default() -> Self {
Self::new()
}
}
unsafe impl GlobalAlloc for TrackingAllocator {
#[cfg(feature = "allocation-tracking")]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe { tracked::tracked_alloc(layout) }
}
#[cfg(feature = "allocation-tracking")]
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { tracked::tracked_dealloc(ptr, layout) }
}
#[cfg(not(feature = "allocation-tracking"))]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe { System.alloc(layout) }
}
#[cfg(not(feature = "allocation-tracking"))]
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
pub fn allocation_tracking_installed() -> bool {
#[cfg(feature = "allocation-tracking")]
{
tracked::INSTALLED.load(std::sync::atomic::Ordering::Relaxed)
}
#[cfg(not(feature = "allocation-tracking"))]
{
false
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct AllocationSlotV2 {
pub allocations: u64,
pub allocated_bytes: u64,
pub live_bytes: u64,
pub peak_live_bytes: u64,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AllocationSnapshotV2 {
pub allocations: u64,
pub deallocations: u64,
pub allocated_bytes: u64,
pub deallocated_bytes: u64,
pub live_bytes: u64,
pub peak_live_bytes: u64,
pub slots: [AllocationSlotV2; STAGE_SLOTS],
}
impl AllocationSnapshotV2 {
pub fn slot(&self, stage: crate::Stage) -> &AllocationSlotV2 {
&self.slots[stage.index()]
}
pub fn root(&self) -> &AllocationSlotV2 {
&self.slots[ROOT_SLOT]
}
pub fn live_delta_since(&self, start: &Self) -> u64 {
self.live_bytes.saturating_sub(start.live_bytes)
}
}
pub fn allocation_snapshot() -> AllocationSnapshotV2 {
let (allocations, deallocations, allocated_bytes, deallocated_bytes, live_bytes, peak) =
backend::snapshot_totals();
AllocationSnapshotV2 {
allocations,
deallocations,
allocated_bytes,
deallocated_bytes,
live_bytes,
peak_live_bytes: peak,
slots: std::array::from_fn(backend::snapshot_slot),
}
}
pub fn reset_allocation_peaks() {
backend::reset_peaks();
}
pub(crate) struct AllocationSessionToken {
active: bool,
overlapped: bool,
}
impl AllocationSessionToken {
pub(crate) const fn inactive() -> Self {
Self {
active: false,
overlapped: false,
}
}
pub(crate) fn evidence_is_reliable(&self) -> bool {
if !self.active {
return true;
}
#[cfg(feature = "allocation-tracking")]
{
backend::session_evidence_reliable(self.overlapped)
}
#[cfg(not(feature = "allocation-tracking"))]
{
true
}
}
}
impl Drop for AllocationSessionToken {
fn drop(&mut self) {
if !self.active {
return;
}
self.active = false;
#[cfg(feature = "allocation-tracking")]
{
backend::leave_session();
}
}
}
pub(crate) fn enter_allocation_session() -> AllocationSessionToken {
#[cfg(feature = "allocation-tracking")]
{
let (active, overlapped) = backend::enter_session();
AllocationSessionToken { active, overlapped }
}
#[cfg(not(feature = "allocation-tracking"))]
{
AllocationSessionToken::inactive()
}
}
pub(crate) fn allocation_capability() -> CollectorCapability {
#[cfg(not(feature = "allocation-tracking"))]
{
CollectorCapability::unavailable(
CollectorId::AllocationTracking,
CollectorAvailability::Disabled,
"enable the keyhog-profile allocation-tracking feature",
)
}
#[cfg(feature = "allocation-tracking")]
{
if allocation_tracking_installed() {
CollectorCapability::available(CollectorId::AllocationTracking)
} else {
CollectorCapability::unavailable(
CollectorId::AllocationTracking,
CollectorAvailability::Unavailable,
"install keyhog_profile::TrackingAllocator as the global allocator to count allocations",
)
}
}
}