use std::cell::Cell;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, OnceLock};
use crate::{MemoryCharge, MemoryClass, MemoryLimitExceeded, governor};
#[derive(Debug)]
pub enum SafepointError {
StackOverflow(String),
Cancelled,
}
pub struct Hooks {
pub safepoint: fn() -> Result<bool, SafepointError>,
pub yield_now: fn() -> Result<(), SafepointError>,
pub reclaim_stacks: fn(),
pub enter_native: fn(&Arc<str>, &NativeDiagnosticId) -> usize,
pub enter_gc_pause: fn() -> usize,
pub exit_native: fn(usize),
}
static HOOKS: OnceLock<Hooks> = OnceLock::new();
static LIVE: AtomicUsize = AtomicUsize::new(0);
static PEAK: AtomicUsize = AtomicUsize::new(0);
static POOLED: AtomicUsize = AtomicUsize::new(0);
static RESERVED: AtomicUsize = AtomicUsize::new(0);
pub fn install(hooks: Hooks) {
let _ = HOOKS.set(hooks);
}
pub fn safepoint() -> Result<(), SafepointError> {
safepoint_with(|yield_now| yield_now())
}
pub fn safepoint_with(
around_yield: impl FnOnce(fn() -> Result<(), SafepointError>) -> Result<(), SafepointError>,
) -> Result<(), SafepointError> {
if let Some(hooks) = HOOKS.get()
&& (hooks.safepoint)()?
{
around_yield(hooks.yield_now)
} else {
Ok(())
}
}
pub(crate) fn reclaim_idle_stacks() {
if let Some(hooks) = HOOKS.get() {
(hooks.reclaim_stacks)();
}
}
#[inline(always)]
pub fn stack_pointer() -> usize {
#[cfg(any(
target_arch = "x86_64",
target_arch = "x86",
target_arch = "aarch64",
target_arch = "arm",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "powerpc64",
))]
{
let pointer: usize;
unsafe {
#[cfg(target_arch = "x86_64")]
std::arch::asm!("mov {}, rsp", out(reg) pointer, options(nomem, nostack, preserves_flags));
#[cfg(target_arch = "x86")]
std::arch::asm!("mov {}, esp", out(reg) pointer, options(nomem, nostack, preserves_flags));
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
std::arch::asm!("mov {}, sp", out(reg) pointer, options(nomem, nostack, preserves_flags));
#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))]
std::arch::asm!("mv {}, sp", out(reg) pointer, options(nomem, nostack, preserves_flags));
#[cfg(target_arch = "loongarch64")]
std::arch::asm!("move {}, $sp", out(reg) pointer, options(nomem, nostack, preserves_flags));
#[cfg(target_arch = "powerpc64")]
std::arch::asm!("mr {}, 1", out(reg) pointer, options(nomem, nostack, preserves_flags));
}
pointer
}
#[cfg(not(any(
target_arch = "x86_64",
target_arch = "x86",
target_arch = "aarch64",
target_arch = "arm",
target_arch = "riscv32",
target_arch = "riscv64",
target_arch = "loongarch64",
target_arch = "powerpc64",
)))]
{
let marker = 0u8;
std::hint::black_box(&marker) as *const u8 as usize
}
}
pub const NATIVE_STACK_MARGIN: usize = 128 * 1024;
thread_local! {
static NATIVE_STACK_LIMIT: Cell<usize> = const { Cell::new(0) };
}
pub fn set_native_stack_limit(limit: usize) -> usize {
NATIVE_STACK_LIMIT.with(|cell| cell.replace(limit))
}
#[inline]
pub fn native_stack_low() -> Option<usize> {
let limit = NATIVE_STACK_LIMIT.with(Cell::get);
if limit == 0 {
return None;
}
let remaining = stack_pointer().saturating_sub(limit);
(remaining < NATIVE_STACK_MARGIN).then_some(remaining)
}
#[derive(Debug, Default)]
pub struct NativeDiagnosticId(AtomicUsize);
impl NativeDiagnosticId {
pub fn get(&self) -> usize {
self.0.load(Ordering::Relaxed)
}
pub fn set(&self, id: usize) {
self.0.store(id, Ordering::Relaxed);
}
}
impl Clone for NativeDiagnosticId {
fn clone(&self) -> Self {
Self(AtomicUsize::new(self.get()))
}
}
pub fn native_frame(name: &Arc<str>, id: &NativeDiagnosticId) -> NativeFrameGuard {
let previous = HOOKS
.get()
.map_or(0, |hooks| (hooks.enter_native)(name, id));
NativeFrameGuard {
previous,
_thread: std::marker::PhantomData,
}
}
pub fn gc_pause() -> NativeFrameGuard {
let previous = HOOKS.get().map_or(0, |hooks| (hooks.enter_gc_pause)());
NativeFrameGuard {
previous,
_thread: std::marker::PhantomData,
}
}
pub struct NativeFrameGuard {
previous: usize,
_thread: std::marker::PhantomData<std::rc::Rc<()>>,
}
impl Drop for NativeFrameGuard {
fn drop(&mut self) {
if let Some(hooks) = HOOKS.get() {
(hooks.exit_native)(self.previous);
}
}
}
pub const STACK_COMMIT_ESTIMATE: usize = 64 * 1024;
pub struct StackReservation {
_charge: MemoryCharge,
pooled: bool,
overhead: Option<MemoryCharge>,
reserved: usize,
}
impl StackReservation {
pub fn new(bytes: usize) -> Result<Self, MemoryLimitExceeded> {
let charge =
governor().reserve_shared(MemoryClass::FiberStack, bytes.min(STACK_COMMIT_ESTIMATE))?;
let live = LIVE.fetch_add(1, Ordering::Relaxed) + 1;
PEAK.fetch_max(live, Ordering::Relaxed);
RESERVED.fetch_add(bytes, Ordering::Relaxed);
Ok(Self {
_charge: charge,
pooled: false,
overhead: None,
reserved: bytes,
})
}
pub fn account_actual_bytes(
&mut self,
requested: usize,
actual: usize,
) -> Result<(), MemoryLimitExceeded> {
if actual > self.reserved {
RESERVED.fetch_add(actual - self.reserved, Ordering::Relaxed);
self.reserved = actual;
}
let extra = actual.saturating_sub(requested);
if extra > self.overhead.as_ref().map_or(0, MemoryCharge::bytes) {
self.overhead = Some(governor().reserve_shared(MemoryClass::FiberStack, extra)?);
}
Ok(())
}
pub fn set_pooled(&mut self, pooled: bool) {
if self.pooled == pooled {
return;
}
self.pooled = pooled;
if pooled {
LIVE.fetch_sub(1, Ordering::Relaxed);
POOLED.fetch_add(1, Ordering::Relaxed);
} else {
POOLED.fetch_sub(1, Ordering::Relaxed);
let live = LIVE.fetch_add(1, Ordering::Relaxed) + 1;
PEAK.fetch_max(live, Ordering::Relaxed);
}
}
}
impl Drop for StackReservation {
fn drop(&mut self) {
RESERVED.fetch_sub(self.reserved, Ordering::Relaxed);
if self.pooled {
POOLED.fetch_sub(1, Ordering::Relaxed);
} else {
LIVE.fetch_sub(1, Ordering::Relaxed);
}
}
}
pub fn reserved_stack_bytes() -> usize {
RESERVED.load(Ordering::Relaxed)
}
pub(crate) fn counts() -> (usize, usize, usize) {
(
LIVE.load(Ordering::Relaxed),
PEAK.load(Ordering::Relaxed),
POOLED.load(Ordering::Relaxed),
)
}