Empty debug contexts are automatically cleaned up

This commit is contained in:
Jonathan Johnson 2023-12-29 08:55:24 -08:00
parent 3fc49d2424
commit 999f920f8c
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
3 changed files with 106 additions and 13 deletions

View file

@ -39,6 +39,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
that can be deallocated independently of other graphs it is connected with.
- `DynamicReader::on_disconnect` allows attaching a callback that is invoked
once the final source `Dynamic` is dropped.
- `Dynamic::instances()` returns the number of clones the dynamic has in
existence.
- `Dynamic::readers()` returns the number of `DynamicReader`s for the dynamic in
existence.
[99]: https://github.com/khonsulabs/cushy/issues/99

View file

@ -4,7 +4,7 @@ use std::fmt::Debug;
use alot::OrderedLots;
use crate::value::{Dynamic, DynamicReader, ForEach};
use crate::value::{Dynamic, DynamicReader, ForEach, WeakDynamic};
use crate::widget::{Children, MakeWidget, WidgetInstance};
use crate::widgets::grid::{Grid, GridWidgets};
use crate::window::Window;
@ -83,6 +83,15 @@ impl DebugContext {
.into_window()
.titled("Cushy Debugger")
}
/// Returns true if this debug context has no child sections or observed
/// values.
#[must_use]
pub fn is_empty(&self) -> bool {
self.section.map_ref(|section| {
section.children.map_ref(OrderedLots::len) + section.values.map_ref(OrderedLots::len)
}) == 0
}
}
impl Open for DebugContext {
@ -98,6 +107,22 @@ impl Open for DebugContext {
}
}
impl Drop for DebugContext {
fn drop(&mut self) {
// If the only two references are this context and the parent owning the
// child section, then we want to remove the section if nothing was
// added to it.
if self.section.instances() == 2 {
let section = self.section.lock();
if let Some(parent) = section.parent.clone() {
let label = section.label.clone();
drop(section);
DebugSection::remove_child_section(&parent, &label);
}
}
}
}
trait Observable: Send {
fn label(&self) -> &str;
fn alive(&self) -> bool;
@ -132,6 +157,7 @@ struct DebugSection {
children: Dynamic<OrderedLots<Dynamic<DebugSection>>>,
values: Dynamic<OrderedLots<Box<dyn Observable>>>,
widget: WidgetInstance,
parent: Option<WeakDynamic<DebugSection>>,
}
impl Default for DebugSection {
@ -162,23 +188,13 @@ impl DebugSection {
let parent = parent.map(Dynamic::downgrade);
// Create a cleanup task to remove this section once it becomes empty.
if let Some(parent) = parent {
if let Some(parent) = parent.clone() {
let label = label.clone();
(&children, &values)
.for_each({
move |(children, values)| {
if children.is_empty() && values.is_empty() {
if let Some(parent) = parent.upgrade() {
let parent = parent.lock();
let mut children = parent.children.lock();
if let Some(index) =
children.iter().enumerate().find_map(|(index, child)| {
child.map_ref(|child| child.label == label).then_some(index)
})
{
children.remove_by_index(index);
}
}
Self::remove_child_section(&parent, &label);
}
}
})
@ -204,6 +220,26 @@ impl DebugSection {
children,
values,
widget,
parent,
}
}
fn remove_child_section(parent: &WeakDynamic<DebugSection>, label: &str) {
if let Some(parent) = parent.upgrade() {
let parent = parent.lock();
let mut children = parent.children.lock();
if let Some(index) = children.iter().enumerate().find_map(|(index, child)| {
child.map_ref(|child| child.label == label).then_some(index)
}) {
children.remove_by_index(index);
}
}
}
}
#[test]
fn empty_child_clears_on_drop() {
let root = DebugContext::default();
drop(root.section("child"));
assert!(root.is_empty());
}

View file

@ -60,6 +60,30 @@ impl<T> Dynamic<T> {
WeakDynamic::from(self)
}
/// Returns the number [`Dynamic`]s that point to this same value.
///
/// The returned count includes `self`.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
#[must_use]
pub fn instances(&self) -> usize {
Arc::strong_count(&self.0) - self.readers()
}
/// Returns the number of [`DynamicReader`]s for this value.
///
/// # Panics
///
/// This function panics if this value is already locked by the current
/// thread.
#[must_use]
pub fn readers(&self) -> usize {
self.state().expect("deadlocked").readers
}
/// Returns a new dynamic that has its contents linked with `self` by the
/// pair of mapping functions provided.
///
@ -2801,3 +2825,32 @@ fn compare_swap() {
assert_eq!(dynamic.compare_swap(&2, 0), Ok(2));
assert_eq!(dynamic.get(), 0);
}
#[test]
fn ref_counts() {
let dynamic = Dynamic::new(1);
assert_eq!(dynamic.instances(), 1);
let second = dynamic.clone();
assert_eq!(dynamic.instances(), 2);
assert_eq!(dynamic.readers(), 0);
let reader = second.into_reader();
assert_eq!(dynamic.instances(), 1);
assert_eq!(dynamic.readers(), 1);
// Test that once the last instance is dropped that the reader is no longer
// connected and that on_disconnect gets invoked.
assert!(reader.connected());
let invoked = Dynamic::new(false);
reader.on_disconnect({
let invoked = invoked.clone();
move || {
invoked.set(true);
}
});
drop(dynamic);
assert!(invoked.get());
assert!(!reader.connected());
}