Lines
92.13 %
Functions
56.47 %
Branches
100 %
//! Helpers for reference counting
//!
//! Two main purposes:
//! * Consistent handling of overflow and underflow
//! * Assurance of incrementing/decrementing as appropriate,
//! including in combination with a slotmap containing the referenced data.
//! The caller is responsible for making sure that the *right instance*'s
//! [`Count`] is passed to the methods on [`Ref`].
//
// There are no separate tests for this module. Many of the tests would want to
// exercise the `Ref`s drop bomb, which is troublesome since it's panic in drop,
// which they're making Rust treat as an abort upstream.
// (This scheme did detect a bug or two during development testing,
// so the drop bomb is known to work.)
// Anyway, these functions are mostly newtype veneers over existing functionality.
// They're tested by the MemoryQuotaTracker's tests.
use crate::internal_prelude::*;
/// Local alias for the counter type
pub(crate) type RawCount = u32;
/// Decrement a refcount and maybe remove a corresponding slotmap entry
///
/// ```rust,ignore
/// fn slotmap_dec_ref!<K, V>(
/// slotmap: &mut SlotMap<K, V>,
/// ref_: Ref<K>,
/// refcount: &mut Count<K>, // (typically) borrows from slotmap
/// )
/// ```
// This macro is a bit out-of-position, up here, because we want to be able to link
// to it in our rustdocs.
macro_rules! slotmap_dec_ref { { $slotmap:expr, $ref_:expr, $refcount:expr } => { { {
use $crate::refcount::*;
let key: Ref<_> = $ref_;
let refcount: &mut Count<_> = $refcount;
if let Some(Garbage(key)) = key.dispose(refcount) {
let slotmap: &mut SlotMap<_, _> = $slotmap;
let removed = slotmap.remove(key).expect("entry vanished or wrong key passed?!");
Some(Garbage(removed))
} else {
None
}
} } } }
/// A reference count, counting references with id type `K`
#[derive(Default, Educe, Ord, PartialOrd, Eq, PartialEq, Deref)]
#[educe(Debug)]
pub(crate) struct Count<K> {
/// Actual count of references
#[deref]
count: RawCount,
/// Bind to the specific key type
// K is generally Send + Sync + 'static so we don't care about variance etc.
#[educe(Debug(ignore))]
marker: PhantomData<K>,
/// An copy of a [`slotmap_careful::Key`] `K`, which is counted by a `RefCount`
/// Ie, a key of type `K` with the property that it
/// keeps the refcounted data structure alive.
/// Must always be deleted using [`dispose`](Ref::dispose), not dropped.
/// In tests, dropping a `RefCounted` will panic.
/// The `Default` value does *not* contribute to a reference count,
/// and is fine to drop.
#[derive(Deref, Educe)]
#[educe(Debug, Default, Ord, Eq, PartialEq)]
pub(crate) struct Ref<K: slotmap_careful::Key> {
/// Actual key (without generics)
raw_key: K,
/// Drop bomb
/// Also forces `Ref` not to be Clone
#[educe(Debug(ignore), Ord(ignore), Eq(ignore), PartialEq(ignore))]
#[allow(dead_code)]
bomb: DropBombCondition,
// educe's Ord is open-coded and triggers clippy::non_canonical_partial_ord_impl
impl<K: slotmap_careful::Key> PartialOrd for Ref<K> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
// Ideally we'd assert_not_impl on Ref but it has generics
assert_not_impl_any!(DropBombCondition: Clone);
/// Error: refcount overflowed
#[derive(Debug, Clone, Error, Eq, PartialEq)]
#[error("memory tracking refcount overflowed")]
pub(crate) struct Overflow;
/// Something which has become garbage
/// Often used within `Option`, for clarity. Examples:
/// * Key whose reference count has reached zero - see [`Ref::dispose`]
/// * Value removed from a SlotMap - see [`slotmap_dec_ref!`]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub(crate) struct Garbage<K>(pub(crate) K);
impl<K> Count<K> {
/// Make a new refcount with a specified value
const fn new_raw(count: RawCount) -> Self {
Count {
count,
marker: PhantomData,
/// Obtain this counter as a `usize`
/// (Reference counts are `u32`, so this might be a conversion.)
pub(crate) fn as_usize(&self) -> usize {
// On a 16-bit platform this could theoretically overflow,
// but there would have to be >2^16 clones, which would be impossible.
let r: u32 = **self;
r as usize
/// Increment this refcount, but don't care about any [`Ref`]s
fn inc_raw(c: &mut RawCount) -> Result<(), Overflow> {
*c = c.checked_add(1).ok_or(Overflow)?;
Ok(())
/// Decrement this refcount, but don't care about any [`Ref`]s
/// Returns [`Some(Garbage(()))`] if the count reached zero
fn dec_raw(c: &mut RawCount) -> Option<Garbage<()>> {
*c = c
.checked_sub(1)
// if this happens, our data structure is corrupted, very bad
.expect("refcount underflow");
(*c == 0).then_some(Garbage(()))
impl<K: slotmap_careful::Key> Ref<K> {
/// Create a refcounted reference `Ref` from an un-counted key, incrementing the count
pub(crate) fn new(key: K, count: &mut Count<K>) -> Result<Self, Overflow> {
inc_raw(&mut count.count)?;
Ok(Ref::from_raw(key))
/// Creates a null `Ref`, which doesn't refer to any slot (lookups always give `None`)
pub(crate) fn null() -> Self {
Ref::from_raw(K::null())
/// Internal function for creating a `Ref`
fn from_raw(raw_key: K) -> Self {
Ref {
raw_key,
bomb: DropBombCondition::new_armed(),
/// Dispose of a refcounted reference `Ref`, decrementing the count
/// If the count reaches zero, the raw key is returned;
/// the caller should remove the corresponding data from the data structure.
pub(crate) fn dispose(mut self, refcount: &mut Count<K>) -> Option<Garbage<K>> {
let was = mem::take(&mut self.raw_key);
assert!(!was.is_null());
dec_raw(&mut refcount.count).map(|_: Garbage<()>| Garbage(was))
/// Dispose of a refcounted reference whose container no longer exists
/// # CORRECTNESS
/// This just forgets the reference, without decrementing any refcount.
/// If the container *does* still exist, a ref count ref will be leaked.
pub(crate) fn dispose_container_destroyed(mut self) {
let _: K = mem::take(&mut self.raw_key);
impl<K: slotmap_careful::Key> DefaultExtTake for Ref<K> {}
/// Insert a new entry into a slotmap using refcounted keys
/// `value_maker` should take the provided `Count`,
/// and incorporate it into a new value.
/// On return, the entry will be in the map, and there will be one reference,
/// which is returned.
/// There is no corresponding `slotmap_remove` in this module.
/// Use [`Ref::dispose`] and handle any [`Garbage`] it returns.
pub(crate) fn slotmap_insert<K: slotmap_careful::Key, V>(
slotmap: &mut SlotMap<K, V>,
value_maker: impl FnOnce(Count<K>) -> V,
) -> Ref<K> {
let (ref_, ()) = slotmap_try_insert(slotmap, move |refcount| {
Ok::<_, Void>((value_maker(refcount), ()))
})
.void_unwrap();
ref_
/// Insert a new entry into a slotmap using refcounted keys, fallibly and with extra data
/// Like [`slotmap_insert`] but:
/// * `value_maker` can also return extra return data `RD` to the caller
/// * `value_maker` is allowed to fail.
/// On successful return, the entry will be in the map, and
/// the new `Ref` is returned along with the data `D`.
pub(crate) fn slotmap_try_insert<K: slotmap_careful::Key, V, E, RD>(
value_maker: impl FnOnce(Count<K>) -> Result<(V, RD), E>,
) -> Result<(Ref<K>, RD), E> {
let refcount = Count::new_raw(1);
let (value, data) = value_maker(refcount)?;
let raw_key = slotmap.insert(value);
let ref_ = Ref {
};
Ok((ref_, data))
/// Unconditionally remove en entry from the slotmap, given a strong ref
/// Other references to this entry will become dangling.
pub(crate) fn slotmap_remove_early<K: slotmap_careful::Key, V>(
key: Ref<K>,
) -> Option<V> {
let r = slotmap.remove(*key);
// Correctness: this becomes a reference to a missing entry
key.dispose_container_destroyed();
r
#[cfg(test)]
impl<K: slotmap_careful::Key> Drop for Ref<K> {
fn drop(&mut self) {
drop_bomb_disarm_assert!(self.bomb, self.raw_key.is_null(),);
impl From<Overflow> for Error {
fn from(_overflow: Overflow) -> Error {
internal!("reference count overflow in memory tracking (out-of-control subsystem?)").into()
mod test {
// @@ begin test lint list maintained by maint/add_warning @@
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
#![allow(clippy::let_and_return)] // TODO this lint is annoying and we should disable it
use super::*;
slotmap_careful::new_key_type! {
struct Id;
#[derive(Eq, PartialEq, Debug)]
struct Record {
refcount: Count<Id>,
type Map = SlotMap<Id, Record>;
fn setup() -> (Map, Ref<Id>) {
let mut map = Map::default();
let ref_ = slotmap_insert(&mut map, |refcount| Record { refcount });
(map, ref_)
#[test]
fn good() {
let (mut map, ref1) = setup();
let ent = map.get_mut(*ref1).unwrap();
let ref2 = Ref::new(*ref1, &mut ent.refcount).unwrap();
let g1: Option<Garbage<Record>> = slotmap_dec_ref!(&mut map, ref1, &mut ent.refcount);
assert_eq!(g1, None);
let ent = map.get_mut(*ref2).unwrap();
let g2: Option<Garbage<Record>> = slotmap_dec_ref!(&mut map, ref2, &mut ent.refcount);
assert!(g2.is_some());
fn try_insert_fail() {
let () = slotmap_try_insert::<_, _, _, String>(&mut map, |_refcount| Err(())).unwrap_err();
fn drop_ref_without_decrement() {
let (_map, mut ref1) = setup();
let h = ref1.bomb.make_simulated();
drop(ref1);
h.expect_exploded();