mirror of
https://github.com/Zenithsiz/dynatos.git
synced 2026-02-03 10:05:32 +00:00
Effect/Trigger dependencies/subscribers are now stored in a global graph.
This commit is contained in:
parent
868b105f20
commit
19166944c8
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -7,6 +7,7 @@
|
||||
"itertools",
|
||||
"loadables",
|
||||
"metavar",
|
||||
"petgraph",
|
||||
"popstate",
|
||||
"qself",
|
||||
"scopeguard",
|
||||
|
||||
36
Cargo.lock
generated
36
Cargo.lock
generated
@ -17,6 +17,12 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
@ -242,6 +248,7 @@ dependencies = [
|
||||
"futures",
|
||||
"itertools",
|
||||
"parking_lot",
|
||||
"petgraph",
|
||||
"pin-project",
|
||||
"priority-queue",
|
||||
"scopeguard",
|
||||
@ -320,6 +327,18 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
@ -429,6 +448,11 @@ name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@ -691,6 +715,18 @@ version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"hashbrown",
|
||||
"indexmap",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.10"
|
||||
|
||||
@ -47,6 +47,7 @@ futures = "0.3.31"
|
||||
itertools = "0.14.0"
|
||||
js-sys = "0.3.77"
|
||||
parking_lot = "0.12.3"
|
||||
petgraph = "0.8.2"
|
||||
pin-project = "1.1.10"
|
||||
priority-queue = "2.3.1"
|
||||
proc-macro2 = "1.0.94"
|
||||
|
||||
@ -14,6 +14,7 @@ extend = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
petgraph = { workspace = true }
|
||||
pin-project = { workspace = true }
|
||||
priority-queue = { workspace = true }
|
||||
scopeguard = { workspace = true }
|
||||
|
||||
217
dynatos-reactive/src/dep_graph.rs
Normal file
217
dynatos-reactive/src/dep_graph.rs
Normal file
@ -0,0 +1,217 @@
|
||||
//! Dependency graph
|
||||
|
||||
// Imports
|
||||
#[cfg(debug_assertions)]
|
||||
use core::panic::Location;
|
||||
use {
|
||||
crate::{Effect, EffectRun, Trigger, WeakEffect, WeakTrigger},
|
||||
core::cell::{LazyCell, RefCell},
|
||||
petgraph::prelude::{NodeIndex, StableGraph},
|
||||
std::collections::HashMap,
|
||||
};
|
||||
|
||||
/// Dependency graph
|
||||
#[thread_local]
|
||||
static DEP_GRAPH: LazyCell<RefCell<DepGraph>> = LazyCell::new(|| RefCell::new(DepGraph::new()));
|
||||
|
||||
/// Effect dependency info
|
||||
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
|
||||
pub struct EffectDepInfo {
|
||||
/// Location this dependency was gathered
|
||||
#[cfg(debug_assertions)]
|
||||
pub gathered_loc: &'static Location<'static>,
|
||||
}
|
||||
|
||||
/// Effect subscriber info
|
||||
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
|
||||
pub struct EffectSubInfo {
|
||||
/// Location this subscriber was executed
|
||||
#[cfg(debug_assertions)]
|
||||
pub exec_loc: &'static Location<'static>,
|
||||
}
|
||||
|
||||
/// Graph node
|
||||
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
|
||||
enum Node {
|
||||
/// Trigger
|
||||
Trigger(WeakTrigger),
|
||||
|
||||
/// Effect
|
||||
Effect(WeakEffect),
|
||||
}
|
||||
|
||||
/// Graph edge
|
||||
// TODO: Make this a ZST in release mode?
|
||||
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
|
||||
enum Edge {
|
||||
/// Effect dependency
|
||||
EffectDep(EffectDepInfo),
|
||||
|
||||
/// Effect subscriber
|
||||
EffectSub(EffectSubInfo),
|
||||
}
|
||||
|
||||
impl Edge {
|
||||
/// Creates an effect dependency edge
|
||||
#[track_caller]
|
||||
pub const fn effect_dep() -> Self {
|
||||
Self::EffectDep(EffectDepInfo {
|
||||
#[cfg(debug_assertions)]
|
||||
gathered_loc: Location::caller(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates an effect subscriber edge
|
||||
pub const fn effect_sub(#[cfg(debug_assertions)] caller_loc: &'static Location<'static>) -> Self {
|
||||
Self::EffectSub(EffectSubInfo {
|
||||
#[cfg(debug_assertions)]
|
||||
exec_loc: caller_loc,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Dependency graph
|
||||
#[derive(Clone, Debug)]
|
||||
struct DepGraph {
|
||||
/// Nodes
|
||||
nodes: HashMap<Node, NodeIndex>,
|
||||
|
||||
/// Graph
|
||||
graph: StableGraph<Node, Edge>,
|
||||
}
|
||||
|
||||
impl DepGraph {
|
||||
/// Creates a new dependency graph
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nodes: HashMap::new(),
|
||||
graph: StableGraph::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the idx of a node, or creates it
|
||||
pub fn get_or_insert_node(&mut self, node: Node) -> NodeIndex {
|
||||
*self
|
||||
.nodes
|
||||
.entry(node)
|
||||
.or_insert_with_key(|node| self.graph.add_node(node.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears an effect's dependencies and subscribers
|
||||
pub fn clear_effect<F: ?Sized + EffectRun>(effect: &Effect<F>) {
|
||||
let mut dep_graph = DEP_GRAPH.borrow_mut();
|
||||
let Some(&effect_idx) = dep_graph.nodes.get(&Node::Effect(effect.downgrade().unsize())) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut deps = dep_graph.graph.neighbors_undirected(effect_idx).detach();
|
||||
while let Some(edge) = deps.next_edge(&dep_graph.graph) {
|
||||
dep_graph.graph.remove_edge(edge);
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses all subscribers of a trigger
|
||||
pub fn with_trigger_subs<F>(trigger: WeakTrigger, mut f: F)
|
||||
where
|
||||
F: FnMut(&WeakEffect, Vec<EffectDepInfo>),
|
||||
{
|
||||
let dep_graph = DEP_GRAPH.borrow();
|
||||
let Some(&trigger_idx) = dep_graph.nodes.get(&Node::Trigger(trigger)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for effect_idx in dep_graph
|
||||
.graph
|
||||
.neighbors_directed(trigger_idx, petgraph::Direction::Outgoing)
|
||||
{
|
||||
let effect = match &dep_graph.graph[effect_idx] {
|
||||
Node::Trigger(_) => unreachable!("Trigger had an outgoing edge to another trigger"),
|
||||
Node::Effect(effect) => effect,
|
||||
};
|
||||
|
||||
let effect_info = dep_graph
|
||||
.graph
|
||||
.edges_connecting(trigger_idx, effect_idx)
|
||||
.map(|edge| match edge.weight() {
|
||||
Edge::EffectDep(dep_info) => dep_info.clone(),
|
||||
Edge::EffectSub(_) => unreachable!("Trigger has an outgoing edge with effect subscriber info"),
|
||||
})
|
||||
.collect();
|
||||
|
||||
f(effect, effect_info);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds an effect dependency
|
||||
#[track_caller]
|
||||
pub fn add_effect_dep(effect: &Effect, trigger: &Trigger) {
|
||||
#[cfg(debug_assertions)]
|
||||
tracing::trace!(
|
||||
"Adding effect dependency\nEffect : {}\nTrigger : {}\nGathered: {}",
|
||||
effect.defined_loc(),
|
||||
trigger.defined_loc(),
|
||||
Location::caller(),
|
||||
);
|
||||
|
||||
let mut dep_graph = DEP_GRAPH.borrow_mut();
|
||||
|
||||
let effect_idx = dep_graph.get_or_insert_node(Node::Effect(effect.downgrade()));
|
||||
let trigger_idx = dep_graph.get_or_insert_node(Node::Trigger(trigger.downgrade()));
|
||||
|
||||
dep_graph.graph.add_edge(trigger_idx, effect_idx, Edge::effect_dep());
|
||||
}
|
||||
|
||||
/// Adds an effect subscriber
|
||||
pub fn add_effect_sub(
|
||||
effect: &Effect,
|
||||
trigger: &Trigger,
|
||||
#[cfg(debug_assertions)] caller_loc: &'static Location<'static>,
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
tracing::trace!(
|
||||
"Adding effect subscriber\nEffect : {}\nTrigger : {}\nExecuted: {}",
|
||||
effect.defined_loc(),
|
||||
trigger.defined_loc(),
|
||||
caller_loc,
|
||||
);
|
||||
|
||||
let mut dep_graph = DEP_GRAPH.borrow_mut();
|
||||
|
||||
let effect_idx = dep_graph.get_or_insert_node(Node::Effect(effect.downgrade()));
|
||||
let trigger_idx = dep_graph.get_or_insert_node(Node::Trigger(trigger.downgrade()));
|
||||
|
||||
dep_graph.graph.add_edge(
|
||||
effect_idx,
|
||||
trigger_idx,
|
||||
Edge::effect_sub(
|
||||
#[cfg(debug_assertions)]
|
||||
caller_loc,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Exports the dependency graph as a dot graph.
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn export_dot() -> String {
|
||||
let dep_graph = &DEP_GRAPH.borrow();
|
||||
let graph = dep_graph.graph.map(
|
||||
|_node_idx, node| match node {
|
||||
Node::Trigger(trigger) => match trigger.upgrade() {
|
||||
Some(trigger) => format!("Trigger({})", trigger.defined_loc()),
|
||||
None => "Trigger(<dropped>)".to_owned(),
|
||||
},
|
||||
Node::Effect(effect) => match effect.upgrade() {
|
||||
Some(effect) => format!("Effect({})", effect.defined_loc()),
|
||||
None => "Effect(<dropped>)".to_owned(),
|
||||
},
|
||||
},
|
||||
|_edge_idx, edge| match edge {
|
||||
Edge::EffectDep(info) => format!("Gather({})", info.gathered_loc),
|
||||
Edge::EffectSub(info) => format!("Exec({})", info.exec_loc),
|
||||
},
|
||||
);
|
||||
|
||||
petgraph::dot::Dot::new(&graph).to_string()
|
||||
}
|
||||
@ -24,16 +24,16 @@ pub use self::{
|
||||
#[cfg(debug_assertions)]
|
||||
use core::panic::Location;
|
||||
use {
|
||||
crate::{effect_stack, WeakTrigger},
|
||||
crate::{dep_graph, effect_stack},
|
||||
core::{
|
||||
cell::{Cell, RefCell},
|
||||
cell::Cell,
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
marker::Unsize,
|
||||
ops::{CoerceUnsized, Deref},
|
||||
ptr,
|
||||
},
|
||||
std::{collections::HashSet, rc::Rc},
|
||||
std::rc::Rc,
|
||||
};
|
||||
|
||||
/// Effect inner
|
||||
@ -46,12 +46,6 @@ pub struct Inner<F: ?Sized> {
|
||||
/// Where this effect was defined
|
||||
defined_loc: &'static Location<'static>,
|
||||
|
||||
/// All dependencies of this effect
|
||||
dependencies: RefCell<HashSet<WeakTrigger>>,
|
||||
|
||||
/// All subscribers to this effect
|
||||
subscribers: RefCell<HashSet<WeakTrigger>>,
|
||||
|
||||
/// Effect runner
|
||||
run: F,
|
||||
}
|
||||
@ -100,8 +94,6 @@ impl<F> Effect<F> {
|
||||
suppressed: Cell::new(false),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_loc: Location::caller(),
|
||||
dependencies: RefCell::new(HashSet::new()),
|
||||
subscribers: RefCell::new(HashSet::new()),
|
||||
run,
|
||||
};
|
||||
|
||||
@ -203,19 +195,10 @@ impl<F: ?Sized> Effect<F> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the dependencies before running
|
||||
#[expect(clippy::iter_over_hash_type, reason = "We don't care about the order here")]
|
||||
for dep in self.inner.dependencies.borrow_mut().drain() {
|
||||
let Some(trigger) = dep.upgrade() else { continue };
|
||||
trigger.remove_subscriber(&self.downgrade().unsize());
|
||||
}
|
||||
#[expect(clippy::iter_over_hash_type, reason = "We don't care about the order here")]
|
||||
for dep in self.inner.subscribers.borrow_mut().drain() {
|
||||
let Some(trigger) = dep.upgrade() else { continue };
|
||||
trigger.remove_dependency(&self.downgrade().unsize());
|
||||
}
|
||||
// Clear the dependencies/subscribers before running
|
||||
dep_graph::clear_effect(self);
|
||||
|
||||
// Otherwise, run it
|
||||
// Then run it
|
||||
let ctx = EffectRunCtx::new();
|
||||
let _gatherer = self.deps_gatherer();
|
||||
self.inner.run.run(ctx);
|
||||
@ -232,16 +215,6 @@ impl<F: ?Sized> Effect<F> {
|
||||
self.inner.suppressed.get()
|
||||
}
|
||||
|
||||
/// Adds a dependency to this effect
|
||||
pub(crate) fn add_dependency(&self, trigger: WeakTrigger) {
|
||||
self.inner.dependencies.borrow_mut().insert(trigger);
|
||||
}
|
||||
|
||||
/// Adds a subscriber to this effect
|
||||
pub(crate) fn add_subscriber(&self, trigger: WeakTrigger) {
|
||||
self.inner.subscribers.borrow_mut().insert(trigger);
|
||||
}
|
||||
|
||||
/// Formats this effect into `s`
|
||||
fn fmt_debug(&self, mut s: fmt::DebugStruct<'_, '_>) -> Result<(), fmt::Error> {
|
||||
s.field_with("inner", |f| fmt::Pointer::fmt(&self.inner_ptr(), f));
|
||||
@ -251,41 +224,6 @@ impl<F: ?Sized> Effect<F> {
|
||||
#[cfg(debug_assertions)]
|
||||
s.field_with("defined_loc", |f| fmt::Display::fmt(self.inner.defined_loc, f));
|
||||
|
||||
s.field_with("dependencies", |f| {
|
||||
Self::fmt_debug_trigger_set(f, &self.inner.dependencies)
|
||||
});
|
||||
s.field_with("subscribers", |f| {
|
||||
Self::fmt_debug_trigger_set(f, &self.inner.subscribers)
|
||||
});
|
||||
|
||||
s.finish()
|
||||
}
|
||||
|
||||
/// Formats a trigger hashset (dependencies / subscribers) into `f`.
|
||||
fn fmt_debug_trigger_set(
|
||||
f: &mut fmt::Formatter<'_>,
|
||||
set: &RefCell<HashSet<WeakTrigger>>,
|
||||
) -> Result<(), fmt::Error> {
|
||||
let mut s = f.debug_list();
|
||||
|
||||
let Ok(deps) = set.try_borrow() else {
|
||||
return s.finish_non_exhaustive();
|
||||
};
|
||||
|
||||
#[expect(clippy::iter_over_hash_type, reason = "We don't care about the order")]
|
||||
for dep in &*deps {
|
||||
let Some(trigger) = dep.upgrade() else {
|
||||
s.entry(&"<...>");
|
||||
continue;
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
s.entry_with(|f| fmt::Display::fmt(&trigger.defined_loc(), f));
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
s.entry_with(|f| fmt::Pointer::fmt(&trigger.inner_ptr(), f));
|
||||
}
|
||||
|
||||
s.finish()
|
||||
}
|
||||
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
|
||||
// Modules
|
||||
pub mod async_signal;
|
||||
pub mod dep_graph;
|
||||
pub mod derived;
|
||||
pub mod effect;
|
||||
pub mod effect_stack;
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
//! Run queue
|
||||
|
||||
// TODO: We should coordinate with the dependency graph to ensure we don't
|
||||
// run effects unnecessarily.
|
||||
|
||||
// Imports
|
||||
use {
|
||||
crate::{trigger::TriggerEffectInfo, WeakEffect},
|
||||
crate::{dep_graph::EffectDepInfo, WeakEffect},
|
||||
core::{
|
||||
cell::{LazyCell, RefCell},
|
||||
cmp::Reverse,
|
||||
@ -14,10 +17,11 @@ use {
|
||||
/// Inner item for the priority queue
|
||||
struct Item {
|
||||
/// Subscriber
|
||||
// TODO: Should the run queue use strong effects?
|
||||
subscriber: WeakEffect,
|
||||
|
||||
/// Info
|
||||
info: TriggerEffectInfo,
|
||||
info: Vec<EffectDepInfo>,
|
||||
}
|
||||
|
||||
impl PartialEq for Item {
|
||||
@ -104,7 +108,7 @@ pub fn dec_ref() -> Option<ExecGuard> {
|
||||
}
|
||||
|
||||
/// Pushes a subscriber to the queue.
|
||||
pub fn push(subscriber: WeakEffect, info: TriggerEffectInfo) {
|
||||
pub fn push(subscriber: WeakEffect, info: Vec<EffectDepInfo>) {
|
||||
let mut inner = RUN_QUEUE.borrow_mut();
|
||||
|
||||
let next = Reverse(inner.next);
|
||||
@ -113,7 +117,7 @@ pub fn push(subscriber: WeakEffect, info: TriggerEffectInfo) {
|
||||
}
|
||||
|
||||
/// Pops a subscriber from the front of the queue
|
||||
pub fn pop() -> Option<(WeakEffect, TriggerEffectInfo)> {
|
||||
pub fn pop() -> Option<(WeakEffect, Vec<EffectDepInfo>)> {
|
||||
let (item, _) = RUN_QUEUE.borrow_mut().queue.pop()?;
|
||||
Some((item.subscriber, item.info))
|
||||
}
|
||||
|
||||
@ -4,98 +4,21 @@
|
||||
//! any subscribers.
|
||||
|
||||
// Imports
|
||||
#[cfg(debug_assertions)]
|
||||
use core::panic::Location;
|
||||
use {
|
||||
crate::{effect, run_queue, WeakEffect},
|
||||
crate::{dep_graph, effect, run_queue},
|
||||
core::{
|
||||
cell::{LazyCell, RefCell},
|
||||
cell::LazyCell,
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
ptr,
|
||||
},
|
||||
std::{
|
||||
collections::{hash_map, HashMap},
|
||||
rc::{Rc, Weak},
|
||||
},
|
||||
std::rc::{Rc, Weak},
|
||||
};
|
||||
#[cfg(debug_assertions)]
|
||||
use {
|
||||
core::{iter, panic::Location},
|
||||
std::collections::HashSet,
|
||||
};
|
||||
|
||||
/// Subscriber / Dependency info
|
||||
// TODO: Make cloning this cheap by wrapping it in an `Arc` or something.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TriggerEffectInfo {
|
||||
#[cfg(debug_assertions)]
|
||||
/// Where this subscriber / dependency was defined
|
||||
defined_locs: HashSet<&'static Location<'static>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
not(debug_assertions),
|
||||
expect(
|
||||
clippy::missing_const_for_fn,
|
||||
reason = "It can't be a `const fn` with `debug_assertions`"
|
||||
)
|
||||
)]
|
||||
impl TriggerEffectInfo {
|
||||
/// Creates new info.
|
||||
#[track_caller]
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::new_with(
|
||||
#[cfg(debug_assertions)]
|
||||
Location::caller(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates new info with the caller location
|
||||
#[must_use]
|
||||
pub fn new_with(#[cfg(debug_assertions)] caller_loc: &'static Location<'static>) -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
defined_locs: iter::once(caller_loc).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates this info
|
||||
#[track_caller]
|
||||
pub fn update(&mut self) {
|
||||
self.update_with(
|
||||
#[cfg(debug_assertions)]
|
||||
Location::caller(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates this info with the caller location
|
||||
pub fn update_with(&mut self, #[cfg(debug_assertions)] caller_loc: &'static Location<'static>) {
|
||||
#[cfg(debug_assertions)]
|
||||
self.defined_locs.insert(caller_loc);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TriggerEffectInfo {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger inner
|
||||
#[cfg_attr(
|
||||
not(debug_assertions),
|
||||
expect(
|
||||
clippy::zero_sized_map_values,
|
||||
reason = "They aren't zero-sized with `debug_assertions`"
|
||||
)
|
||||
)]
|
||||
struct Inner {
|
||||
/// Dependencies
|
||||
dependencies: RefCell<HashMap<WeakEffect, TriggerEffectInfo>>,
|
||||
|
||||
/// Subscribers
|
||||
subscribers: RefCell<HashMap<WeakEffect, TriggerEffectInfo>>,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
/// Where this trigger was defined
|
||||
defined_loc: &'static Location<'static>,
|
||||
@ -112,18 +35,9 @@ impl Trigger {
|
||||
#[must_use]
|
||||
#[track_caller]
|
||||
pub fn new() -> Self {
|
||||
#[cfg_attr(
|
||||
not(debug_assertions),
|
||||
expect(
|
||||
clippy::zero_sized_map_values,
|
||||
reason = "They aren't zero-sized with `debug_assertions`"
|
||||
)
|
||||
)]
|
||||
let inner = Inner {
|
||||
dependencies: RefCell::new(HashMap::new()),
|
||||
subscribers: RefCell::new(HashMap::new()),
|
||||
#[cfg(debug_assertions)]
|
||||
defined_loc: Location::caller(),
|
||||
defined_loc: Location::caller(),
|
||||
};
|
||||
Self { inner: Rc::new(inner) }
|
||||
}
|
||||
@ -161,18 +75,7 @@ impl Trigger {
|
||||
#[track_caller]
|
||||
pub fn gather_subscribers(&self) {
|
||||
match effect::running() {
|
||||
Some(effect) => {
|
||||
#[cfg(debug_assertions)]
|
||||
tracing::trace!(
|
||||
"Adding effect dependency\nEffect : {}\nTrigger : {}\nGathered: {}",
|
||||
effect.defined_loc(),
|
||||
self.defined_loc(),
|
||||
Location::caller(),
|
||||
);
|
||||
|
||||
effect.add_dependency(self.downgrade());
|
||||
self.add_subscriber(effect.downgrade().unsize());
|
||||
},
|
||||
Some(effect) => dep_graph::add_effect_dep(&effect, self),
|
||||
|
||||
// TODO: Add some way to turn off this warning at a global
|
||||
// scale, with something like
|
||||
@ -194,69 +97,6 @@ impl Trigger {
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a subscriber to this trigger.
|
||||
///
|
||||
/// Returns if the subscriber already existed.
|
||||
#[track_caller]
|
||||
fn add_subscriber(&self, subscriber: WeakEffect) -> bool {
|
||||
let mut subscribers = self.inner.subscribers.borrow_mut();
|
||||
match (*subscribers).entry(subscriber) {
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
entry.get_mut().update();
|
||||
true
|
||||
},
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
entry.insert(TriggerEffectInfo::new());
|
||||
false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a dependency to this trigger.
|
||||
///
|
||||
/// Returns if the dependency already existed.
|
||||
fn add_dependency(
|
||||
&self,
|
||||
dependency: WeakEffect,
|
||||
#[cfg(debug_assertions)] caller_loc: &'static Location<'static>,
|
||||
) -> bool {
|
||||
let mut dependencies = self.inner.dependencies.borrow_mut();
|
||||
match (*dependencies).entry(dependency) {
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
entry.get_mut().update_with(
|
||||
#[cfg(debug_assertions)]
|
||||
caller_loc,
|
||||
);
|
||||
true
|
||||
},
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
entry.insert(TriggerEffectInfo::new_with(
|
||||
#[cfg(debug_assertions)]
|
||||
caller_loc,
|
||||
));
|
||||
false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a subscriber from this trigger.
|
||||
///
|
||||
/// Returns if the subscriber existed
|
||||
#[track_caller]
|
||||
pub(crate) fn remove_subscriber(&self, subscriber: &WeakEffect) -> bool {
|
||||
let mut subscribers = self.inner.subscribers.borrow_mut();
|
||||
subscribers.remove(subscriber).is_some()
|
||||
}
|
||||
|
||||
/// Removes a dependency from this trigger.
|
||||
///
|
||||
/// Returns if the dependency existed
|
||||
#[track_caller]
|
||||
pub(crate) fn remove_dependency(&self, dependency: &WeakEffect) -> bool {
|
||||
let mut dependencies = self.inner.dependencies.borrow_mut();
|
||||
dependencies.remove(dependency).is_some()
|
||||
}
|
||||
|
||||
/// Executes this trigger.
|
||||
///
|
||||
/// Adds all subscribers to the run queue, and once the returned
|
||||
@ -291,133 +131,47 @@ impl Trigger {
|
||||
pub(crate) fn exec_inner(&self, #[cfg(debug_assertions)] caller_loc: &'static Location<'static>) -> TriggerExec {
|
||||
// If there's a running effect, register it as our dependency
|
||||
if let Some(effect) = effect::running() {
|
||||
#[cfg(debug_assertions)]
|
||||
tracing::trace!(
|
||||
"Adding effect subscriber\nEffect : {}\nTrigger : {}\nExecuted: {}",
|
||||
effect.defined_loc(),
|
||||
self.defined_loc(),
|
||||
caller_loc,
|
||||
);
|
||||
|
||||
effect.add_subscriber(self.downgrade());
|
||||
self.add_dependency(
|
||||
effect.downgrade(),
|
||||
dep_graph::add_effect_sub(
|
||||
&effect,
|
||||
self,
|
||||
#[cfg(debug_assertions)]
|
||||
caller_loc,
|
||||
);
|
||||
}
|
||||
|
||||
let subscribers = self.inner.subscribers.borrow();
|
||||
|
||||
// Increase the ref count
|
||||
run_queue::inc_ref();
|
||||
|
||||
// Then all all of our subscribers
|
||||
// TODO: Should we care about the order? Randomizing it is probably good, since
|
||||
// it'll bring to the surface weird bugs or performance dependent on effect run order.
|
||||
#[expect(clippy::iter_over_hash_type, reason = "We don't care about which order they go in")]
|
||||
for (subscriber, info) in &*subscribers {
|
||||
// If the effect doesn't exist anymore, remove it
|
||||
// Then add all subscribers to the run queue
|
||||
dep_graph::with_trigger_subs(self.downgrade(), |subscriber, subscriber_info| {
|
||||
// If the effect doesn't exist anymore, skip it
|
||||
let Some(effect) = subscriber.upgrade() else {
|
||||
continue;
|
||||
return;
|
||||
};
|
||||
|
||||
// Skip suppressed effects
|
||||
if effect.is_suppressed() {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Should the run queue use strong effects?
|
||||
run_queue::push(subscriber.clone(), info.clone());
|
||||
}
|
||||
run_queue::push(effect.downgrade(), subscriber_info);
|
||||
});
|
||||
|
||||
TriggerExec {
|
||||
#[cfg(debug_assertions)]
|
||||
trigger_defined_loc: self.inner.defined_loc,
|
||||
trigger_defined_loc: self.defined_loc(),
|
||||
#[cfg(debug_assertions)]
|
||||
exec_defined_loc: caller_loc,
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats this trigger into `s`
|
||||
#[cfg_attr(
|
||||
not(debug_assertions),
|
||||
expect(clippy::unused_self, reason = "We use it in with `debug_assertions`")
|
||||
)]
|
||||
fn fmt_debug(&self, mut s: fmt::DebugStruct<'_, '_>) -> Result<(), fmt::Error> {
|
||||
s.field_with("inner", |f| fmt::Pointer::fmt(&self.inner_ptr(), f));
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
s.field_with("defined_loc", |f| fmt::Display::fmt(self.inner.defined_loc, f));
|
||||
|
||||
s.field_with("dependencies", |f| {
|
||||
Self::fmt_debug_effect_map(f, &self.inner.dependencies)
|
||||
});
|
||||
s.field_with("subscribers", |f| {
|
||||
Self::fmt_debug_effect_map(f, &self.inner.subscribers)
|
||||
});
|
||||
|
||||
s.finish()
|
||||
}
|
||||
|
||||
/// Formats an effect hashmap (dependencies / subscribers) into `f`.
|
||||
fn fmt_debug_effect_map(
|
||||
f: &mut fmt::Formatter<'_>,
|
||||
map: &RefCell<HashMap<WeakEffect, TriggerEffectInfo>>,
|
||||
) -> Result<(), fmt::Error> {
|
||||
#[cfg(debug_assertions)]
|
||||
let mut s = f.debug_map();
|
||||
#[cfg(not(debug_assertions))]
|
||||
let mut s = f.debug_list();
|
||||
|
||||
let Ok(deps) = map.try_borrow() else {
|
||||
return s.finish_non_exhaustive();
|
||||
};
|
||||
#[expect(clippy::iter_over_hash_type, reason = "We don't care about the order")]
|
||||
for (dep, info) in &*deps {
|
||||
#[cfg(not(debug_assertions))]
|
||||
let _ = info;
|
||||
|
||||
let Some(effect) = dep.upgrade() else {
|
||||
#[cfg(debug_assertions)]
|
||||
s.entry(&"<...>", info);
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
s.entry_with(|f| {
|
||||
fmt::Pointer::fmt(&dep.inner_ptr(), f)?;
|
||||
f.write_str(" (dead)")
|
||||
});
|
||||
|
||||
continue;
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
s.key_with(|f| fmt::Display::fmt(&effect.defined_loc(), f))
|
||||
.value_with(|f| Self::fmt_debug_effect_info(f, info));
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
s.entry_with(|f| fmt::Pointer::fmt(&effect.inner_ptr(), f));
|
||||
}
|
||||
|
||||
s.finish()
|
||||
}
|
||||
|
||||
/// Formats an effect info into `f`.
|
||||
#[cfg(debug_assertions)]
|
||||
fn fmt_debug_effect_info(f: &mut fmt::Formatter<'_>, info: &TriggerEffectInfo) -> Result<(), fmt::Error> {
|
||||
let mut s = f.debug_struct("Info");
|
||||
|
||||
s.field_with("defined_locs", |f| {
|
||||
let mut s = f.debug_list();
|
||||
|
||||
#[expect(clippy::iter_over_hash_type, reason = "We don't care about order for debugging")]
|
||||
for &loc in &info.defined_locs {
|
||||
s.entry_with(|f| fmt::Display::fmt(loc, f));
|
||||
}
|
||||
|
||||
s.finish()
|
||||
});
|
||||
|
||||
s.finish()
|
||||
}
|
||||
}
|
||||
@ -524,6 +278,13 @@ pub struct TriggerExec {
|
||||
trigger_defined_loc: &'static Location<'static>,
|
||||
|
||||
/// Execution defined location
|
||||
// TODO: If a trigger gets executed inside of an effect,
|
||||
// this location will point to this file (in the `Drop` impl),
|
||||
// and we can't `#[track_caller]` past the drop impl, so this
|
||||
// can be wrong.
|
||||
// We can get a better guess by going to the dependency graph and
|
||||
// getting the effect subscriber info, which will be where we're
|
||||
// executed.
|
||||
#[cfg(debug_assertions)]
|
||||
exec_defined_loc: &'static Location<'static>,
|
||||
}
|
||||
@ -544,25 +305,22 @@ impl Drop for TriggerExec {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
tracing::trace!(
|
||||
"Running effect due to trigger\nEffect : {}\nSubscribers: {}\nTrigger : {}\nExecution: {}",
|
||||
"Running effect due to trigger\nEffect : {}\nGathered : {}\nTrigger : {}\nExecution: {}",
|
||||
effect.defined_loc(),
|
||||
match info.defined_locs.is_empty() {
|
||||
match info.is_empty() {
|
||||
true => "[]".to_owned(),
|
||||
#[expect(clippy::format_collect, reason = "TODO")]
|
||||
false => info
|
||||
.defined_locs
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|loc| format!("\n - {loc}"))
|
||||
.map(|info| format!("\n - {}", info.gathered_loc))
|
||||
.collect::<String>(),
|
||||
},
|
||||
self.trigger_defined_loc,
|
||||
self.exec_defined_loc,
|
||||
);
|
||||
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
let _: TriggerEffectInfo = info;
|
||||
let _: Vec<_> = info;
|
||||
|
||||
effect.run();
|
||||
}
|
||||
|
||||
36
examples/Cargo.lock
generated
36
examples/Cargo.lock
generated
@ -17,6 +17,12 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
@ -264,6 +270,7 @@ dependencies = [
|
||||
"futures",
|
||||
"itertools",
|
||||
"parking_lot",
|
||||
"petgraph",
|
||||
"pin-project",
|
||||
"priority-queue",
|
||||
"scopeguard",
|
||||
@ -326,6 +333,18 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
@ -435,6 +454,11 @@ name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@ -719,6 +743,18 @@ version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54acf3a685220b533e437e264e4d932cfbdc4cc7ec0cd232ed73c08d03b8a7ca"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"hashbrown",
|
||||
"indexmap",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.10"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user