Skip to content

Commit

Permalink
Better debugging for ReactiveScopes (#307)
Browse files Browse the repository at this point in the history
* Store ReactiveScope creation Location

* DebugScopeHierarchy

* Add track_caller attributes

* Add more track_callers

* Attach all attributes except doc comments to function instead of struct
  • Loading branch information
lukechu10 authored Nov 30, 2021
1 parent 4663c7c commit 16ff0a9
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 12 deletions.
17 changes: 15 additions & 2 deletions packages/sycamore-macro/src/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,23 @@ pub fn component_impl(
arg,
mut generics,
vis,
attrs,
mut attrs,
name,
return_type,
} = component;

let mut doc_attrs = Vec::new();
let mut i = 0;
while i < attrs.len() {
if attrs[i].path.is_ident("doc") {
// Attribute is a doc attribute. Remove from attrs and add to doc_attrs.
let at = attrs.remove(i);
doc_attrs.push(at);
} else {
i += 1;
}
}

let prop_ty = match &arg {
FnArg::Receiver(_) => unreachable!(),
FnArg::Typed(pat_ty) => &pat_ty.ty,
Expand Down Expand Up @@ -226,7 +238,7 @@ pub fn component_impl(
}

let quoted = quote! {
#(#attrs)*
#(#doc_attrs)*
#vis struct #component_name#generics {
#[doc(hidden)]
_marker: ::std::marker::PhantomData<(#phantom_generics)>,
Expand All @@ -239,6 +251,7 @@ pub fn component_impl(
const NAME: &'static ::std::primitive::str = #component_name_str;
type Props = #prop_ty;

#(#attrs)*
fn create_component(#arg) -> #return_type{
#block
}
Expand Down
2 changes: 1 addition & 1 deletion packages/sycamore-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ mod view;
/// A macro for ergonomically creating complex UI structures.
///
/// To learn more about the template syntax, see the chapter on
/// [the `view!` macro](https://sycamore-rs.netlify.app/docs/basics/template) in the Sycamore Book.
/// [the `view!` macro](https://sycamore-rs.netlify.app/docs/basics/view) in the Sycamore Book.
#[proc_macro]
pub fn view(component: TokenStream) -> TokenStream {
let component = parse_macro_input!(component as view::HtmlRoot);
Expand Down
27 changes: 24 additions & 3 deletions packages/sycamore-reactive/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@ pub(super) trait ContextAny {
/// Get the value stored in the context. The concrete type of the returned value is guaranteed
/// to match the type when calling [`get_type_id`](ContextAny::get_type_id).
fn get_value(&self) -> &dyn Any;

/// Get the name of type of context or `None` if not available.
fn get_type_name(&self) -> Option<&'static str>;
}

/// Inner representation of a context.
struct Context<T: 'static> {
value: T,
/// The type name of the context. Only available in debug mode.
#[cfg(debug_assertions)]
type_name: &'static str,
}

impl<T: 'static> ContextAny for Context<T> {
Expand All @@ -31,6 +37,13 @@ impl<T: 'static> ContextAny for Context<T> {
fn get_value(&self) -> &dyn Any {
&self.value
}

fn get_type_name(&self) -> Option<&'static str> {
#[cfg(debug_assertions)]
return Some(self.type_name);
#[cfg(not(debug_assertions))]
return None;
}
}

/// Get the value of a context in the current [`ReactiveScope`] or `None` if not found.
Expand Down Expand Up @@ -68,11 +81,19 @@ pub fn use_context<T: Clone + 'static>() -> T {
}

/// Creates a new [`ReactiveScope`] with a context and runs the supplied callback function.
#[cfg_attr(debug_assertions, track_caller)]
pub fn create_context_scope<T: 'static, Out>(value: T, f: impl FnOnce() -> Out) -> Out {
// Create a new ReactiveScope.
// We make sure to create the ReactiveScope outside of the closure so that track_caller can do
// its thing.
let scope = ReactiveScope::new();
SCOPES.with(|scopes| {
// Create a new ReactiveScope with a context.
let scope = ReactiveScope::new();
scope.0.borrow_mut().context = Some(Box::new(Context { value }));
// Attach the context to the scope.
scope.0.borrow_mut().context = Some(Box::new(Context {
value,
#[cfg(debug_assertions)]
type_name: std::any::type_name::<T>(),
}));
scopes.borrow_mut().push(scope);
let out = f();
let scope = scopes.borrow_mut().pop().unwrap_throw();
Expand Down
98 changes: 95 additions & 3 deletions packages/sycamore-reactive/src/effect.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::cell::RefCell;
use std::fmt::{Debug, Formatter};
use std::future::Future;
use std::hash::{Hash, Hasher};
use std::panic::Location;
use std::rc::{Rc, Weak};
use std::{mem, ptr};

Expand Down Expand Up @@ -57,7 +59,6 @@ impl Listener {
}

/// Internal representation for [`ReactiveScope`].
#[derive(Default)]
pub(crate) struct ReactiveScopeInner {
/// Effects created in this scope.
effects: SmallVec<[Rc<RefCell<Option<Listener>>>; REACTIVE_SCOPE_EFFECTS_STACK_CAPACITY]>,
Expand All @@ -66,6 +67,31 @@ pub(crate) struct ReactiveScopeInner {
/// Contexts created in this scope.
pub context: Option<Box<dyn ContextAny>>,
pub parent: ReactiveScopeWeak,
/// The source location where this scope was created.
/// Only available when in debug mode.
#[cfg(debug_assertions)]
pub loc: &'static Location<'static>,
}

impl ReactiveScopeInner {
#[cfg_attr(debug_assertions, track_caller)]
pub fn new() -> Self {
Self {
effects: SmallVec::new(),
cleanup: Vec::new(),
context: None,
parent: ReactiveScopeWeak::default(),
#[cfg(debug_assertions)]
loc: Location::caller(),
}
}
}

impl Default for ReactiveScopeInner {
#[cfg_attr(debug_assertions, track_caller)]
fn default() -> Self {
Self::new()
}
}

/// Owns the effects created in the current reactive scope.
Expand All @@ -75,15 +101,17 @@ pub(crate) struct ReactiveScopeInner {
/// A new [`ReactiveScope`] is usually created with [`create_root`]. A new [`ReactiveScope`] is also
/// created when a new effect is created with [`create_effect`] and other reactive utilities that
/// call it under the hood.
#[derive(Default)]
pub struct ReactiveScope(pub(crate) Rc<RefCell<ReactiveScopeInner>>);

impl ReactiveScope {
/// Create a new empty [`ReactiveScope`].
///
/// This should be rarely used and only serve as a placeholder.
#[cfg_attr(debug_assertions, track_caller)]
pub fn new() -> Self {
Self::default()
// We call this first to make sure that track_caller can do its thing.
let inner = ReactiveScopeInner::new();
Self(Rc::new(RefCell::new(inner)))
}

/// Add an effect that is owned by this [`ReactiveScope`].
Expand Down Expand Up @@ -126,6 +154,21 @@ impl ReactiveScope {
});
u
}

/// Returns the source code [`Location`] where this [`ReactiveScope`] was created.
pub fn creation_loc(&self) -> Option<&'static Location<'static>> {
#[cfg(debug_assertions)]
return Some(self.0.borrow().loc);
#[cfg(not(debug_assertions))]
return None;
}
}

impl Default for ReactiveScope {
#[cfg_attr(debug_assertions, track_caller)]
fn default() -> Self {
Self::new()
}
}

impl Drop for ReactiveScope {
Expand Down Expand Up @@ -621,6 +664,55 @@ pub fn current_scope() -> Option<ReactiveScopeWeak> {
})
}

/// A struct that can be debug-printed to view the scope hierarchy at the location it was created.
pub struct DebugScopeHierarchy {
scope: Option<Rc<RefCell<ReactiveScopeInner>>>,
loc: &'static Location<'static>,
}

/// Returns a [`DebugScopeHierarchy`] which can be printed using [`std::fmt::Debug`] to debug the
/// scope hierarchy at the current level.
#[track_caller]
pub fn debug_scope_hierarchy() -> DebugScopeHierarchy {
let loc = Location::caller();
SCOPES.with(|scope| DebugScopeHierarchy {
scope: scope.borrow().last().map(|x| x.0.clone()),
loc,
})
}

impl Debug for DebugScopeHierarchy {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Reactive scope hierarchy at {}:", self.loc)?;
if let Some(scope) = &self.scope {
let mut s = Some(scope.clone());
while let Some(x) = s {
// Print scope.
if let Some(loc) = ReactiveScope(x.clone()).creation_loc() {
write!(f, "\tScope created at {}", loc)?;
} else {
write!(f, "\tScope")?;
}
// Print context.
if let Some(context) = &x.borrow().context {
let type_name = context.get_type_name();
if let Some(type_name) = type_name {
write!(f, " with context (type = {})", type_name)?;
} else {
write!(f, " with context")?;
}
}
writeln!(f)?;
// Set next iteration with scope parent.
s = x.borrow().parent.0.upgrade();
}
} else {
writeln!(f, "Not inside a reactive scope")?;
}
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
10 changes: 7 additions & 3 deletions packages/sycamore-reactive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,23 @@ pub fn create_child_scope_in<'a>(
/// ```
/// TODO: deprecate this method in favor of [`create_scope`].
#[must_use = "create_root returns the reactive scope of the effects created inside this scope"]
#[cfg_attr(debug_assertions, track_caller)]
pub fn create_root<'a>(callback: impl FnOnce() + 'a) -> ReactiveScope {
_create_child_scope_in(None, Box::new(callback))
}

/// Internal implementation: use dynamic dispatch to reduce code bloat.
#[cfg_attr(debug_assertions, track_caller)]
fn _create_child_scope_in<'a>(
parent: Option<&ReactiveScopeWeak>,
callback: Box<dyn FnOnce() + 'a>,
) -> ReactiveScope {
SCOPES.with(|scopes| {
// Push new empty scope on the stack.
let scope = ReactiveScope::new();
// Push new empty scope on the stack.
// We make sure to create the ReactiveScope outside of the closure so that track_caller can do
// its thing.
let scope = ReactiveScope::new();

SCOPES.with(|scopes| {
// If `parent` was specified, use it as the parent of the new scope. Else use the parent of
// the scope this function is called in.
if let Some(parent) = parent {
Expand Down
1 change: 1 addition & 0 deletions packages/sycamore/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ where
/// # }
/// ```
#[component(ContextProvider<G>)]
#[cfg_attr(debug_assertions, track_caller)]
pub fn context_provider<T, F>(props: ContextProviderProps<T, F, G>) -> View<G>
where
T: 'static,
Expand Down

0 comments on commit 16ff0a9

Please sign in to comment.