refslots 0.1.0

Reference-counted handles to slots in a slotmap
Documentation
//! Leak detection for handles, enabled by the `recorder` feature.
//!
//! The recorder tracks live handles for each slot, along with a backtrace captured when each handle
//! was created or cloned. Backtraces are resolved only when a leak is reported, which keeps
//! recording cheap enough for use in tests.

pub use backtrace::Backtrace;

use slotmap::Key;
use std::{collections::HashMap, fmt::Debug, thread};

pub mod assert;

mod backtrace;
mod messages;

/// Identifies a handle instance.
///
/// Ids are allocated monotonically by a [`Recorder`], so a handle with a larger id was created
/// later. [`RecorderSnapshot`] uses this to tell handles created before a snapshot from those
/// created after it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct HandleId(u64);

impl HandleId {
    // Id of a handle created after its registry was dropped. It is never recorded.
    pub(crate) const DETACHED: HandleId = HandleId(u64::MAX);
}

/// State of a slot in a [`RecorderSnapshot`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SlotSnapshot {
    /// Label provided when the slot's first handle was recorded, such as a type name.
    pub label: Option<&'static str>,
    /// Number of living handles pointing to the slot.
    pub handle_count: usize,
}

/// Point-in-time view of slots that had live handles when it was taken.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecorderSnapshot<K: Key> {
    /// Slots with at least one live handle.
    pub slots: HashMap<K, SlotSnapshot>,
    /// Id the next handle will receive.
    next_handle_id: u64,
}

#[derive(Debug, Clone)]
struct SlotRecord {
    label: Option<&'static str>,
    handles: HashMap<HandleId, Backtrace>,
}

impl SlotRecord {
    fn new(label: Option<&'static str>) -> Self {
        Self { label, handles: HashMap::default() }
    }

    fn snapshot(&self) -> SlotSnapshot {
        SlotSnapshot { label: self.label, handle_count: self.handles.len() }
    }
}

/// Tracks live handles for every slot in a registry, to detect leaks.
///
/// Each handle creation and clone are recorded with a backtrace, and each drop removes the record.
/// A slot's record is removed when its last handle is dropped so the recorder only holds slots that
/// still have live handles.
///
/// # Panics
///
/// Dropping a recorder that still has live handles panics. Since a registry owns a recorder, this
/// means dropping a registry that has live slots will also panic.
#[derive(Debug, Clone)]
pub struct Recorder<K: Key> {
    slots: HashMap<K, SlotRecord>,
    next_handle_id: u64,
}

impl<K: Key> Recorder<K> {
    /// Records a new handle to the slot `key` was created and returns its id.
    ///
    /// `label` names the slot in leak reports. Only the label passed for the slot's first handle is
    /// kept, so clones can pass `None`.
    pub fn record_create_handle(
        &mut self,
        key: K,
        label: Option<&'static str>,
        backtrace: Backtrace,
    ) -> HandleId {
        let handle_id = self.next_handle_id();
        let record = self.slots.entry(key).or_insert_with(|| SlotRecord::new(label));
        record.handles.insert(handle_id, backtrace);
        handle_id
    }

    /// Records that a handle for the slot `key` was dropped.
    ///
    /// Unknown keys and handle ids are ignored. The slot's record is removed when its last handle
    /// has been recorded as being dropped.
    pub fn record_drop_handle(&mut self, key: K, handle_id: HandleId) {
        let Some(record) = self.slots.get_mut(&key) else { return };
        record.handles.remove(&handle_id);
        if record.handles.is_empty() {
            self.slots.remove(&key);
        }
    }

    /// Captures the current set of live slots.
    pub fn snapshot(&self) -> RecorderSnapshot<K> {
        let mut slots = HashMap::with_capacity(self.slots.len());
        for (key, record) in &self.slots {
            slots.insert(*key, record.snapshot());
        }
        RecorderSnapshot { slots, next_handle_id: self.next_handle_id }
    }

    fn next_handle_id(&mut self) -> HandleId {
        let id = self.next_handle_id;
        self.next_handle_id += 1;
        HandleId(id)
    }
}

impl<K: Key> Default for Recorder<K> {
    fn default() -> Self {
        Self { slots: HashMap::default(), next_handle_id: 0 }
    }
}

impl<K: Key> Drop for Recorder<K> {
    fn drop(&mut self) {
        if !thread::panicking() {
            assert::all_handles_released(self);
        }
    }
}

fn collect_sorted<K: Ord, T>(map: &HashMap<K, T>) -> Vec<(&K, &T)> {
    let mut entries = map.iter().collect::<Vec<_>>();
    entries.sort_by_key(|(key, _)| *key);
    entries
}

#[cfg(test)]
pub mod tests {
    use crate::recorder::{Backtrace, HandleId};
    use slotmap::{DefaultKey, KeyData};

    pub type Recorder = super::Recorder<DefaultKey>;

    mod create_handle {
        use super::*;

        #[test]
        fn record_one() {
            let mut recorder = Recorder::default();
            let key = make_key(1);

            record_create(&mut recorder, key);
            assert!(recorder.slots.contains_key(&key));
            let snapshot = recorder.snapshot();
            assert_eq!(snapshot.slots.get(&key).map(|slot| slot.handle_count), Some(1));
            shutdown(&mut recorder);
        }

        #[test]
        fn record_many() {
            let mut recorder = Recorder::default();
            let key = make_key(1);

            record_create(&mut recorder, key);
            record_create(&mut recorder, key);
            assert!(recorder.slots.contains_key(&key));
            let snapshot = recorder.snapshot();
            assert_eq!(snapshot.slots.get(&key).map(|slot| slot.handle_count), Some(2));
            shutdown(&mut recorder);
        }
    }

    mod drop_handle {
        use super::*;

        #[test]
        fn record_one() {
            let mut recorder = Recorder::default();
            let key = make_key(1);
            let handle_id = record_create(&mut recorder, key);

            recorder.record_drop_handle(key, handle_id);
            assert!(!recorder.slots.contains_key(&key));
            let snapshot = recorder.snapshot();
            assert!(snapshot.slots.is_empty());
        }

        #[test]
        fn record_many() {
            let mut recorder = Recorder::default();
            let key = make_key(1);
            let handle_id = record_create(&mut recorder, key);
            record_create(&mut recorder, key);

            recorder.record_drop_handle(key, handle_id);
            assert!(recorder.slots.contains_key(&key));
            let snapshot = recorder.snapshot();
            assert_eq!(snapshot.slots.get(&key).map(|slot| slot.handle_count), Some(1));
            shutdown(&mut recorder);
        }
    }

    #[test]
    fn drop_released() {
        let mut recorder = Recorder::default();
        let key = make_key(1);
        let handle_id = record_create(&mut recorder, key);
        recorder.record_drop_handle(key, handle_id);
    }

    #[test]
    #[should_panic(expected = "1 slots with undropped handles")]
    fn drop_unreleased() {
        let mut recorder = Recorder::default();
        let key = make_key(1);
        record_create(&mut recorder, key);
    }

    #[test]
    fn snapshot_empty() {
        let recorder = Recorder::default();
        assert!(recorder.snapshot().slots.is_empty());
    }

    pub fn make_key(value: u64) -> DefaultKey {
        KeyData::from_ffi(value).into()
    }

    pub fn record_create(recorder: &mut Recorder, key: DefaultKey) -> HandleId {
        recorder.record_create_handle(key, None, Backtrace::new_unresolved())
    }

    pub fn shutdown(recorder: &mut Recorder) {
        recorder.slots.clear();
    }
}