gpui: Document the leak detector (#44208)

Lukas Wirth created

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

crates/gpui/src/app/entity_map.rs | 112 +++++++++++++++++++++++++++++++-
1 file changed, 107 insertions(+), 5 deletions(-)

Detailed changes

crates/gpui/src/app/entity_map.rs 🔗

@@ -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}");
         }
     }
 }