Skip to content

Commit 0090846

Browse files
authored
Better ScheduleBuildError introspection and handling (#20256)
# Objective - Part of #20115 I want to pull out the ad-hoc graph manipulation and analysis functions into generalized reusable functions, but in doing so encounter some borrow-checker issues. This is because the graphs are mutably borrowed during the build process at the points where we need to render warning and error messages (which require full access to the `ScheduleGraph`). ## Solution So, lets defer message rendering until the consumer actually needs it: - Replaced `String`s in `ScheduleBuildError` variants with just the `NodeId`/`SystemKey`/`SystemSetKey`. - Added `ScheduleBuildError::to_string` to render the messages as they were previously. - Added `ScheduleBuildWarning`, a subset of the error enum of just the possible warnings. - Collected warnings into a `Vec` and returned them from the build process, caching them in the `Schedule` and made accessible via `Schedule::warnings`. Additionally automatically `warn!()` these warnings after the schedule is built. - Exposed `ScheduleGraph::get_node_name` so external consumers can do any special rendering themselves. - Finally, moved `ScheduleBuildError` and `ScheduleBuildWarning` to their own files to help incrementally minimize the large `schedule.rs` module. ## Testing Reusing current tests.
1 parent 4b1b70d commit 0090846

File tree

4 files changed

+380
-220
lines changed

4 files changed

+380
-220
lines changed

crates/bevy_ecs/src/schedule/error.rs

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
use alloc::{format, string::String, vec::Vec};
2+
use core::fmt::Write as _;
3+
4+
use thiserror::Error;
5+
6+
use crate::{
7+
component::{ComponentId, Components},
8+
schedule::{graph::GraphNodeId, NodeId, ScheduleGraph, SystemKey, SystemSetKey},
9+
world::World,
10+
};
11+
12+
/// Category of errors encountered during [`Schedule::initialize`](crate::schedule::Schedule::initialize).
13+
#[non_exhaustive]
14+
#[derive(Error, Debug)]
15+
pub enum ScheduleBuildError {
16+
/// A system set contains itself.
17+
#[error("System set `{0:?}` contains itself.")]
18+
HierarchyLoop(NodeId),
19+
/// The hierarchy of system sets contains a cycle.
20+
#[error("The hierarchy of system sets contains a cycle: {0:?}")]
21+
HierarchyCycle(Vec<Vec<NodeId>>),
22+
/// A system (set) has been told to run before itself.
23+
#[error("`{0:?}` has been told to run before itself.")]
24+
DependencyLoop(NodeId),
25+
/// The dependency graph contains a cycle.
26+
#[error("The dependency graph contains a cycle: {0:?}")]
27+
DependencyCycle(Vec<Vec<NodeId>>),
28+
/// 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),
31+
/// 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),
34+
/// 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),
37+
/// Tried to run a schedule before all of its systems have been initialized.
38+
#[error("Tried to run a schedule before all of its systems have been initialized.")]
39+
Uninitialized,
40+
/// A warning that was elevated to an error.
41+
#[error(transparent)]
42+
Elevated(#[from] ScheduleBuildWarning),
43+
}
44+
45+
/// Category of warnings encountered during [`Schedule::initialize`](crate::schedule::Schedule::initialize).
46+
#[non_exhaustive]
47+
#[derive(Error, Debug)]
48+
pub enum ScheduleBuildWarning {
49+
/// The hierarchy of system sets contains redundant edges.
50+
///
51+
/// This warning is **enabled** by default, but can be disabled by setting
52+
/// [`ScheduleBuildSettings::hierarchy_detection`] to [`LogLevel::Ignore`]
53+
/// or upgraded to a [`ScheduleBuildError`] by setting it to [`LogLevel::Error`].
54+
///
55+
/// [`ScheduleBuildSettings::hierarchy_detection`]: crate::schedule::ScheduleBuildSettings::hierarchy_detection
56+
/// [`LogLevel::Ignore`]: crate::schedule::LogLevel::Ignore
57+
/// [`LogLevel::Error`]: crate::schedule::LogLevel::Error
58+
#[error("The hierarchy of system sets contains redundant edges: {0:?}")]
59+
HierarchyRedundancy(Vec<(NodeId, NodeId)>),
60+
/// Systems with conflicting access have indeterminate run order.
61+
///
62+
/// This warning is **disabled** by default, but can be enabled by setting
63+
/// [`ScheduleBuildSettings::ambiguity_detection`] to [`LogLevel::Warn`]
64+
/// or upgraded to a [`ScheduleBuildError`] by setting it to [`LogLevel::Error`].
65+
///
66+
/// [`ScheduleBuildSettings::ambiguity_detection`]: crate::schedule::ScheduleBuildSettings::ambiguity_detection
67+
/// [`LogLevel::Warn`]: crate::schedule::LogLevel::Warn
68+
/// [`LogLevel::Error`]: crate::schedule::LogLevel::Error
69+
#[error("Systems with conflicting access have indeterminate run order: {0:?}")]
70+
Ambiguity(Vec<(SystemKey, SystemKey, Vec<ComponentId>)>),
71+
}
72+
73+
impl ScheduleBuildError {
74+
/// Renders the error as a human-readable string with node identifiers
75+
/// replaced with their names.
76+
///
77+
/// The given `graph` and `world` are used to resolve the names of the nodes
78+
/// and components involved in the error. The same `graph` and `world`
79+
/// should be used as those used to [`initialize`] the [`Schedule`]. Failure
80+
/// to do so will result in incorrect or incomplete error messages.
81+
///
82+
/// [`initialize`]: crate::schedule::Schedule::initialize
83+
/// [`Schedule`]: crate::schedule::Schedule
84+
pub fn to_string(&self, graph: &ScheduleGraph, world: &World) -> String {
85+
match self {
86+
ScheduleBuildError::HierarchyLoop(node_id) => {
87+
Self::hierarchy_loop_to_string(node_id, graph)
88+
}
89+
ScheduleBuildError::HierarchyCycle(cycles) => {
90+
Self::hierarchy_cycle_to_string(cycles, graph)
91+
}
92+
ScheduleBuildError::DependencyLoop(node_id) => {
93+
Self::dependency_loop_to_string(node_id, graph)
94+
}
95+
ScheduleBuildError::DependencyCycle(cycles) => {
96+
Self::dependency_cycle_to_string(cycles, graph)
97+
}
98+
ScheduleBuildError::CrossDependency(a, b) => {
99+
Self::cross_dependency_to_string(a, b, graph)
100+
}
101+
ScheduleBuildError::SetsHaveOrderButIntersect(a, b) => {
102+
Self::sets_have_order_but_intersect_to_string(a, b, graph)
103+
}
104+
ScheduleBuildError::SystemTypeSetAmbiguity(set) => {
105+
Self::system_type_set_ambiguity_to_string(set, graph)
106+
}
107+
ScheduleBuildError::Uninitialized => Self::uninitialized_to_string(),
108+
ScheduleBuildError::Elevated(e) => e.to_string(graph, world),
109+
}
110+
}
111+
112+
fn hierarchy_loop_to_string(node_id: &NodeId, graph: &ScheduleGraph) -> String {
113+
format!(
114+
"{} `{}` contains itself",
115+
node_id.kind(),
116+
graph.get_node_name(node_id)
117+
)
118+
}
119+
120+
fn hierarchy_cycle_to_string(cycles: &[Vec<NodeId>], graph: &ScheduleGraph) -> String {
121+
let mut message = format!("schedule has {} in_set cycle(s):\n", cycles.len());
122+
for (i, cycle) in cycles.iter().enumerate() {
123+
let mut names = cycle.iter().map(|id| (id.kind(), graph.get_node_name(id)));
124+
let (first_kind, first_name) = names.next().unwrap();
125+
writeln!(
126+
message,
127+
"cycle {}: {first_kind} `{first_name}` contains itself",
128+
i + 1,
129+
)
130+
.unwrap();
131+
writeln!(message, "{first_kind} `{first_name}`").unwrap();
132+
for (kind, name) in names.chain(core::iter::once((first_kind, first_name))) {
133+
writeln!(message, " ... which contains {kind} `{name}`").unwrap();
134+
}
135+
writeln!(message).unwrap();
136+
}
137+
message
138+
}
139+
140+
fn hierarchy_redundancy_to_string(
141+
transitive_edges: &[(NodeId, NodeId)],
142+
graph: &ScheduleGraph,
143+
) -> String {
144+
let mut message = String::from("hierarchy contains redundant edge(s)");
145+
for (parent, child) in transitive_edges {
146+
writeln!(
147+
message,
148+
" -- {} `{}` cannot be child of {} `{}`, longer path exists",
149+
child.kind(),
150+
graph.get_node_name(child),
151+
parent.kind(),
152+
graph.get_node_name(parent),
153+
)
154+
.unwrap();
155+
}
156+
message
157+
}
158+
159+
fn dependency_loop_to_string(node_id: &NodeId, graph: &ScheduleGraph) -> String {
160+
format!(
161+
"{} `{}` has been told to run before itself",
162+
node_id.kind(),
163+
graph.get_node_name(node_id)
164+
)
165+
}
166+
167+
fn dependency_cycle_to_string(cycles: &[Vec<NodeId>], graph: &ScheduleGraph) -> String {
168+
let mut message = format!("schedule has {} before/after cycle(s):\n", cycles.len());
169+
for (i, cycle) in cycles.iter().enumerate() {
170+
let mut names = cycle.iter().map(|id| (id.kind(), graph.get_node_name(id)));
171+
let (first_kind, first_name) = names.next().unwrap();
172+
writeln!(
173+
message,
174+
"cycle {}: {first_kind} `{first_name}` must run before itself",
175+
i + 1,
176+
)
177+
.unwrap();
178+
writeln!(message, "{first_kind} `{first_name}`").unwrap();
179+
for (kind, name) in names.chain(core::iter::once((first_kind, first_name))) {
180+
writeln!(message, " ... which must run before {kind} `{name}`").unwrap();
181+
}
182+
writeln!(message).unwrap();
183+
}
184+
message
185+
}
186+
187+
fn cross_dependency_to_string(a: &NodeId, b: &NodeId, graph: &ScheduleGraph) -> String {
188+
format!(
189+
"{} `{}` and {} `{}` have both `in_set` and `before`-`after` relationships (these might be transitive). \
190+
This combination is unsolvable as a system cannot run before or after a set it belongs to.",
191+
a.kind(),
192+
graph.get_node_name(a),
193+
b.kind(),
194+
graph.get_node_name(b)
195+
)
196+
}
197+
198+
fn sets_have_order_but_intersect_to_string(
199+
a: &SystemSetKey,
200+
b: &SystemSetKey,
201+
graph: &ScheduleGraph,
202+
) -> String {
203+
format!(
204+
"`{}` and `{}` have a `before`-`after` relationship (which may be transitive) but share systems.",
205+
graph.get_node_name(&NodeId::Set(*a)),
206+
graph.get_node_name(&NodeId::Set(*b)),
207+
)
208+
}
209+
210+
fn system_type_set_ambiguity_to_string(set: &SystemSetKey, graph: &ScheduleGraph) -> String {
211+
let name = graph.get_node_name(&NodeId::Set(*set));
212+
format!(
213+
"Tried to order against `{name}` in a schedule that has more than one `{name}` instance. `{name}` is a \
214+
`SystemTypeSet` and cannot be used for ordering if ambiguous. Use a different set without this restriction."
215+
)
216+
}
217+
218+
pub(crate) fn ambiguity_to_string(
219+
ambiguities: &[(SystemKey, SystemKey, Vec<ComponentId>)],
220+
graph: &ScheduleGraph,
221+
components: &Components,
222+
) -> String {
223+
let n_ambiguities = ambiguities.len();
224+
let mut message = format!(
225+
"{n_ambiguities} pairs of systems with conflicting data access have indeterminate execution order. \
226+
Consider adding `before`, `after`, or `ambiguous_with` relationships between these:\n",
227+
);
228+
let ambiguities = graph.conflicts_to_string(ambiguities, components);
229+
for (name_a, name_b, conflicts) in ambiguities {
230+
writeln!(message, " -- {name_a} and {name_b}").unwrap();
231+
232+
if !conflicts.is_empty() {
233+
writeln!(message, " conflict on: {conflicts:?}").unwrap();
234+
} else {
235+
// one or both systems must be exclusive
236+
let world = core::any::type_name::<World>();
237+
writeln!(message, " conflict on: {world}").unwrap();
238+
}
239+
}
240+
message
241+
}
242+
243+
fn uninitialized_to_string() -> String {
244+
String::from("tried to run a schedule before all of its systems have been initialized")
245+
}
246+
}
247+
248+
impl ScheduleBuildWarning {
249+
/// Renders the warning as a human-readable string with node identifiers
250+
/// replaced with their names.
251+
pub fn to_string(&self, graph: &ScheduleGraph, world: &World) -> String {
252+
match self {
253+
ScheduleBuildWarning::HierarchyRedundancy(transitive_edges) => {
254+
ScheduleBuildError::hierarchy_redundancy_to_string(transitive_edges, graph)
255+
}
256+
ScheduleBuildWarning::Ambiguity(ambiguities) => {
257+
ScheduleBuildError::ambiguity_to_string(ambiguities, graph, world.components())
258+
}
259+
}
260+
}
261+
}

crates/bevy_ecs/src/schedule/mod.rs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
mod auto_insert_apply_deferred;
44
mod condition;
55
mod config;
6+
mod error;
67
mod executor;
78
mod node;
89
mod pass;
@@ -12,7 +13,7 @@ mod stepping;
1213

1314
pub use self::graph::GraphInfo;
1415
use self::graph::*;
15-
pub use self::{condition::*, config::*, executor::*, node::*, schedule::*, set::*};
16+
pub use self::{condition::*, config::*, error::*, executor::*, node::*, schedule::*, set::*};
1617
pub use pass::ScheduleBuildPass;
1718

1819
/// An implementation of a graph data structure.
@@ -701,7 +702,9 @@ mod tests {
701702
let result = schedule.initialize(&mut world);
702703
assert!(matches!(
703704
result,
704-
Err(ScheduleBuildError::HierarchyRedundancy(_))
705+
Err(ScheduleBuildError::Elevated(
706+
ScheduleBuildWarning::HierarchyRedundancy(_)
707+
))
705708
));
706709
}
707710

@@ -763,7 +766,12 @@ mod tests {
763766

764767
schedule.add_systems((res_ref, res_mut));
765768
let result = schedule.initialize(&mut world);
766-
assert!(matches!(result, Err(ScheduleBuildError::Ambiguity(_))));
769+
assert!(matches!(
770+
result,
771+
Err(ScheduleBuildError::Elevated(
772+
ScheduleBuildWarning::Ambiguity(_)
773+
))
774+
));
767775
}
768776
}
769777

@@ -1130,11 +1138,9 @@ mod tests {
11301138
));
11311139

11321140
schedule.graph_mut().initialize(&mut world);
1133-
let _ = schedule.graph_mut().build_schedule(
1134-
&mut world,
1135-
TestSchedule.intern(),
1136-
&BTreeSet::new(),
1137-
);
1141+
let _ = schedule
1142+
.graph_mut()
1143+
.build_schedule(&mut world, &BTreeSet::new());
11381144

11391145
let ambiguities: Vec<_> = schedule
11401146
.graph()
@@ -1190,11 +1196,9 @@ mod tests {
11901196

11911197
let mut world = World::new();
11921198
schedule.graph_mut().initialize(&mut world);
1193-
let _ = schedule.graph_mut().build_schedule(
1194-
&mut world,
1195-
TestSchedule.intern(),
1196-
&BTreeSet::new(),
1197-
);
1199+
let _ = schedule
1200+
.graph_mut()
1201+
.build_schedule(&mut world, &BTreeSet::new());
11981202

11991203
let ambiguities: Vec<_> = schedule
12001204
.graph()

0 commit comments

Comments
 (0)