Effect/Trigger dependencies/subscribers are now stored in a global graph.

This commit is contained in:
Filipe Rodrigues 2025-06-27 21:08:36 +01:00
parent 868b105f20
commit 19166944c8
Signed by: zenithsiz
SSH Key Fingerprint: SHA256:Mb5ppb3Sh7IarBO/sBTXLHbYEOz37hJAlslLQPPAPaU
10 changed files with 336 additions and 343 deletions

View File

@ -7,6 +7,7 @@
"itertools",
"loadables",
"metavar",
"petgraph",
"popstate",
"qself",
"scopeguard",

36
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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 }

View 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()
}

View File

@ -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()
}

View File

@ -37,6 +37,7 @@
// Modules
pub mod async_signal;
pub mod dep_graph;
pub mod derived;
pub mod effect;
pub mod effect_stack;

View File

@ -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))
}

View File

@ -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
View File

@ -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"