use alloc::vec::Vec;
use core::panic::Location;
use crate::reactor::State;
use crate::stats::GraphStats;
use crate::{NodeId, NodeKind, Reactor, ReactorId};
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum NodeState {
Clean,
Check,
Dirty,
}
impl From<State> for NodeState {
fn from(state: State) -> Self {
match state {
State::Clean => Self::Clean,
State::Check => Self::Check,
State::Dirty => Self::Dirty,
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GraphNode {
pub id: NodeId,
pub kind: NodeKind,
pub origin: &'static Location<'static>,
pub version: u64,
pub state: Option<NodeState>,
pub dependencies: usize,
pub observers: usize,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GraphEdge {
pub observer: NodeId,
pub observable: NodeId,
pub version: u64,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RecordedDependency {
pub node: NodeId,
pub version: u64,
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GraphSnapshot {
pub reactor: ReactorId,
pub nodes: Vec<GraphNode>,
pub edges: Vec<GraphEdge>,
pub stats: GraphStats,
pub ownership: crate::OwnershipStats,
}
impl GraphSnapshot {
pub fn node(&self, id: NodeId) -> Option<&GraphNode> {
if let Ok(index) = self.nodes.binary_search_by_key(&id, |node| node.id) {
return Some(&self.nodes[index]);
}
self.nodes.iter().find(|node| node.id == id)
}
pub fn stale(&self) -> impl Iterator<Item = &GraphNode> {
self.nodes
.iter()
.filter(|node| !matches!(node.state, None | Some(NodeState::Clean)))
}
}
impl Reactor {
pub fn is_observed(&self, node: NodeId) -> bool {
self.observer_count(node) > 0
}
pub fn observer_count(&self, node: NodeId) -> usize {
self.inner
.dependents
.borrow()
.get(&node)
.map_or(0, |observers| observers.len())
}
#[must_use = "walking the whole graph and discarding the result inspects nothing"]
pub fn graph_snapshot(&self) -> GraphSnapshot {
let meta = self.inner.meta.borrow();
let dependencies = self.inner.dependencies.borrow();
let dependents = self.inner.dependents.borrow();
let observers = self.inner.observers.borrow();
let mut nodes = Vec::with_capacity(meta.len());
let mut edges = Vec::new();
for (id, entry) in meta.iter() {
let outgoing = dependencies.get(id);
if let Some(outgoing) = outgoing {
edges.extend(outgoing.iter().map(|(observable, version)| GraphEdge {
observer: *id,
observable: *observable,
version: *version,
}));
}
nodes.push(GraphNode {
id: *id,
kind: entry.kind,
origin: entry.origin,
version: entry.version,
state: observers
.get(id)
.and_then(|weak| weak.upgrade())
.map(|hook| hook.state().into()),
dependencies: outgoing.map_or(0, hashbrown::HashMap::len),
observers: dependents.get(id).map_or(0, hashbrown::HashSet::len),
});
}
nodes.sort_unstable_by_key(|node| node.id);
edges.sort_unstable();
drop((meta, dependencies, dependents, observers));
GraphSnapshot {
reactor: self.inner.id,
nodes,
edges,
stats: self.graph_stats(),
ownership: crate::ownership_stats(),
}
}
pub fn dependency_count(&self, node: NodeId) -> usize {
self.inner
.dependencies
.borrow()
.get(&node)
.map_or(0, hashbrown::HashMap::len)
}
pub fn observers_of(&self, node: NodeId) -> Vec<NodeId> {
self.inner
.dependents
.borrow()
.get(&node)
.map(|observers| observers.iter().copied().collect())
.unwrap_or_default()
}
pub fn dependencies_of(&self, node: NodeId) -> Vec<RecordedDependency> {
self.inner
.dependencies
.borrow()
.get(&node)
.map(|edges| {
edges
.iter()
.map(|(id, version)| RecordedDependency {
node: *id,
version: *version,
})
.collect()
})
.unwrap_or_default()
}
pub fn node_state(&self, node: NodeId) -> Option<NodeState> {
self.inner
.observers
.borrow()
.get(&node)
.and_then(|weak| weak.upgrade())
.map(|hook| hook.state().into())
}
pub fn node_origin(&self, node: NodeId) -> Option<&'static Location<'static>> {
self.inner.meta.borrow().get(&node).map(|meta| meta.origin)
}
pub fn node_kind(&self, node: NodeId) -> Option<NodeKind> {
self.inner.meta.borrow().get(&node).map(|meta| meta.kind)
}
pub fn node_version(&self, node: NodeId) -> Option<u64> {
self.inner.meta.borrow().get(&node).map(|meta| meta.version)
}
#[must_use = "this reads counters and asserts nothing; the result is the whole point"]
pub fn graph_stats(&self) -> GraphStats {
self.inner.counters.snapshot(
self.inner.id,
self.inner.meta.borrow().len(),
self.inner.observed_nodes.get(),
self.inner.pending_jobs.borrow().len(),
self.inner.flush_depth.get(),
self.inner.flush_epoch.get(),
)
}
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use crate::{NodeKind, NodeState, Reactor, memo_in, signal_in, source_in};
#[test]
fn a_snapshot_describes_the_whole_graph_and_is_ordered() {
let reactor = Reactor::new();
let left = signal_in(&reactor, 1_u32);
let right = signal_in(&reactor, 2_u32);
let total = memo_in(&reactor, {
let left = left.clone();
let right = right.clone();
move || left.get() + right.get()
});
let effect = reactor.effect({
let total = total.clone();
move || {
let _ = total.get();
}
});
reactor.flush_now();
let snapshot = reactor.graph_snapshot();
assert_eq!(snapshot.reactor, reactor.id());
assert_eq!(snapshot.nodes.len(), 4);
assert_eq!(snapshot.stats.live_nodes, 4);
let ids = snapshot
.nodes
.iter()
.map(|node| node.id)
.collect::<Vec<_>>();
let mut sorted = ids.clone();
sorted.sort_unstable();
assert_eq!(
ids, sorted,
"nodes must be ordered so snapshots can be diffed"
);
let memo = snapshot.node(total.id()).expect("the memo is live");
assert_eq!(memo.kind, NodeKind::Memo);
assert_eq!(memo.dependencies, 2);
assert_eq!(memo.observers, 1);
assert_eq!(memo.state, Some(NodeState::Clean));
assert!(memo.origin.file().ends_with("inspect.rs"));
assert_eq!(snapshot.node(left.id()).expect("live").state, None);
assert_eq!(snapshot.edges.len(), 3);
assert_eq!(snapshot.stats.live_edges, 3);
assert!(
snapshot
.edges
.iter()
.any(|edge| edge.observer == total.id() && edge.observable == left.id())
);
effect.dispose();
}
#[test]
fn staleness_is_reported_without_being_resolved() {
let reactor = Reactor::new();
let source = signal_in(&reactor, 1_u32);
let doubled = memo_in(&reactor, {
let source = source.clone();
move || source.get() * 2
});
assert_eq!(doubled.get(), 2);
assert_eq!(reactor.graph_snapshot().stale().count(), 0);
source.set(5);
for _ in 0..2 {
let snapshot = reactor.graph_snapshot();
let stale = snapshot.stale().collect::<Vec<_>>();
assert_eq!(stale.len(), 1);
assert_eq!(stale[0].id, doubled.id());
assert_eq!(stale[0].state, Some(NodeState::Dirty));
}
assert_eq!(doubled.get(), 10);
assert_eq!(reactor.graph_snapshot().stale().count(), 0);
}
#[test]
fn a_check_marked_node_is_distinguishable_from_a_dirty_one() {
let reactor = Reactor::new();
let source = signal_in(&reactor, 1_u32);
let first = memo_in(&reactor, {
let source = source.clone();
move || source.get() + 1
});
let second = memo_in(&reactor, {
let first = first.clone();
move || first.get() + 1
});
assert_eq!(second.get(), 3);
source.set(2);
let snapshot = reactor.graph_snapshot();
assert_eq!(
snapshot.node(first.id()).expect("live").state,
Some(NodeState::Dirty)
);
assert_eq!(
snapshot.node(second.id()).expect("live").state,
Some(NodeState::Check)
);
assert_eq!(snapshot.stale().count(), 2);
let id = second.id();
drop(second);
assert!(reactor.graph_snapshot().node(id).is_none());
}
#[test]
fn an_invalidated_effect_reports_as_stale_before_it_runs() {
let reactor = Reactor::new();
let source = signal_in(&reactor, 0_u32);
let effect = reactor.effect({
let source = source.clone();
move || {
let _ = source.get();
}
});
reactor.flush_now();
assert_eq!(reactor.node_state(effect.id()), Some(NodeState::Clean));
assert_eq!(reactor.graph_snapshot().stale().count(), 0);
source.set(1);
assert_eq!(
reactor.node_state(effect.id()),
Some(NodeState::Dirty),
"the effect is invalidated and has not run yet"
);
assert!(
reactor
.graph_snapshot()
.stale()
.any(|node| node.id == effect.id())
);
reactor.flush_now();
assert_eq!(reactor.node_state(effect.id()), Some(NodeState::Clean));
effect.dispose();
assert_eq!(
reactor.node_state(effect.id()),
None,
"a node that has left the graph has no state to report"
);
}
#[test]
fn a_graph_nobody_flushes_is_visible_as_a_graph_nobody_flushes() {
let application = Reactor::new();
let stranded = Reactor::new();
let held = signal_in(&stranded, 0_u32);
let effect = application.effect(|| {});
application.flush_now();
let orphan = stranded.graph_snapshot();
assert_eq!(orphan.nodes.len(), 1);
assert_eq!(orphan.edges.len(), 0);
assert_eq!(orphan.stats.observed_nodes, 0);
assert_eq!(
orphan.stats.flushes, 0,
"the telltale: this graph has never been flushed"
);
assert!(
application.graph_stats().flushes > 0,
"the application's graph did flush, which is what makes the orphan's zero mean \
something"
);
drop(held);
effect.dispose();
}
#[test]
fn an_unobserved_source_still_appears() {
let reactor = Reactor::new();
let node = source_in(&reactor);
let snapshot = reactor.graph_snapshot();
assert_eq!(snapshot.nodes.len(), 1);
assert_eq!(
snapshot.node(node.id()).expect("live").kind,
NodeKind::Source
);
assert_eq!(snapshot.node(node.id()).expect("live").observers, 0);
assert!(snapshot.edges.is_empty());
}
}