use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
struct Allocation {
buffer: Mutex<Option<Box<dyn RuntimeMemoryPermit>>>,
address: AtomicUsize,
next: Option<Arc<Self>>,
_metadata: Option<Box<dyn RuntimeMemoryPermit>>,
}
#[derive(Default, Clone)]
pub(crate) struct CheckpointAllocationOwner {
head: Option<Arc<Allocation>>,
}
impl std::fmt::Debug for CheckpointAllocationOwner {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("CheckpointAllocationOwner")
.field("has_retained_allocations", &self.head.is_some())
.finish()
}
}
pub(crate) struct CheckpointAllocationToken(Option<Arc<Allocation>>);
pub(crate) struct CheckpointAllocationLookup {
address: usize,
current: Option<Arc<Allocation>>,
}
impl Drop for CheckpointAllocationLookup {
fn drop(&mut self) {
drop(CheckpointAllocationOwner {
head: self.current.take(),
});
}
}
impl CheckpointAllocationToken {
pub(crate) fn address(&self, address: usize) {
if let Some(node) = &self.0 {
node.address.store(address, Ordering::Relaxed);
}
}
pub(crate) fn release_buffer(self) {
if let Some(node) = &self.0 {
let permit = node
.buffer
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.take();
drop(permit);
}
}
}
impl CheckpointAllocationOwner {
pub(crate) fn is_empty(&self) -> bool {
self.head.is_none()
}
pub(crate) const METADATA_BYTES: usize = std::mem::size_of::<Allocation>()
+ 2 * std::mem::size_of::<std::sync::atomic::AtomicUsize>();
pub(crate) fn reserve(
&mut self,
bytes: usize,
work: &CheckpointWorkContext,
) -> Result<CheckpointAllocationToken, CheckpointWorkError> {
if bytes == 0 {
return Ok(CheckpointAllocationToken(None));
}
let metadata = work.reserve_memory(Self::METADATA_BYTES)?;
let buffer = work.reserve_memory(bytes)?;
if metadata.is_none() && buffer.is_none() {
return Ok(CheckpointAllocationToken(None));
}
let node = Arc::new(Allocation {
buffer: Mutex::new(buffer),
address: AtomicUsize::new(0),
next: self.head.take(),
_metadata: metadata,
});
self.head = Some(node.clone());
Ok(CheckpointAllocationToken(Some(node)))
}
pub(crate) fn find(
&self,
address: usize,
work: &CheckpointWorkContext,
) -> Result<CheckpointAllocationToken, CheckpointWorkError> {
self.find_retained(address, &mut None, work)
}
pub(crate) fn find_retained(
&self,
address: usize,
pending: &mut Option<CheckpointAllocationLookup>,
work: &CheckpointWorkContext,
) -> Result<CheckpointAllocationToken, CheckpointWorkError> {
let state = pending.get_or_insert_with(|| CheckpointAllocationLookup {
address,
current: self.head.clone(),
});
if state.address != address {
return Err(work.record_failure(CheckpointWorkError::Allocation {
bytes: 0,
reason: "retained allocation lookup address changed".into(),
}));
}
while let Some(node) = state.current.clone() {
let unit = work.start_unit()?;
let found = node.address.load(Ordering::Relaxed) == address;
if found {
*pending = None;
unit.finish();
return Ok(CheckpointAllocationToken(Some(node)));
}
state.current = node.next.clone();
unit.finish();
}
*pending = None;
Ok(CheckpointAllocationToken(None))
}
pub(crate) fn coalesce_retained_head(&mut self) -> bool {
let Some(head) = self.head.as_mut().and_then(Arc::get_mut) else {
return false;
};
let Some(next) = head.next.as_mut().and_then(Arc::get_mut) else {
return false;
};
let target = head.buffer.get_mut().unwrap_or_else(|p| p.into_inner());
let source = next.buffer.get_mut().unwrap_or_else(|p| p.into_inner());
match (target.as_mut(), source.take()) {
(_, None) => {}
(Some(target), Some(source_permit)) => {
if let Err(source_permit) = target.merge(source_permit) {
*source = Some(source_permit);
return false;
}
}
(None, Some(source_permit)) => *target = Some(source_permit),
}
let suffix = next.next.take();
let retired = head.next.take();
head.next = suffix;
drop(CheckpointAllocationOwner { head: retired });
true
}
pub(crate) fn append_retained(
&mut self,
incoming: &mut Self,
work: &CheckpointWorkContext,
) -> Result<(), CheckpointWorkError> {
while let Some(head) = incoming.head.as_mut().and_then(Arc::get_mut) {
let Some(next) = head.next.as_mut().and_then(Arc::get_mut) else {
break;
};
let unit = work.start_unit()?;
let target = head.buffer.get_mut().unwrap_or_else(|p| p.into_inner());
let source = next.buffer.get_mut().unwrap_or_else(|p| p.into_inner());
let merged = match (target.as_mut(), source.take()) {
(_, None) => true,
(Some(target), Some(source_permit)) => match target.merge(source_permit) {
Ok(()) => true,
Err(source_permit) => {
*source = Some(source_permit);
false
}
},
(None, Some(source_permit)) => {
*target = Some(source_permit);
true
}
};
if !merged {
unit.finish_without_progress();
break;
}
let suffix = next.next.take();
let retired = head.next.take();
head.next = suffix;
drop(CheckpointAllocationOwner { head: retired });
unit.finish();
}
let unique_record = incoming
.head
.as_ref()
.is_some_and(|head| head.next.is_none());
if unique_record
&& let Some(target) = self.head.as_mut().and_then(Arc::get_mut)
&& let Some(source) = incoming.head.as_mut().and_then(Arc::get_mut)
{
let unit = work.start_unit()?;
let target = target.buffer.get_mut().unwrap_or_else(|p| p.into_inner());
let source = source.buffer.get_mut().unwrap_or_else(|p| p.into_inner());
let merged = match (target.as_mut(), source.take()) {
(_, None) => true,
(Some(target), Some(source_permit)) => match target.merge(source_permit) {
Ok(()) => true,
Err(source_permit) => {
*source = Some(source_permit);
false
}
},
(None, Some(source_permit)) => {
*target = Some(source_permit);
true
}
};
if merged {
drop(CheckpointAllocationOwner {
head: incoming.head.take(),
});
unit.finish();
return Ok(());
}
unit.finish_without_progress();
}
self.append(incoming, work)
}
pub(crate) fn append(
&mut self,
incoming: &mut Self,
work: &CheckpointWorkContext,
) -> Result<(), CheckpointWorkError> {
let Some(mut current) = incoming.head.as_mut() else {
return Ok(());
};
loop {
let unit = work.start_unit()?;
let node = Arc::get_mut(current).ok_or_else(|| {
work.record_failure(CheckpointWorkError::Allocation {
bytes: 0,
reason: "decoded allocation transfer requires unique incoming ownership".into(),
})
})?;
let last = node.next.is_none();
unit.finish();
if last {
work.checkpoint()?;
node.next = self.head.take();
break;
}
current = node
.next
.as_mut()
.expect("checked incoming allocation tail");
}
self.head = incoming.head.take();
Ok(())
}
}
impl Drop for CheckpointAllocationOwner {
fn drop(&mut self) {
let mut current = self.head.take();
while let Some(node) = current {
match Arc::try_unwrap(node) {
Ok(mut node) => {
current = node.next.take();
drop(node);
}
Err(shared) => {
drop(shared);
break;
}
}
}
}
}
#[cfg(test)]
mod retained_tests;