gpui: Document the leak detector (#44208)

Release Notes:

- N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Lukas Wirth
2025-12-05 12:35:05 +00:00
committed by GitHub
parent 35da6d000a
commit a5ab5c7d5d
+107 -5
View File
@@ -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}");
}
}
}