use alloc::boxed::Box;
use alloc::collections::VecDeque;
use alloc::rc::{Rc, Weak};
use alloc::vec::Vec;
use core::cell::{Cell, RefCell};
use core::error::Error;
use core::fmt;
use core::panic::Location;
use core::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use hashbrown::{HashMap, HashSet};
use runite::queue_microtask;
use crate::stats::{FlushAccounting, FlushStats, GraphCounters};
use crate::{
DiagnosticEvent, DiagnosticSubscription, InvalidationCause, InvalidationLevel, NodeId,
NodeKind, ReactorId, trace_targets,
};
type Job = Box<dyn FnOnce() + 'static>;
type DiagnosticCallback = Rc<dyn Fn(DiagnosticEvent)>;
static NEXT_REACTOR_ID: AtomicU64 = AtomicU64::new(1);
thread_local! {
static CURRENT_REACTOR: RefCell<Weak<ReactorInner>> = const { RefCell::new(Weak::new()) };
static ANCHORED_REACTOR: RefCell<Option<Rc<ReactorInner>>> = const { RefCell::new(None) };
static HAS_HAD_DEFAULT: Cell<bool> = const { Cell::new(false) };
static UNTRACKED_DEPTH: Cell<u32> = const { Cell::new(0) };
}
#[cfg(debug_assertions)]
thread_local! {
static RUNNING_REACTOR: Cell<*const ()> = const { Cell::new(core::ptr::null()) };
}
pub fn current() -> Reactor {
Reactor::current()
}
pub fn try_current() -> Option<Reactor> {
Reactor::try_current()
}
pub struct EnterGuard {
previous_default: Weak<ReactorInner>,
previous_anchor: Option<Rc<ReactorInner>>,
}
impl fmt::Debug for EnterGuard {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EnterGuard").finish_non_exhaustive()
}
}
impl Drop for EnterGuard {
fn drop(&mut self) {
let _ = CURRENT_REACTOR
.try_with(|slot| slot.replace(core::mem::take(&mut self.previous_default)));
let _ = ANCHORED_REACTOR.try_with(|slot| slot.replace(self.previous_anchor.take()));
tracing::debug!(
target: trace_targets::GRAPH,
event = "reactor_exit",
"restored the previous thread default reactor"
);
}
}
pub fn untrack<T>(f: impl FnOnce() -> T) -> T {
let entered = UNTRACKED_DEPTH
.try_with(|depth| depth.set(depth.get() + 1))
.is_ok();
struct Guard {
entered: bool,
}
impl Drop for Guard {
fn drop(&mut self) {
if !self.entered {
return;
}
let _ = UNTRACKED_DEPTH.try_with(|depth| depth.set(depth.get().saturating_sub(1)));
}
}
let _guard = Guard { entered };
f()
}
fn is_untracked() -> bool {
UNTRACKED_DEPTH
.try_with(|depth| depth.get() > 0)
.unwrap_or(false)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Mark {
Check,
Dirty,
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) enum State {
Clean,
Check,
Dirty,
}
impl From<Mark> for State {
fn from(mark: Mark) -> Self {
match mark {
Mark::Check => State::Check,
Mark::Dirty => State::Dirty,
}
}
}
impl From<Mark> for InvalidationLevel {
fn from(mark: Mark) -> Self {
match mark {
Mark::Check => Self::Check,
Mark::Dirty => Self::Dirty,
}
}
}
pub(crate) struct DirtyOnUnwind<'a> {
pub(crate) state: &'a Cell<State>,
pub(crate) armed: bool,
}
impl Drop for DirtyOnUnwind<'_> {
fn drop(&mut self) {
if self.armed {
self.state.set(State::Dirty);
}
}
}
pub(crate) trait ObserverHook {
fn mark(&self, mark: Mark, cause: Option<InvalidationCause>);
fn refresh(&self) {}
fn state(&self) -> State;
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReactCycleError {
cycle: Vec<NodeId>,
origins: Vec<Option<&'static Location<'static>>>,
}
impl ReactCycleError {
fn new(cycle: Vec<NodeId>, origins: Vec<Option<&'static Location<'static>>>) -> Self {
Self { cycle, origins }
}
pub fn cycle(&self) -> &[NodeId] {
&self.cycle
}
pub fn origins(&self) -> &[Option<&'static Location<'static>>] {
&self.origins
}
}
impl fmt::Display for ReactCycleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "reactive cycle detected: ")?;
for (index, node) in self.cycle.iter().enumerate() {
if index != 0 {
write!(f, " -> ")?;
}
write!(f, "{node}")?;
if let Some(origin) = self.origins.get(index).copied().flatten() {
write!(f, " (created at {origin})")?;
}
}
Ok(())
}
}
impl Error for ReactCycleError {}
#[derive(Clone)]
pub struct Reactor {
pub(crate) inner: Rc<ReactorInner>,
}
impl Reactor {
pub fn new() -> Self {
let reactor = Self {
inner: Rc::new(ReactorInner::new()),
};
tracing::debug!(
target: trace_targets::GRAPH,
event = "reactor_new",
"created reactive reactor"
);
reactor
}
pub fn subscribe_diagnostics(
&self,
callback: impl Fn(DiagnosticEvent) + 'static,
) -> DiagnosticSubscription {
let token = self.inner.next_diagnostic.get();
self.inner.next_diagnostic.set(token.wrapping_add(1).max(1));
self.inner
.diagnostics
.borrow_mut()
.push((token, Rc::new(callback)));
self.inner.diagnostics_active.set(true);
let inner = Rc::clone(&self.inner);
DiagnosticSubscription::new(move || {
inner
.diagnostics
.borrow_mut()
.retain(|(candidate, _)| *candidate != token);
let still_active = !inner.diagnostics.borrow().is_empty();
inner.diagnostics_active.set(still_active);
if !still_active {
inner.flushes.reset();
}
})
}
pub fn current() -> Self {
if let Some(reactor) = Self::try_current() {
#[cfg(debug_assertions)]
tracing::trace!(
target: trace_targets::GRAPH,
event = "current_reactor_reuse",
"reusing current thread default reactor"
);
return reactor;
}
let reactor = Self::new();
let installed = CURRENT_REACTOR
.try_with(|slot| slot.replace(Rc::downgrade(&reactor.inner)))
.is_ok();
let had_default = HAS_HAD_DEFAULT
.try_with(|flag| flag.replace(true))
.unwrap_or(false);
if installed && had_default {
tracing::warn!(
target: trace_targets::GRAPH,
event = "current_reactor_reinstall",
reactor_id = reactor.inner.id.get(),
"this thread had a default reactor earlier and has none now, so a fresh, \
unrelated one was installed; nodes created from here on are on a separate graph \
from the ones created before. Hold the reactor alive with Reactor::enter for as \
long as ambient constructors may run, create state with reactor.signal(..) and \
friends, or use try_current to make the absence an error"
);
} else {
tracing::debug!(
target: trace_targets::GRAPH,
event = "current_reactor_install",
"installed current thread default reactor"
);
}
reactor
}
pub fn try_current() -> Option<Self> {
CURRENT_REACTOR
.try_with(|r| r.borrow().upgrade())
.ok()
.flatten()
.map(|inner| Self { inner })
}
#[must_use = "the reactor is the thread default only while the guard is held; \
`reactor.enter();` in statement position installs nothing"]
pub fn enter(&self) -> EnterGuard {
let previous_default = CURRENT_REACTOR
.try_with(|slot| slot.replace(Rc::downgrade(&self.inner)))
.unwrap_or_default();
let previous_anchor = ANCHORED_REACTOR
.try_with(|slot| slot.replace(Some(Rc::clone(&self.inner))))
.unwrap_or_default();
let _ = HAS_HAD_DEFAULT.try_with(|flag| flag.set(true));
tracing::debug!(
target: trace_targets::GRAPH,
event = "reactor_enter",
reactor_id = self.inner.id.get(),
"entered reactor as the thread default"
);
EnterGuard {
previous_default,
previous_anchor,
}
}
pub fn run_in_context<T>(&self, observer: NodeId, f: impl FnOnce() -> T) -> T {
let _span = tracing::debug_span!(
target: trace_targets::GRAPH,
"reactor.run_in_context",
observer_id = observer.0
)
.entered();
let inserted = self.inner.active_computations.borrow_mut().insert(observer);
assert!(
inserted,
"adaptite: a reactive computation re-entered itself, which cannot be tracked \
coherently — the inner run would clear the dependencies the outer run is still \
recording. This usually means a computation, or a callback it invoked, wrote state \
it depends on and then forced a synchronous flush"
);
self.clear_observer_dependencies(observer, true);
self.inner.stack.borrow_mut().push(observer);
let previous_untracked = UNTRACKED_DEPTH.try_with(|depth| depth.replace(0)).ok();
#[cfg(debug_assertions)]
let previous_running = RUNNING_REACTOR
.try_with(|running| running.replace(Rc::as_ptr(&self.inner).cast::<()>()))
.ok();
struct Guard<'a> {
inner: &'a ReactorInner,
previous_untracked: Option<u32>,
#[cfg(debug_assertions)]
previous_running: Option<*const ()>,
}
impl Drop for Guard<'_> {
fn drop(&mut self) {
let popped = self.inner.stack.borrow_mut().pop();
debug_assert!(popped.is_some(), "reactor observer stack underflow");
if let Some(node) = popped {
let removed = self.inner.active_computations.borrow_mut().remove(&node);
debug_assert!(removed, "observer should have been active");
}
if let Some(previous) = self.previous_untracked {
let _ = UNTRACKED_DEPTH.try_with(|depth| depth.set(previous));
}
#[cfg(debug_assertions)]
if let Some(previous) = self.previous_running {
let _ = RUNNING_REACTOR.try_with(|running| running.set(previous));
}
}
}
let _guard = Guard {
inner: &self.inner,
previous_untracked,
#[cfg(debug_assertions)]
previous_running,
};
f()
}
pub(crate) fn cycle_check(&self, observable: NodeId) -> Result<(), ReactCycleError> {
if self
.inner
.active_computations
.borrow()
.contains(&observable)
{
let stack = self.inner.stack.borrow();
let start = stack
.iter()
.position(|node| *node == observable)
.expect("active computation should appear in observer stack");
let mut cycle = stack[start..].to_vec();
cycle.push(observable);
let origins = cycle.iter().map(|node| self.node_origin(*node)).collect();
tracing::debug!(
target: trace_targets::GRAPH,
event = "cycle_detected",
observable_id = observable.0,
cycle_len = cycle.len(),
"reactive cycle detected"
);
return Err(ReactCycleError::new(cycle, origins));
}
Ok(())
}
pub(crate) fn assert_no_cycle(&self, observable: NodeId) {
if let Err(e) = self.cycle_check(observable) {
panic!("{e}");
}
}
pub fn try_observe(&self, observable: NodeId) -> Result<(), ReactCycleError> {
self.cycle_check(observable)?;
if is_untracked() {
return Ok(());
}
#[cfg(debug_assertions)]
self.assert_running_reactor();
let current = self.inner.stack.borrow().last().copied();
let Some(observer) = current else {
return Ok(());
};
#[cfg(debug_assertions)]
tracing::trace!(
target: trace_targets::GRAPH,
event = "observe",
observer_id = observer.0,
observable_id = observable.0,
"recording reactive dependency"
);
let existing = self
.inner
.dependencies
.borrow_mut()
.entry(observer)
.or_default()
.insert(observable, self.version(observable));
if existing.is_none() {
self.inner.counters.edge_added();
self.record_flush(|stats| stats.edges_added = stats.edges_added.saturating_add(1));
}
let became_observed = {
let mut dependents = self.inner.dependents.borrow_mut();
let observers = dependents.entry(observable).or_default();
let was_unobserved = observers.is_empty();
observers.insert(observer);
was_unobserved
};
if became_observed {
self.inner
.observed_nodes
.set(self.inner.observed_nodes.get() + 1);
}
if became_observed {
self.note_observation_change(observable, true);
}
Ok(())
}
pub fn observe(&self, observable: NodeId) {
if let Err(e) = self.try_observe(observable) {
panic!("{e}");
}
}
#[track_caller]
pub fn trigger(&self, observable: NodeId) {
self.bump_version(observable);
let write_origin = Location::caller();
let cause = if self.diagnostics_enabled() {
self.node_origin(observable)
.map(|node_origin| InvalidationCause {
node: observable,
version: self.version(observable),
node_origin,
write_origin,
})
} else {
None
};
if let Some(cause) = cause {
self.inner.emit(DiagnosticEvent::ReactiveWrite {
reactor: self.inner.id,
kind: self.node_kind(observable).unwrap_or(NodeKind::Source),
cause,
});
self.record_flush(|stats| stats.root_writes = stats.root_writes.saturating_add(1));
}
self.mark_dependents(observable, Mark::Dirty, cause);
}
pub(crate) fn mark_dependents(
&self,
observable: NodeId,
mark: Mark,
cause: Option<InvalidationCause>,
) {
let mut dependents;
{
let map = self.inner.dependents.borrow();
let Some(nodes) = map.get(&observable).filter(|nodes| !nodes.is_empty()) else {
return;
};
dependents = self.take_node_buffer();
dependents.extend(nodes.iter().copied());
}
#[cfg(debug_assertions)]
tracing::trace!(
target: trace_targets::GRAPH,
event = "mark_dependents",
observable_id = observable.0,
dependent_count = dependents.len(),
?mark,
"marking reactive dependents"
);
if !self.inner.diagnostics_active.get() {
for &dependent in &dependents {
self.deliver_mark(dependent, mark, cause);
}
self.give_node_buffer(dependents);
return;
}
let depth = self.inner.mark_depth.get() + 1;
self.inner.mark_depth.set(depth);
struct DepthGuard<'a>(&'a Cell<u32>);
impl Drop for DepthGuard<'_> {
fn drop(&mut self) {
self.0.set(self.0.get().saturating_sub(1));
}
}
let _depth_guard = DepthGuard(&self.inner.mark_depth);
for &dependent in &dependents {
if self.deliver_mark(dependent, mark, cause) {
self.record_flush(|stats| {
match mark {
Mark::Check => {
stats.nodes_marked_check = stats.nodes_marked_check.saturating_add(1);
}
Mark::Dirty => {
stats.nodes_marked_dirty = stats.nodes_marked_dirty.saturating_add(1);
}
}
stats.max_propagation_depth = stats.max_propagation_depth.max(depth);
});
}
}
self.give_node_buffer(dependents);
}
fn deliver_mark(
&self,
dependent: NodeId,
mark: Mark,
cause: Option<InvalidationCause>,
) -> bool {
let hook = self
.inner
.observers
.borrow()
.get(&dependent)
.cloned()
.and_then(|weak| weak.upgrade());
match hook {
Some(hook) => {
hook.mark(mark, cause);
true
}
None => {
self.inner.observers.borrow_mut().remove(&dependent);
false
}
}
}
pub(crate) fn refresh_node(&self, node: NodeId) {
self.assert_no_cycle(node);
let hook = self
.inner
.observers
.borrow()
.get(&node)
.cloned()
.and_then(|weak| weak.upgrade());
if let Some(hook) = hook {
hook.refresh();
}
}
pub(crate) fn dependencies_changed(&self, observer: NodeId) -> bool {
let mut recorded = self.take_edge_buffer();
if let Some(edges) = self.inner.dependencies.borrow().get(&observer) {
recorded.extend(
edges
.iter()
.map(|(node, version)| crate::RecordedDependency {
node: *node,
version: *version,
}),
);
}
let mut changed = false;
for &entry in &recorded {
self.refresh_node(entry.node);
if self.version(entry.node) != entry.version {
changed = true;
break;
}
}
self.give_edge_buffer(recorded);
changed
}
pub fn dispose(&self, node: NodeId) {
tracing::debug!(
target: trace_targets::GRAPH,
event = "dispose_node",
node_id = node.0,
"disposing reactive node bookkeeping"
);
let edges_held = if self.diagnostics_enabled() {
Some((
self.inner
.dependencies
.borrow()
.get(&node)
.map_or(0, hashbrown::HashMap::len),
self.inner
.dependents
.borrow()
.get(&node)
.map_or(0, hashbrown::HashSet::len),
))
} else {
None
};
self.clear_observer_dependencies(node, false);
let removed_dependents = self.inner.dependents.borrow_mut().remove(&node);
if removed_dependents
.as_ref()
.is_some_and(|nodes| !nodes.is_empty())
{
self.inner
.observed_nodes
.set(self.inner.observed_nodes.get().saturating_sub(1));
}
let incoming = removed_dependents
.map(|nodes| nodes.into_iter().collect::<Vec<_>>())
.unwrap_or_default();
self.inner.counters.edges_removed(incoming.len());
let removed = incoming.len();
self.record_flush(|stats| {
stats.edges_removed = stats.edges_removed.saturating_add(removed as u32);
});
for observer in incoming {
let mut dependencies = self.inner.dependencies.borrow_mut();
if let Some(observed) = dependencies.get_mut(&observer) {
observed.remove(&node);
if observed.is_empty() {
dependencies.remove(&observer);
}
}
}
self.inner.observers.borrow_mut().remove(&node);
let removed = self.inner.meta.borrow_mut().remove(&node);
self.unregister_observation_hooks(node);
let Some(meta) = removed else {
return;
};
self.inner.counters.node_disposed(meta.kind);
if let Some((dependencies, dependents)) = edges_held {
self.inner.emit(DiagnosticEvent::NodeDisposed {
reactor: self.inner.id,
node,
kind: meta.kind,
origin: meta.origin,
dependencies,
observers: dependents,
});
}
}
pub fn schedule(&self, job: impl FnOnce() + 'static) {
let pending = {
let mut jobs = self.inner.pending_jobs.borrow_mut();
jobs.push_back(Box::new(job));
jobs.len()
};
self.inner.counters.job_queued(pending);
#[cfg(debug_assertions)]
tracing::trace!(
target: trace_targets::GRAPH,
event = "schedule_job",
pending_jobs = self.inner.pending_jobs.borrow().len(),
"queued reactive job for microtask flush"
);
self.inner.ensure_flush_scheduled();
}
pub fn flush_now(&self) {
Rc::clone(&self.inner).flush_jobs();
}
pub fn external_flush<T>(&self, f: impl FnOnce() -> T) -> T {
struct Guard<'a>(&'a ReactorInner);
impl Drop for Guard<'_> {
fn drop(&mut self) {
self.0.end_flush();
}
}
let _guard = Guard(&self.inner);
self.inner.begin_flush();
f()
}
pub(crate) fn in_flush(&self) -> bool {
self.inner.flush_depth.get() > 0
}
pub(crate) fn begin_flush(&self) {
self.inner.begin_flush();
}
pub(crate) fn end_flush(&self) {
self.inner.end_flush();
}
#[track_caller]
pub(crate) fn allocate_node(&self, kind: NodeKind) -> NodeId {
let raw = self.inner.next_node.get();
self.inner.next_node.set(raw.wrapping_add(1));
let id = NodeId::new(raw);
let origin = Location::caller();
self.inner.meta.borrow_mut().insert(
id,
NodeMeta {
version: 0,
origin,
kind,
},
);
self.inner
.counters
.node_created(kind, self.inner.meta.borrow().len());
#[cfg(debug_assertions)]
tracing::trace!(
target: trace_targets::GRAPH,
event = "allocate_node",
node_id = id.0,
?kind,
"allocated reactive node id"
);
if self.diagnostics_enabled() {
self.inner.emit(DiagnosticEvent::NodeCreated {
reactor: self.inner.id,
node: id,
kind,
origin,
});
}
id
}
pub(crate) fn register_observer(&self, id: NodeId, observer: Rc<dyn ObserverHook>) {
self.inner
.observers
.borrow_mut()
.insert(id, Rc::downgrade(&observer));
}
pub(crate) fn unregister_observer(&self, id: NodeId) {
self.inner.observers.borrow_mut().remove(&id);
}
pub(crate) fn bump_version(&self, node: NodeId) {
if let Some(meta) = self.inner.meta.borrow_mut().get_mut(&node) {
meta.version = meta.version.wrapping_add(1);
}
}
pub(crate) fn version(&self, node: NodeId) -> u64 {
self.node_version(node).unwrap_or(0)
}
pub(crate) fn drain_epoch(&self) -> u64 {
self.inner.drain_epoch.get()
}
pub(crate) fn flush_epoch(&self) -> u64 {
self.inner.flush_epoch.get()
}
pub(crate) fn emit_diagnostic(&self, event: DiagnosticEvent) {
self.inner.emit(event);
}
#[inline]
pub(crate) fn record_flush(&self, f: impl FnOnce(&mut FlushStats)) {
if self.inner.diagnostics_active.get() {
self.inner.flushes.record(f);
}
}
pub(crate) fn is_computation_active(&self, observer: NodeId) -> bool {
self.inner.active_computations.borrow().contains(&observer)
}
pub(crate) fn counters(&self) -> &GraphCounters {
&self.inner.counters
}
#[cfg(test)]
pub(crate) fn walk_observed_nodes(&self) -> usize {
self.inner
.dependents
.borrow()
.values()
.filter(|observers| !observers.is_empty())
.count()
}
#[cfg(test)]
pub(crate) fn node_scratch_pool_len(&self) -> usize {
self.inner.node_scratch.borrow().len()
}
#[cfg(test)]
pub(crate) fn walk_edge_counts(&self) -> (usize, usize) {
let outgoing = self
.inner
.dependencies
.borrow()
.values()
.map(HashMap::len)
.sum();
let incoming = self
.inner
.dependents
.borrow()
.values()
.map(HashSet::len)
.sum();
(outgoing, incoming)
}
pub(crate) fn diagnostics_enabled(&self) -> bool {
self.inner.diagnostics_active.get()
}
pub fn id(&self) -> ReactorId {
self.inner.id
}
pub(crate) fn diagnostic_id(&self) -> ReactorId {
self.inner.id
}
#[cfg(debug_assertions)]
fn assert_running_reactor(&self) {
let _ = RUNNING_REACTOR.try_with(|running| {
let running = running.get();
if !running.is_null() && running != Rc::as_ptr(&self.inner).cast::<()>() {
panic!(
"adaptite: a node belonging to one reactor was read inside a computation \
running in a different reactor on the same thread; this dependency cannot \
be tracked and the observer will not re-run when the node changes"
);
}
});
}
fn clear_observer_dependencies(&self, observer: NodeId, retain_table: bool) {
let mut observed;
{
let mut map = self.inner.dependencies.borrow_mut();
let Some(edges) = map.get_mut(&observer) else {
return;
};
if edges.is_empty() {
if !retain_table {
map.remove(&observer);
}
return;
}
observed = self.take_node_buffer();
observed.extend(edges.keys().copied());
if retain_table {
edges.clear();
} else {
map.remove(&observer);
}
}
self.inner.counters.edges_removed(observed.len());
let removed = observed.len();
self.record_flush(|stats| {
stats.edges_removed = stats.edges_removed.saturating_add(removed as u32);
});
let mut unobserved = self.take_node_buffer();
for &observable in &observed {
let mut dependents = self.inner.dependents.borrow_mut();
if let Some(observers) = dependents.get_mut(&observable) {
observers.remove(&observer);
if observers.is_empty() {
self.inner
.observed_nodes
.set(self.inner.observed_nodes.get().saturating_sub(1));
unobserved.push(observable);
}
}
}
self.give_node_buffer(observed);
for &observable in &unobserved {
self.note_observation_change(observable, false);
}
self.give_node_buffer(unobserved);
}
fn take_node_buffer(&self) -> Vec<NodeId> {
self.inner
.node_scratch
.borrow_mut()
.pop()
.unwrap_or_default()
}
fn give_node_buffer(&self, mut buffer: Vec<NodeId>) {
buffer.clear();
let mut pool = self.inner.node_scratch.borrow_mut();
if pool.len() < SCRATCH_POOL_LIMIT {
pool.push(buffer);
}
}
fn take_edge_buffer(&self) -> Vec<crate::RecordedDependency> {
self.inner
.edge_scratch
.borrow_mut()
.pop()
.unwrap_or_default()
}
fn give_edge_buffer(&self, mut buffer: Vec<crate::RecordedDependency>) {
buffer.clear();
let mut pool = self.inner.edge_scratch.borrow_mut();
if pool.len() < SCRATCH_POOL_LIMIT {
pool.push(buffer);
}
}
fn note_observation_change(&self, node: NodeId, observed: bool) {
let hooks = self.inner.observation_hooks.borrow().get(&node).cloned();
let Some(hooks) = hooks else {
return;
};
hooks.observed.set(observed);
if hooks.queued.replace(true) {
return;
}
self.schedule(move || {
hooks.queued.set(false);
if hooks.cancelled.get() {
return;
}
let observed = hooks.observed.get();
if hooks.delivered.replace(observed) == observed {
return;
}
if observed {
(hooks.on_watch)();
} else {
(hooks.on_unwatch)();
}
});
}
pub(crate) fn register_observation_hooks(
&self,
node: NodeId,
on_watch: impl Fn() + 'static,
on_unwatch: impl Fn() + 'static,
) {
self.inner.observation_hooks.borrow_mut().insert(
node,
Rc::new(ObservationHooks {
on_watch: Box::new(on_watch),
on_unwatch: Box::new(on_unwatch),
observed: Cell::new(false),
delivered: Cell::new(false),
queued: Cell::new(false),
cancelled: Cell::new(false),
}),
);
}
pub(crate) fn unregister_observation_hooks(&self, node: NodeId) {
if let Some(hooks) = self.inner.observation_hooks.borrow_mut().remove(&node) {
hooks.cancelled.set(true);
}
}
}
struct ObservationHooks {
on_watch: Box<dyn Fn()>,
on_unwatch: Box<dyn Fn()>,
observed: Cell<bool>,
delivered: Cell<bool>,
queued: Cell<bool>,
cancelled: Cell<bool>,
}
impl Default for Reactor {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for Reactor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Reactor")
.field("ptr", &Rc::as_ptr(&self.inner))
.finish()
}
}
pub(crate) struct NodeMeta {
pub(crate) version: u64,
pub(crate) origin: &'static Location<'static>,
pub(crate) kind: NodeKind,
}
const SCRATCH_POOL_LIMIT: usize = 512;
pub(crate) struct ReactorInner {
pub(crate) id: ReactorId,
next_node: Cell<u64>,
pub(crate) meta: RefCell<HashMap<NodeId, NodeMeta>>,
pub(crate) dependencies: RefCell<HashMap<NodeId, HashMap<NodeId, u64>>>,
pub(crate) dependents: RefCell<HashMap<NodeId, HashSet<NodeId>>>,
pub(crate) observed_nodes: Cell<usize>,
node_scratch: RefCell<Vec<Vec<NodeId>>>,
edge_scratch: RefCell<Vec<Vec<crate::RecordedDependency>>>,
pub(crate) observers: RefCell<HashMap<NodeId, Weak<dyn ObserverHook>>>,
stack: RefCell<Vec<NodeId>>,
active_computations: RefCell<HashSet<NodeId>>,
pub(crate) pending_jobs: RefCell<VecDeque<Job>>,
flush_scheduled: Cell<bool>,
pub(crate) flush_epoch: Cell<u64>,
open_flush_epoch: Cell<u64>,
pub(crate) drain_epoch: Cell<u64>,
pub(crate) flush_depth: Cell<u32>,
next_diagnostic: Cell<u64>,
diagnostics_active: Cell<bool>,
diagnostics: RefCell<Vec<(u64, DiagnosticCallback)>>,
observation_hooks: RefCell<HashMap<NodeId, Rc<ObservationHooks>>>,
pub(crate) counters: GraphCounters,
flushes: FlushAccounting,
mark_depth: Cell<u32>,
}
impl ReactorInner {
fn new() -> Self {
Self {
id: ReactorId(NEXT_REACTOR_ID.fetch_add(1, AtomicOrdering::Relaxed)),
next_node: Cell::new(1),
meta: RefCell::new(HashMap::new()),
dependencies: RefCell::new(HashMap::new()),
dependents: RefCell::new(HashMap::new()),
observed_nodes: Cell::new(0),
node_scratch: RefCell::new(Vec::new()),
edge_scratch: RefCell::new(Vec::new()),
observers: RefCell::new(HashMap::new()),
stack: RefCell::new(Vec::new()),
active_computations: RefCell::new(HashSet::new()),
pending_jobs: RefCell::new(VecDeque::new()),
flush_scheduled: Cell::new(false),
flush_epoch: Cell::new(0),
flush_depth: Cell::new(0),
next_diagnostic: Cell::new(1),
diagnostics_active: Cell::new(false),
diagnostics: RefCell::new(Vec::new()),
observation_hooks: RefCell::new(HashMap::new()),
counters: GraphCounters::default(),
open_flush_epoch: Cell::new(0),
drain_epoch: Cell::new(0),
flushes: FlushAccounting::default(),
mark_depth: Cell::new(0),
}
}
fn emit_from_drop(&self, event: DiagnosticEvent) {
if !self.diagnostics_active.get() {
return;
}
if !std::thread::panicking() {
self.emit(event);
return;
}
let delivered = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.emit(event)));
if delivered.is_err() {
tracing::error!(
target: trace_targets::GRAPH,
event = "diagnostic_subscriber_panic_during_unwind",
"a diagnostic subscriber panicked while the thread was already unwinding; the \
event was dropped so the original panic could propagate instead of aborting"
);
}
}
fn emit(&self, event: DiagnosticEvent) {
if !self.diagnostics_active.get() {
return;
}
let callbacks = self.diagnostics.borrow();
if callbacks.len() == 1 {
let callback = Rc::clone(&callbacks[0].1);
drop(callbacks);
callback(event);
return;
}
let snapshot = callbacks
.iter()
.map(|(_, callback)| Rc::clone(callback))
.collect::<Vec<_>>();
drop(callbacks);
for callback in snapshot {
callback(event);
}
}
fn begin_flush(&self) {
let depth = self.flush_depth.get();
self.flush_depth.set(depth + 1);
if depth > 0 {
return;
}
self.drain_epoch.set(self.drain_epoch.get().wrapping_add(1));
self.flush_epoch.set(self.flush_epoch.get().wrapping_add(1));
self.counters.flush_opened();
let epoch = self.flush_epoch.get();
self.open_flush_epoch.set(epoch);
if self.diagnostics_active.get() {
self.flushes.open_flush(self.pending_jobs.borrow().len());
}
#[cfg(debug_assertions)]
tracing::trace!(
target: trace_targets::GRAPH,
event = "begin_external_flush",
flush_epoch = epoch,
"opened an externally driven flush"
);
if self.diagnostics_active.get() {
let pending_jobs = self.pending_jobs.borrow().len();
self.emit(DiagnosticEvent::FlushStarted {
reactor: self.id,
flush_epoch: epoch,
pending_jobs,
});
}
}
fn end_flush(&self) {
let depth = self.flush_depth.get().saturating_sub(1);
self.flush_depth.set(depth);
if depth > 0 {
return;
}
if self.diagnostics_active.get() {
let remaining_jobs = self.pending_jobs.borrow().len();
let Some(stats) = self
.flushes
.close_flush(remaining_jobs, self.counters.queued_effects())
else {
return;
};
self.emit_from_drop(DiagnosticEvent::FlushFinished {
reactor: self.id,
flush_epoch: self.open_flush_epoch.get(),
remaining_jobs,
stats,
});
}
}
fn ensure_flush_scheduled(self: &Rc<Self>) {
if self.flush_scheduled.replace(true) {
return;
}
#[cfg(debug_assertions)]
tracing::trace!(
target: trace_targets::GRAPH,
event = "schedule_flush",
pending_jobs = self.pending_jobs.borrow().len(),
"scheduling reactive microtask flush"
);
let reactor = Rc::clone(self);
queue_microtask(move || {
reactor.flush_jobs();
});
}
fn flush_jobs(self: Rc<Self>) {
if self.pending_jobs.borrow().is_empty() {
self.flush_scheduled.set(false);
return;
}
let _span = tracing::debug_span!(
target: trace_targets::GRAPH,
"reactor.flush_jobs"
)
.entered();
if self.flush_depth.get() == 0 {
self.drain_epoch.set(self.drain_epoch.get().wrapping_add(1));
}
self.flush_epoch.set(self.flush_epoch.get().wrapping_add(1));
self.counters.flush_opened();
let epoch = self.flush_epoch.get();
if self.diagnostics_active.get() {
self.flushes.open_flush(self.pending_jobs.borrow().len());
}
if self.diagnostics_active.get() {
let pending_jobs = self.pending_jobs.borrow().len();
self.emit(DiagnosticEvent::FlushStarted {
reactor: self.id,
flush_epoch: epoch,
pending_jobs,
});
}
struct FlushGuard {
inner: Rc<ReactorInner>,
epoch: u64,
}
impl Drop for FlushGuard {
fn drop(&mut self) {
self.inner
.flush_depth
.set(self.inner.flush_depth.get().saturating_sub(1));
if self.inner.diagnostics_active.get() {
let remaining_jobs = self.inner.pending_jobs.borrow().len();
if let Some(stats) = self
.inner
.flushes
.close_flush(remaining_jobs, self.inner.counters.queued_effects())
{
self.inner.emit_from_drop(DiagnosticEvent::FlushFinished {
reactor: self.inner.id,
flush_epoch: self.epoch,
remaining_jobs,
stats,
});
}
}
self.inner.flush_scheduled.set(false);
if !self.inner.pending_jobs.borrow().is_empty() {
self.inner.ensure_flush_scheduled();
}
}
}
self.flush_depth.set(self.flush_depth.get() + 1);
let guard = FlushGuard {
inner: Rc::clone(&self),
epoch,
};
loop {
let job = guard.inner.pending_jobs.borrow_mut().pop_front();
let Some(job) = job else {
break;
};
#[cfg(debug_assertions)]
tracing::trace!(
target: trace_targets::GRAPH,
event = "run_job",
remaining_jobs = guard.inner.pending_jobs.borrow().len(),
"running reactive scheduled job"
);
job();
}
}
}
#[cfg(test)]
mod tests;