pub fn snapshot_zone<R>(f: impl FnOnce() -> R) -> R {
#[cfg(debug_assertions)]
{
imp::DEPTH.with(|d| d.set(d.get() + 1));
let _guard = crate::reactive::guard::defer(|| {
imp::DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
});
f()
}
#[cfg(not(debug_assertions))]
{
f()
}
}
#[cfg_attr(debug_assertions, track_caller)]
#[inline]
pub(crate) fn check_reactive_scope() {
#[cfg(debug_assertions)]
imp::check();
}
pub(crate) fn non_finite_value(id: crate::tree::WidgetId, property: &'static str) {
#[cfg(debug_assertions)]
imp::non_finite(id, property);
#[cfg(not(debug_assertions))]
let _ = (id, property);
}
#[track_caller]
pub(crate) fn clock_outside_its_pass(read: &'static str, owner: &'static str, other: &'static str) {
#[cfg(debug_assertions)]
imp::clock_outside_its_pass(read, owner, other);
#[cfg(not(debug_assertions))]
let _ = (read, owner, other);
}
#[cfg(debug_assertions)]
mod imp {
use std::cell::{Cell, RefCell};
use rustc_hash::FxHashSet;
thread_local! {
pub(super) static DEPTH: Cell<u32> = const { Cell::new(0) };
static REPORTED: RefCell<FxHashSet<(&'static str, u32, u32)>> =
RefCell::new(FxHashSet::default());
pub(super) static REPORTS: Cell<u64> = const { Cell::new(0) };
}
#[track_caller]
pub(super) fn check() {
if DEPTH.with(|d| d.get()) > 0 {
return;
}
if crate::reactive::invalidation::widget_tracking_active()
|| crate::reactive::runtime::effect_tracking_active()
{
return;
}
let loc = std::panic::Location::caller();
let key = (loc.file(), loc.line(), loc.column());
let first_time = REPORTED.with(|r| r.borrow_mut().insert(key));
if !first_time {
return;
}
report(format!(
"{}:{}:{}: signal read with no reactive scope — this value is a \
snapshot and will not update. Pass a closure instead (e.g. \
`move || …` rather than the value it computes), or use \
`get_untracked()`/`with_untracked()` if a snapshot is what you \
meant. (debug builds only)",
loc.file(),
loc.line(),
loc.column(),
));
}
fn report(message: String) {
REPORTS.with(|c| c.set(c.get() + 1));
if log::max_level() == log::LevelFilter::Off {
eprintln!("guido: {message}");
} else {
log::warn!("{message}");
}
}
thread_local! {
static NON_FINITE: RefCell<FxHashSet<(crate::tree::WidgetId, &'static str)>> =
RefCell::new(FxHashSet::default());
}
pub(super) fn non_finite(id: crate::tree::WidgetId, property: &'static str) {
if !NON_FINITE.with(|r| r.borrow_mut().insert((id, property))) {
return;
}
report(format!(
"widget {id:?}: `{property}` resolved to a value that is not a \
finite number, so it was read as if it had not been declared. A \
division by a measured zero is the usual cause. (debug builds only)"
));
}
#[track_caller]
pub(super) fn clock_outside_its_pass(
read: &'static str,
owner: &'static str,
other: &'static str,
) {
let loc = std::panic::Location::caller();
if !CLOCKS.with(|r| {
r.borrow_mut()
.insert((loc.file(), loc.line(), loc.column()))
}) {
return;
}
report(format!(
"{}:{}:{}: `{read}()` was read outside {owner}, so it answered \
with the wall clock rather than the moment asked for. Either \
the read belongs inside {owner}, or the question was \
`{other}()`. (debug builds only)",
loc.file(),
loc.line(),
loc.column(),
));
}
thread_local! {
static CLOCKS: RefCell<FxHashSet<(&'static str, u32, u32)>> =
RefCell::new(FxHashSet::default());
}
pub(crate) fn reset() {
CLOCKS.with(|r| r.borrow_mut().clear());
NON_FINITE.with(|r| r.borrow_mut().clear());
REPORTED.with(|r| r.borrow_mut().clear());
DEPTH.with(|d| d.set(0));
REPORTS.with(|c| c.set(0));
}
}
#[cfg(debug_assertions)]
pub(crate) fn reset() {
imp::reset();
}
#[cfg(not(debug_assertions))]
pub(crate) fn reset() {}
#[cfg(all(debug_assertions, test))]
pub(crate) fn report_count() -> u64 {
imp::REPORTS.with(|c| c.get())
}
#[cfg(all(test, debug_assertions))]
mod tests {
use super::*;
use crate::jobs::JobType;
use crate::reactive::invalidation::with_signal_tracking;
use crate::reactive::{create_signal, create_stored};
use crate::tree::WidgetId;
fn reports_of(f: impl FnOnce()) -> u64 {
let before = report_count();
f();
report_count() - before
}
#[test]
fn a_read_with_no_scope_is_reported() {
let count = create_signal(0);
assert_eq!(
reports_of(|| {
let _ = count.get();
}),
1
);
}
#[test]
fn legitimate_reads_are_not_reported() {
let count = create_signal(0);
let stored = create_stored(7);
let wid = WidgetId::from_u64(9001);
assert_eq!(
reports_of(|| with_signal_tracking(wid, JobType::Paint, || {
let _ = count.get();
})),
0
);
assert_eq!(
reports_of(|| {
let _ = count.get_untracked();
}),
0
);
assert_eq!(reports_of(|| count.with_untracked(|_| ())), 0);
assert_eq!(
reports_of(|| snapshot_zone(|| {
let _ = count.get();
})),
0
);
assert_eq!(
reports_of(|| {
let _ = stored.get();
}),
0
);
assert_eq!(
reports_of(|| crate::reactive::create_effect(move || {
let _ = count.get();
})),
0
);
}
#[test]
fn a_call_site_is_reported_once() {
let count = create_signal(0);
assert_eq!(
reports_of(|| {
for _ in 0..50 {
let _ = count.get();
}
}),
1
);
}
}