Add LeakDetector for gpui2

Co-Authored-By: Julia <julia@zed.dev>
This commit is contained in:
Conrad Irwin 2024-01-04 20:16:25 -07:00
parent 22c3eb7d5f
commit 5037cca7ec
2 changed files with 126 additions and 3 deletions

View file

@ -1,6 +1,8 @@
use crate::{private::Sealed, AppContext, Context, Entity, ModelContext}; use crate::{private::Sealed, AppContext, Context, Entity, ModelContext};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use collections::HashMap;
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
use lazy_static::lazy_static;
use parking_lot::{RwLock, RwLockUpgradableReadGuard}; use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use slotmap::{SecondaryMap, SlotMap}; use slotmap::{SecondaryMap, SlotMap};
use std::{ use std::{
@ -38,6 +40,8 @@ pub(crate) struct EntityMap {
struct EntityRefCounts { struct EntityRefCounts {
counts: SlotMap<EntityId, AtomicUsize>, counts: SlotMap<EntityId, AtomicUsize>,
dropped_entity_ids: Vec<EntityId>, dropped_entity_ids: Vec<EntityId>,
#[cfg(any(test, feature = "test-support"))]
leak_detector: LeakDetector,
} }
impl EntityMap { impl EntityMap {
@ -47,6 +51,11 @@ impl EntityMap {
ref_counts: Arc::new(RwLock::new(EntityRefCounts { ref_counts: Arc::new(RwLock::new(EntityRefCounts {
counts: SlotMap::with_key(), counts: SlotMap::with_key(),
dropped_entity_ids: Vec::new(), dropped_entity_ids: Vec::new(),
#[cfg(any(test, feature = "test-support"))]
leak_detector: LeakDetector {
next_handle_id: 0,
entity_handles: HashMap::default(),
},
})), })),
} }
} }
@ -156,6 +165,8 @@ pub struct AnyModel {
pub(crate) entity_id: EntityId, pub(crate) entity_id: EntityId,
pub(crate) entity_type: TypeId, pub(crate) entity_type: TypeId,
entity_map: Weak<RwLock<EntityRefCounts>>, entity_map: Weak<RwLock<EntityRefCounts>>,
#[cfg(any(test, feature = "test-support"))]
handle_id: HandleId,
} }
impl AnyModel { impl AnyModel {
@ -163,7 +174,14 @@ impl AnyModel {
Self { Self {
entity_id: id, entity_id: id,
entity_type, entity_type,
entity_map, entity_map: entity_map.clone(),
#[cfg(any(test, feature = "test-support"))]
handle_id: entity_map
.upgrade()
.unwrap()
.write()
.leak_detector
.handle_created(id),
} }
} }
@ -207,11 +225,20 @@ impl Clone for AnyModel {
assert_ne!(prev_count, 0, "Detected over-release of a model."); assert_ne!(prev_count, 0, "Detected over-release of a model.");
} }
Self { let this = Self {
entity_id: self.entity_id, entity_id: self.entity_id,
entity_type: self.entity_type, entity_type: self.entity_type,
entity_map: self.entity_map.clone(), entity_map: self.entity_map.clone(),
} #[cfg(any(test, feature = "test-support"))]
handle_id: self
.entity_map
.upgrade()
.unwrap()
.write()
.leak_detector
.handle_created(self.entity_id),
};
this
} }
} }
@ -231,6 +258,14 @@ impl Drop for AnyModel {
entity_map.dropped_entity_ids.push(self.entity_id); entity_map.dropped_entity_ids.push(self.entity_id);
} }
} }
#[cfg(any(test, feature = "test-support"))]
if let Some(entity_map) = self.entity_map.upgrade() {
entity_map
.write()
.leak_detector
.handle_dropped(self.entity_id, self.handle_id)
}
} }
} }
@ -423,13 +458,43 @@ impl AnyWeakModel {
return None; return None;
} }
ref_count.fetch_add(1, SeqCst); ref_count.fetch_add(1, SeqCst);
drop(ref_counts);
Some(AnyModel { Some(AnyModel {
entity_id: self.entity_id, entity_id: self.entity_id,
entity_type: self.entity_type, entity_type: self.entity_type,
entity_map: self.entity_ref_counts.clone(), entity_map: self.entity_ref_counts.clone(),
#[cfg(any(test, feature = "test-support"))]
handle_id: self
.entity_ref_counts
.upgrade()
.unwrap()
.write()
.leak_detector
.handle_created(self.entity_id),
}) })
} }
#[cfg(any(test, feature = "test-support"))]
pub fn assert_dropped(&self) {
self.entity_ref_counts
.upgrade()
.unwrap()
.write()
.leak_detector
.assert_dropped(self.entity_id);
if self
.entity_ref_counts
.upgrade()
.and_then(|ref_counts| Some(ref_counts.read().counts.get(self.entity_id)?.load(SeqCst)))
.is_some()
{
panic!(
"entity was recently dropped but resources are retained until the end of the effect cycle."
)
}
}
} }
impl<T> From<WeakModel<T>> for AnyWeakModel { impl<T> From<WeakModel<T>> for AnyWeakModel {
@ -534,6 +599,59 @@ impl<T> PartialEq<Model<T>> for WeakModel<T> {
} }
} }
#[cfg(any(test, feature = "test-support"))]
lazy_static! {
static ref LEAK_BACKTRACE: bool =
std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty());
}
#[cfg(any(test, feature = "test-support"))]
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
pub struct HandleId {
id: u64, // id of the handle itself, not the pointed at object
}
#[cfg(any(test, feature = "test-support"))]
pub struct LeakDetector {
next_handle_id: u64,
entity_handles: HashMap<EntityId, HashMap<HandleId, Option<backtrace::Backtrace>>>,
}
#[cfg(any(test, feature = "test-support"))]
impl LeakDetector {
#[track_caller]
pub fn handle_created(&mut self, entity_id: EntityId) -> HandleId {
let id = util::post_inc(&mut self.next_handle_id);
let handle_id = HandleId { id };
let handles = self.entity_handles.entry(entity_id).or_default();
handles.insert(
handle_id,
LEAK_BACKTRACE.then(|| backtrace::Backtrace::new_unresolved()),
);
handle_id
}
pub fn handle_dropped(&mut self, entity_id: EntityId, handle_id: HandleId) {
let handles = self.entity_handles.entry(entity_id).or_default();
handles.remove(&handle_id);
}
pub fn assert_dropped(&mut self, entity_id: EntityId) {
let handles = self.entity_handles.entry(entity_id).or_default();
if !handles.is_empty() {
for (_, backtrace) in handles {
if let Some(mut backtrace) = backtrace.take() {
backtrace.resolve();
eprintln!("Leaked handle: {:#?}", backtrace);
} else {
eprintln!("Leaked handle: export LEAK_BACKTRACE to find allocation site");
}
}
panic!();
}
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::EntityMap; use crate::EntityMap;

View file

@ -143,6 +143,11 @@ impl<V: 'static> WeakView<V> {
let view = self.upgrade().context("error upgrading view")?; let view = self.upgrade().context("error upgrading view")?;
Ok(view.update(cx, f)).flatten() Ok(view.update(cx, f)).flatten()
} }
#[cfg(any(test, feature = "test-support"))]
pub fn assert_dropped(&self) {
self.model.assert_dropped()
}
} }
impl<V> Clone for WeakView<V> { impl<V> Clone for WeakView<V> {