Skip to content

Commit 4809a3c

Browse files
authored
Move more functions off of ScheduleGraph (#21817)
## Objective - Part of #20115 - Reduce `ScheduleGraph` implementation surface and extract reusable parts. ## Solution - Upgraded and exposed `Dag<N>` as a fully-fledged directed acyclic graph type. - The validity of the cached topological order is now tracked by a `dirty` flag. All modifications to the graph (via `DerefMut` or `Dag::graph_mut`) cause the DAG to be marked as dirty. - Added `Dag::toposort`: if the DAG is dirty, computes topological order, caches it, and marks the DAG as clean. If already clean, returns the cached topological order. We now also reuse the previous toposort `Vec` allocation. - Added `Dag::get_toposort`: can be used to access the topological order with `&self`, with the stipulation that it returns an `Option`, and `None` if the DAG is dirty. - Replaced `check_graph` with `Dag::analyze`, and made it publicly accessible. - Added `Dag::remove_redundant_edges`, which uses the output of `Dag::analyze`. - Renamed `CheckGraphResults` to `DagAnalysis`. - Added `DagAnalysis::check_for_redundant_edges`, replacing `ScheduleGraph::optionally_check_hierarchy_conflicts`. - Added `DagAnalysis::check_for_cross_dependencies`, replacing `ScheduleGraph::check_for_cross_dependencies`. It now takes two full `DagAnalysis` for comparison. - Added `DagAnalysis::check_for_cross_intersection`, replacing `ScheduleGraph::check_order_but_intersect`. - Added `DagGroups` to encapsulate the `HashMap<SystemSetKey, Vec<SystemKey>>` with additional capabilities: - `DagGroups::flatten` and `DagGroups::flatten_undirected` handle the graph reduction operations previously performed by functions on `ScheduleGraph`. - Added `ConflictingSystems` to encapsulate `Vec<(SystemKey, SystemKey, ComponentId)>` with additional capabilities and type safety. See the included migration guide for breaking changes. # Testing - Ran examples - Added new tests for `Dag`, `DagAnalysis`, and `DagGroups` functionality. # Future work - Consider replacing `HashSet<SystemKey>` with a `FixedBitSet`-like type for better performance.
1 parent d87d491 commit 4809a3c

File tree

10 files changed

+1347
-669
lines changed

10 files changed

+1347
-669
lines changed

crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use alloc::{boxed::Box, collections::BTreeSet, vec::Vec};
22

3-
use bevy_platform::collections::HashMap;
3+
use bevy_platform::collections::{HashMap, HashSet};
44

55
use crate::{
6-
schedule::{SystemKey, SystemSetKey},
6+
schedule::{graph::Dag, SystemKey, SystemSetKey},
77
system::{IntoSystem, System},
88
world::World,
99
};
@@ -72,11 +72,11 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass {
7272
&mut self,
7373
_world: &mut World,
7474
graph: &mut ScheduleGraph,
75-
dependency_flattened: &mut DiGraph<SystemKey>,
75+
dependency_flattened: &mut Dag<SystemKey>,
7676
) -> Result<(), ScheduleBuildError> {
77-
let mut sync_point_graph = dependency_flattened.clone();
78-
let topo = dependency_flattened
79-
.toposort()
77+
let mut sync_point_graph = dependency_flattened.graph().clone();
78+
let (topo, flat_dependency) = dependency_flattened
79+
.toposort_and_graph()
8080
.map_err(ScheduleBuildError::FlatDependencySort)?;
8181

8282
fn set_has_conditions(graph: &ScheduleGraph, set: SystemSetKey) -> bool {
@@ -124,7 +124,7 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass {
124124
let mut distance_to_explicit_sync_node: HashMap<u32, SystemKey> = HashMap::default();
125125

126126
// Determine the distance for every node and collect the explicit sync points.
127-
for &key in &topo {
127+
for &key in topo {
128128
let (node_distance, mut node_needs_sync) = distances_and_pending_sync
129129
.get(&key)
130130
.copied()
@@ -146,7 +146,7 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass {
146146
node_needs_sync = graph.systems[key].has_deferred();
147147
}
148148

149-
for target in dependency_flattened.neighbors_directed(key, Direction::Outgoing) {
149+
for target in flat_dependency.neighbors_directed(key, Direction::Outgoing) {
150150
let (target_distance, target_pending_sync) =
151151
distances_and_pending_sync.entry(target).or_default();
152152

@@ -179,13 +179,13 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass {
179179

180180
// Find any edges which have a different number of sync points between them and make sure
181181
// there is a sync point between them.
182-
for &key in &topo {
182+
for &key in topo {
183183
let (node_distance, _) = distances_and_pending_sync
184184
.get(&key)
185185
.copied()
186186
.unwrap_or_default();
187187

188-
for target in dependency_flattened.neighbors_directed(key, Direction::Outgoing) {
188+
for target in flat_dependency.neighbors_directed(key, Direction::Outgoing) {
189189
let (target_distance, _) = distances_and_pending_sync
190190
.get(&target)
191191
.copied()
@@ -215,14 +215,14 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass {
215215
}
216216
}
217217

218-
*dependency_flattened = sync_point_graph;
218+
**dependency_flattened = sync_point_graph;
219219
Ok(())
220220
}
221221

222222
fn collapse_set(
223223
&mut self,
224224
set: SystemSetKey,
225-
systems: &[SystemKey],
225+
systems: &HashSet<SystemKey>,
226226
dependency_flattening: &DiGraph<NodeId>,
227227
) -> impl Iterator<Item = (NodeId, NodeId)> {
228228
if systems.is_empty() {

crates/bevy_ecs/src/schedule/error.rs

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ use core::fmt::Write as _;
44
use thiserror::Error;
55

66
use crate::{
7-
component::{ComponentId, Components},
7+
component::Components,
88
schedule::{
9-
graph::{DiGraphToposortError, GraphNodeId},
10-
NodeId, ScheduleGraph, SystemKey, SystemSetKey,
9+
graph::{
10+
DagCrossDependencyError, DagOverlappingGroupError, DagRedundancyError,
11+
DiGraphToposortError, GraphNodeId,
12+
},
13+
AmbiguousSystemConflictsWarning, ConflictingSystems, NodeId, ScheduleGraph, SystemKey,
14+
SystemSetKey, SystemTypeSetAmbiguityError,
1115
},
1216
world::World,
1317
};
@@ -26,14 +30,14 @@ pub enum ScheduleBuildError {
2630
#[error("Failed to topologically sort the flattened dependency graph: {0}")]
2731
FlatDependencySort(DiGraphToposortError<SystemKey>),
2832
/// Tried to order a system (set) relative to a system set it belongs to.
29-
#[error("`{0:?}` and `{1:?}` have both `in_set` and `before`-`after` relationships (these might be transitive). This combination is unsolvable as a system cannot run before or after a set it belongs to.")]
30-
CrossDependency(NodeId, NodeId),
33+
#[error("`{:?}` and `{:?}` have both `in_set` and `before`-`after` relationships (these might be transitive). This combination is unsolvable as a system cannot run before or after a set it belongs to.", .0.0, .0.1)]
34+
CrossDependency(#[from] DagCrossDependencyError<NodeId>),
3135
/// Tried to order system sets that share systems.
32-
#[error("`{0:?}` and `{1:?}` have a `before`-`after` relationship (which may be transitive) but share systems.")]
33-
SetsHaveOrderButIntersect(SystemSetKey, SystemSetKey),
36+
#[error("`{:?}` and `{:?}` have a `before`-`after` relationship (which may be transitive) but share systems.", .0.0, .0.1)]
37+
SetsHaveOrderButIntersect(#[from] DagOverlappingGroupError<SystemSetKey>),
3438
/// Tried to order a system (set) relative to all instances of some system function.
35-
#[error("Tried to order against `{0:?}` in a schedule that has more than one `{0:?}` instance. `{0:?}` is a `SystemTypeSet` and cannot be used for ordering if ambiguous. Use a different set without this restriction.")]
36-
SystemTypeSetAmbiguity(SystemSetKey),
39+
#[error(transparent)]
40+
SystemTypeSetAmbiguity(#[from] SystemTypeSetAmbiguityError),
3741
/// Tried to run a schedule before all of its systems have been initialized.
3842
#[error("Tried to run a schedule before all of its systems have been initialized.")]
3943
Uninitialized,
@@ -56,7 +60,7 @@ pub enum ScheduleBuildWarning {
5660
/// [`LogLevel::Ignore`]: crate::schedule::LogLevel::Ignore
5761
/// [`LogLevel::Error`]: crate::schedule::LogLevel::Error
5862
#[error("The hierarchy of system sets contains redundant edges: {0:?}")]
59-
HierarchyRedundancy(Vec<(NodeId, NodeId)>),
63+
HierarchyRedundancy(#[from] DagRedundancyError<NodeId>),
6064
/// Systems with conflicting access have indeterminate run order.
6165
///
6266
/// This warning is **disabled** by default, but can be enabled by setting
@@ -66,8 +70,8 @@ pub enum ScheduleBuildWarning {
6670
/// [`ScheduleBuildSettings::ambiguity_detection`]: crate::schedule::ScheduleBuildSettings::ambiguity_detection
6771
/// [`LogLevel::Warn`]: crate::schedule::LogLevel::Warn
6872
/// [`LogLevel::Error`]: crate::schedule::LogLevel::Error
69-
#[error("Systems with conflicting access have indeterminate run order: {0:?}")]
70-
Ambiguity(Vec<(SystemKey, SystemKey, Vec<ComponentId>)>),
73+
#[error(transparent)]
74+
Ambiguity(#[from] AmbiguousSystemConflictsWarning),
7175
}
7276

7377
impl ScheduleBuildError {
@@ -101,13 +105,13 @@ impl ScheduleBuildError {
101105
ScheduleBuildError::FlatDependencySort(DiGraphToposortError::Cycle(cycles)) => {
102106
Self::dependency_cycle_to_string(cycles, graph)
103107
}
104-
ScheduleBuildError::CrossDependency(a, b) => {
105-
Self::cross_dependency_to_string(a, b, graph)
108+
ScheduleBuildError::CrossDependency(error) => {
109+
Self::cross_dependency_to_string(error, graph)
106110
}
107-
ScheduleBuildError::SetsHaveOrderButIntersect(a, b) => {
111+
ScheduleBuildError::SetsHaveOrderButIntersect(DagOverlappingGroupError(a, b)) => {
108112
Self::sets_have_order_but_intersect_to_string(a, b, graph)
109113
}
110-
ScheduleBuildError::SystemTypeSetAmbiguity(set) => {
114+
ScheduleBuildError::SystemTypeSetAmbiguity(SystemTypeSetAmbiguityError(set)) => {
111115
Self::system_type_set_ambiguity_to_string(set, graph)
112116
}
113117
ScheduleBuildError::Uninitialized => Self::uninitialized_to_string(),
@@ -195,7 +199,11 @@ impl ScheduleBuildError {
195199
message
196200
}
197201

198-
fn cross_dependency_to_string(a: &NodeId, b: &NodeId, graph: &ScheduleGraph) -> String {
202+
fn cross_dependency_to_string(
203+
error: &DagCrossDependencyError<NodeId>,
204+
graph: &ScheduleGraph,
205+
) -> String {
206+
let DagCrossDependencyError(a, b) = error;
199207
format!(
200208
"{} `{}` and {} `{}` have both `in_set` and `before`-`after` relationships (these might be transitive). \
201209
This combination is unsolvable as a system cannot run before or after a set it belongs to.",
@@ -227,7 +235,7 @@ impl ScheduleBuildError {
227235
}
228236

229237
pub(crate) fn ambiguity_to_string(
230-
ambiguities: &[(SystemKey, SystemKey, Vec<ComponentId>)],
238+
ambiguities: &ConflictingSystems,
231239
graph: &ScheduleGraph,
232240
components: &Components,
233241
) -> String {
@@ -236,7 +244,7 @@ impl ScheduleBuildError {
236244
"{n_ambiguities} pairs of systems with conflicting data access have indeterminate execution order. \
237245
Consider adding `before`, `after`, or `ambiguous_with` relationships between these:\n",
238246
);
239-
let ambiguities = graph.conflicts_to_string(ambiguities, components);
247+
let ambiguities = ambiguities.to_string(graph, components);
240248
for (name_a, name_b, conflicts) in ambiguities {
241249
writeln!(message, " -- {name_a} and {name_b}").unwrap();
242250

@@ -261,10 +269,10 @@ impl ScheduleBuildWarning {
261269
/// replaced with their names.
262270
pub fn to_string(&self, graph: &ScheduleGraph, world: &World) -> String {
263271
match self {
264-
ScheduleBuildWarning::HierarchyRedundancy(transitive_edges) => {
272+
ScheduleBuildWarning::HierarchyRedundancy(DagRedundancyError(transitive_edges)) => {
265273
ScheduleBuildError::hierarchy_redundancy_to_string(transitive_edges, graph)
266274
}
267-
ScheduleBuildWarning::Ambiguity(ambiguities) => {
275+
ScheduleBuildWarning::Ambiguity(AmbiguousSystemConflictsWarning(ambiguities)) => {
268276
ScheduleBuildError::ambiguity_to_string(ambiguities, graph, world.components())
269277
}
270278
}

0 commit comments

Comments
 (0)