#![cfg(all(feature = "allocator", feature = "allocation-tracking"))]
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use std::sync::atomic::{AtomicI64, Ordering};
use common::mem_registry as registry;
use super::usable::ReservedSize;
static GLOBAL_REQUESTED_BYTES: AtomicI64 = AtomicI64::new(0);
static GLOBAL_RESERVED_BYTES: AtomicI64 = AtomicI64::new(0);
const BATCH_THRESHOLD: i64 = 12 * 1024;
const MAX_DEPTH: u32 = 3;
thread_local! {
static THREAD_STATE: ThreadState = const { ThreadState::new() };
static RECURSION_DEPTH: Cell<u32> = const { Cell::new(0) };
}
struct ThreadState {
local_requested: Cell<i64>,
local_reserved: Cell<i64>,
}
impl ThreadState {
const fn new() -> Self {
Self {
local_requested: Cell::new(0),
local_reserved: Cell::new(0),
}
}
fn flush_to_global(&self) {
let requested = self.local_requested.replace(0);
if requested != 0 {
GLOBAL_REQUESTED_BYTES.fetch_add(requested, Ordering::Relaxed);
}
let reserved = self.local_reserved.replace(0);
if reserved != 0 {
GLOBAL_RESERVED_BYTES.fetch_add(reserved, Ordering::Relaxed);
}
}
}
pub struct TrackAlloc<Alloc = System> {
alloc: Alloc,
}
impl<A> TrackAlloc<A> {
#[inline]
pub const fn new(alloc: A) -> Self {
Self {
alloc,
}
}
}
impl<A: GlobalAlloc> TrackAlloc<A> {
pub fn memory_requested(&self) -> usize {
GLOBAL_REQUESTED_BYTES.load(Ordering::Relaxed).max(0) as usize
}
pub fn memory_reserved(&self) -> usize {
GLOBAL_RESERVED_BYTES.load(Ordering::Relaxed).max(0) as usize
}
pub fn memory_allocated(&self) -> usize {
let heap_memory = self.memory_reserved();
let external_memory = registry::memory_reporters_allocated_total();
heap_memory + external_memory
}
pub fn flush_local_allocations(&self) {
THREAD_STATE.with(|state| {
state.flush_to_global();
});
}
pub fn is_beyond_threshold(&self) -> bool {
self.beyond_threshold_by().is_some()
}
pub fn beyond_threshold_by(&self) -> Option<(usize, usize)> {
match *surrealdb_cnf::MEMORY_THRESHOLD {
0 => None,
threshold => {
let allocated = self.memory_allocated();
(allocated > threshold).then_some((allocated, threshold))
}
}
}
fn add(&self, requested: usize, reserved: usize) {
self.record(requested as i64, reserved as i64);
}
fn sub(&self, requested: usize, reserved: usize) {
self.record(-(requested as i64), -(reserved as i64));
}
fn record(&self, requested: i64, reserved: i64) {
let depth = RECURSION_DEPTH.with(|d| {
let current = d.get();
if current >= MAX_DEPTH {
return MAX_DEPTH;
}
d.set(current + 1);
current
});
if depth >= MAX_DEPTH {
return;
}
THREAD_STATE.with(|state| {
let requested = state.local_requested.get().saturating_add(requested);
let reserved = state.local_reserved.get().saturating_add(reserved);
state.local_requested.set(requested);
state.local_reserved.set(reserved);
if requested.saturating_abs() >= BATCH_THRESHOLD
|| reserved.saturating_abs() >= BATCH_THRESHOLD
{
state.flush_to_global();
}
});
RECURSION_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
}
}
unsafe impl<A: GlobalAlloc + ReservedSize> GlobalAlloc for TrackAlloc<A> {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ret = unsafe { self.alloc.alloc(layout) };
if !ret.is_null() {
self.add(layout.size(), self.alloc.reserved_size(layout));
}
ret
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
let reserved = self.alloc.reserved_size(layout);
unsafe { self.alloc.dealloc(ptr, layout) };
self.sub(layout.size(), reserved);
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
let ret = unsafe { self.alloc.alloc_zeroed(layout) };
if !ret.is_null() {
self.add(layout.size(), self.alloc.reserved_size(layout));
}
ret
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let ret = unsafe { self.alloc.realloc(ptr, layout, new_size) };
if !ret.is_null() {
let new_layout = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) };
self.sub(layout.size(), self.alloc.reserved_size(layout));
self.add(new_size, self.alloc.reserved_size(new_layout));
}
ret
}
}
#[cfg(test)]
mod tests {
use std::alloc::{Layout, alloc, dealloc};
use super::*;
use crate::mem::ALLOC;
const AWKWARD_SIZES: [usize; 6] = [1, 3, 17, 100, 1000, 6000];
const ALIGNMENTS: [usize; 4] = [8, 16, 64, 4096];
fn layouts() -> impl Iterator<Item = Layout> {
AWKWARD_SIZES.into_iter().flat_map(|size| {
ALIGNMENTS
.into_iter()
.filter_map(move |align| Layout::from_size_align(size, align).ok())
})
}
#[test]
fn reserved_size_covers_the_requested_size() {
for layout in layouts() {
let reserved = ALLOC.alloc.reserved_size(layout);
assert!(
reserved >= layout.size(),
"reserved {reserved} bytes for a {}-byte request at align {}",
layout.size(),
layout.align(),
);
}
}
#[test]
fn reserved_size_depends_only_on_the_layout() {
for layout in layouts() {
let first = ALLOC.alloc.reserved_size(layout);
let churn: Vec<*mut u8> = layouts()
.map(|l| {
let ptr = unsafe { alloc(l) };
assert!(!ptr.is_null(), "allocation of {} bytes failed", l.size());
ptr
})
.collect();
for (ptr, l) in churn.into_iter().zip(layouts()) {
unsafe { dealloc(ptr, l) };
}
let second = ALLOC.alloc.reserved_size(layout);
assert_eq!(
first,
second,
"reserved size changed across allocation traffic for {} bytes at align {}",
layout.size(),
layout.align(),
);
}
}
#[test]
fn reserved_total_dominates_requested_total() {
let held: Vec<Vec<u8>> = AWKWARD_SIZES.iter().map(|&s| Vec::with_capacity(s)).collect();
ALLOC.flush_local_allocations();
let requested = ALLOC.memory_requested();
let reserved = ALLOC.memory_reserved();
assert!(
reserved >= requested,
"reserved {reserved} bytes is below requested {requested} bytes"
);
drop(held);
}
}