gpui: Document the leak detector (#44208)
Release Notes: - N/A *or* Added/Fixed/Improved ...
This commit is contained in:
@@ -584,7 +584,33 @@ impl AnyWeakEntity {
|
||||
})
|
||||
}
|
||||
|
||||
/// Assert that entity referenced by this weak handle has been released.
|
||||
/// Asserts that the entity referenced by this weak handle has been fully released.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// let entity = cx.new(|_| MyEntity::new());
|
||||
/// let weak = entity.downgrade();
|
||||
/// drop(entity);
|
||||
///
|
||||
/// // Verify the entity was released
|
||||
/// weak.assert_released();
|
||||
/// ```
|
||||
///
|
||||
/// # Debugging Leaks
|
||||
///
|
||||
/// If this method panics due to leaked handles, set the `LEAK_BACKTRACE` environment
|
||||
/// variable to see where the leaked handles were allocated:
|
||||
///
|
||||
/// ```bash
|
||||
/// LEAK_BACKTRACE=1 cargo test my_test
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// - Panics if any strong handles to the entity are still alive.
|
||||
/// - Panics if the entity was recently dropped but cleanup hasn't completed yet
|
||||
/// (resources are retained until the end of the effect cycle).
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
pub fn assert_released(&self) {
|
||||
self.entity_ref_counts
|
||||
@@ -814,16 +840,70 @@ impl<T: 'static> PartialOrd for WeakEntity<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Controls whether backtraces are captured when entity handles are created.
|
||||
///
|
||||
/// Set the `LEAK_BACKTRACE` environment variable to any non-empty value to enable
|
||||
/// backtrace capture. This helps identify where leaked handles were allocated.
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
static LEAK_BACKTRACE: std::sync::LazyLock<bool> =
|
||||
std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").is_ok_and(|b| !b.is_empty()));
|
||||
|
||||
/// Unique identifier for a specific entity handle instance.
|
||||
///
|
||||
/// This is distinct from `EntityId` - while multiple handles can point to the same
|
||||
/// entity (same `EntityId`), each handle has its own unique `HandleId`.
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
|
||||
pub(crate) struct HandleId {
|
||||
id: u64, // id of the handle itself, not the pointed at object
|
||||
id: u64,
|
||||
}
|
||||
|
||||
/// Tracks entity handle allocations to detect leaks.
|
||||
///
|
||||
/// The leak detector is enabled in tests and when the `leak-detection` feature is active.
|
||||
/// It tracks every `Entity<T>` and `AnyEntity` handle that is created and released,
|
||||
/// allowing you to verify that all handles to an entity have been properly dropped.
|
||||
///
|
||||
/// # How do leaks happen?
|
||||
///
|
||||
/// Entities are reference-counted structures that can own other entities
|
||||
/// allowing to form cycles. If such a strong-reference counted cycle is
|
||||
/// created, all participating strong entities in this cycle will effectively
|
||||
/// leak as they cannot be released anymore.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// You can use `WeakEntity::assert_released` or `AnyWeakEntity::assert_released`
|
||||
/// to verify that an entity has been fully released:
|
||||
///
|
||||
/// ```ignore
|
||||
/// let entity = cx.new(|_| MyEntity::new());
|
||||
/// let weak = entity.downgrade();
|
||||
/// drop(entity);
|
||||
///
|
||||
/// // This will panic if any handles to the entity are still alive
|
||||
/// weak.assert_released();
|
||||
/// ```
|
||||
///
|
||||
/// # Debugging Leaks
|
||||
///
|
||||
/// When a leak is detected, the detector will panic with information about the leaked
|
||||
/// handles. To see where the leaked handles were allocated, set the `LEAK_BACKTRACE`
|
||||
/// environment variable:
|
||||
///
|
||||
/// ```bash
|
||||
/// LEAK_BACKTRACE=1 cargo test my_test
|
||||
/// ```
|
||||
///
|
||||
/// This will capture and display backtraces for each leaked handle, helping you
|
||||
/// identify where handles were created but not released.
|
||||
///
|
||||
/// # How It Works
|
||||
///
|
||||
/// - When an entity handle is created (via `Entity::new`, `Entity::clone`, or
|
||||
/// `WeakEntity::upgrade`), `handle_created` is called to register the handle.
|
||||
/// - When a handle is dropped, `handle_released` removes it from tracking.
|
||||
/// - `assert_released` verifies that no handles remain for a given entity.
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
pub(crate) struct LeakDetector {
|
||||
next_handle_id: u64,
|
||||
@@ -832,6 +912,11 @@ pub(crate) struct LeakDetector {
|
||||
|
||||
#[cfg(any(test, feature = "leak-detection"))]
|
||||
impl LeakDetector {
|
||||
/// Records that a new handle has been created for the given entity.
|
||||
///
|
||||
/// Returns a unique `HandleId` that must be passed to `handle_released` when
|
||||
/// the handle is dropped. If `LEAK_BACKTRACE` is set, captures a backtrace
|
||||
/// at the allocation site.
|
||||
#[track_caller]
|
||||
pub fn handle_created(&mut self, entity_id: EntityId) -> HandleId {
|
||||
let id = util::post_inc(&mut self.next_handle_id);
|
||||
@@ -844,23 +929,40 @@ impl LeakDetector {
|
||||
handle_id
|
||||
}
|
||||
|
||||
/// Records that a handle has been released (dropped).
|
||||
///
|
||||
/// This removes the handle from tracking. The `handle_id` should be the same
|
||||
/// one returned by `handle_created` when the handle was allocated.
|
||||
pub fn handle_released(&mut self, entity_id: EntityId, handle_id: HandleId) {
|
||||
let handles = self.entity_handles.entry(entity_id).or_default();
|
||||
handles.remove(&handle_id);
|
||||
}
|
||||
|
||||
/// Asserts that all handles to the given entity have been released.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if any handles to the entity are still alive. The panic message
|
||||
/// includes backtraces for each leaked handle if `LEAK_BACKTRACE` is set,
|
||||
/// otherwise it suggests setting the environment variable to get more info.
|
||||
pub fn assert_released(&mut self, entity_id: EntityId) {
|
||||
use std::fmt::Write as _;
|
||||
let handles = self.entity_handles.entry(entity_id).or_default();
|
||||
if !handles.is_empty() {
|
||||
let mut out = String::new();
|
||||
for backtrace in handles.values_mut() {
|
||||
if let Some(mut backtrace) = backtrace.take() {
|
||||
backtrace.resolve();
|
||||
eprintln!("Leaked handle: {:#?}", backtrace);
|
||||
writeln!(out, "Leaked handle:\n{:?}", backtrace).unwrap();
|
||||
} else {
|
||||
eprintln!("Leaked handle: export LEAK_BACKTRACE to find allocation site");
|
||||
writeln!(
|
||||
out,
|
||||
"Leaked handle: (export LEAK_BACKTRACE to find allocation site)"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
panic!();
|
||||
panic!("{out}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user