Skip to content

Commit 1705039

Browse files
Split ambiguity detection logic off into its own file
1 parent a8b1d8e commit 1705039

File tree

3 files changed

+299
-291
lines changed

3 files changed

+299
-291
lines changed
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
use crate::component::ComponentId;
2+
use crate::schedule::{AmbiguityDetection, SystemContainer, SystemStage};
3+
use crate::world::World;
4+
5+
use bevy_utils::HashMap;
6+
use fixedbitset::FixedBitSet;
7+
8+
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
9+
pub enum AmbiguityReportLevel {
10+
Off,
11+
Minimal,
12+
Verbose,
13+
}
14+
15+
/// Systems that access the same Component or Resource within the same stage
16+
/// risk an ambiguous order that could result in logic bugs, unless they have an
17+
/// explicit execution ordering constraint between them.
18+
///
19+
/// This occurs because, in the absence of explicit constraints, systems are executed in
20+
/// an unstable, arbitrary order within each stage that may vary between runs and frames.
21+
///
22+
/// Some ambiguities reported by the ambiguity checker may be warranted (to allow two systems to run
23+
/// without blocking each other) or spurious, as the exact combination of archetypes used may
24+
/// prevent them from ever conflicting during actual gameplay. You can resolve the warnings produced
25+
/// by the ambiguity checker by adding `.before` or `.after` to one of the conflicting systems
26+
/// referencing the other system to force a specific ordering.
27+
///
28+
/// The checker may report a system more times than the amount of constraints it would actually need
29+
/// to have unambiguous order with regards to a group of already-constrained systems.
30+
///
31+
/// By default only a warning with the number of unresolved ambiguities detected will be reported per [`SystemStage`].
32+
/// This behavior can be changed by explicitly adding this resource using the following constructors:
33+
/// * [`ReportExecutionOrderAmbiguities::off()`] - Disables all messages reported by the ambiguity checker.
34+
/// * [`ReportExecutionOrderAmbiguities::minimal()`] - Displays only the number of unresolved ambiguities detected by the ambiguity checker.
35+
/// * [`ReportExecutionOrderAmbiguities::verbose()`] - Displays a full report of ambiguities detected by the ambiguity checker.
36+
///
37+
/// The ambiguity checker will ignore ambiguities within official Bevy crates.
38+
/// To ignore a custom crate, use [`ReportExecutionOrderAmbiguities::ignore`]
39+
/// with an list of crate names as an argument.
40+
/// This resource should be added before any bevy internal plugin.
41+
///
42+
/// ## Example
43+
/// ```ignore
44+
/// # use bevy_app::App;
45+
/// # use bevy_ecs::schedule::ReportExecutionOrderAmbiguities;
46+
/// App::new()
47+
/// .insert_resource(ReportExecutionOrderAmbiguities::verbose().ignore(&["my_external_crate"]));
48+
/// ```
49+
pub struct ReportExecutionOrderAmbiguities {
50+
pub level: AmbiguityReportLevel,
51+
pub ignore_crates: Vec<String>,
52+
}
53+
54+
/// Returns vector containing all pairs of indices of systems with ambiguous execution order,
55+
/// along with specific components that have triggered the warning.
56+
/// Systems must be topologically sorted beforehand.
57+
pub(super) fn find_ambiguities(
58+
systems: &[impl SystemContainer],
59+
crates_filter: &[String],
60+
) -> Vec<(usize, usize, Vec<ComponentId>)> {
61+
fn should_ignore_ambiguity(
62+
systems: &[impl SystemContainer],
63+
index_a: usize,
64+
index_b: usize,
65+
crates_filter: &[String],
66+
) -> bool {
67+
let system_a = &systems[index_a];
68+
let system_b = &systems[index_b];
69+
70+
(match system_a.ambiguity_detection() {
71+
AmbiguityDetection::Ignore => true,
72+
AmbiguityDetection::Check => false,
73+
AmbiguityDetection::IgnoreWithLabel(labels) => {
74+
labels.iter().any(|l| system_b.labels().contains(l))
75+
}
76+
}) || (match system_b.ambiguity_detection() {
77+
AmbiguityDetection::Ignore => true,
78+
AmbiguityDetection::Check => false,
79+
AmbiguityDetection::IgnoreWithLabel(labels) => {
80+
labels.iter().any(|l| system_a.labels().contains(l))
81+
}
82+
}) || (crates_filter.iter().any(|s| system_a.name().starts_with(s))
83+
&& crates_filter.iter().any(|s| system_b.name().starts_with(s)))
84+
}
85+
86+
let mut ambiguity_set_labels = HashMap::default();
87+
for set in systems.iter().flat_map(|c| c.ambiguity_sets()) {
88+
let len = ambiguity_set_labels.len();
89+
ambiguity_set_labels.entry(set).or_insert(len);
90+
}
91+
let mut all_ambiguity_sets = Vec::<FixedBitSet>::with_capacity(systems.len());
92+
let mut all_dependencies = Vec::<FixedBitSet>::with_capacity(systems.len());
93+
let mut all_dependants = Vec::<FixedBitSet>::with_capacity(systems.len());
94+
for (index, container) in systems.iter().enumerate() {
95+
let mut ambiguity_sets = FixedBitSet::with_capacity(ambiguity_set_labels.len());
96+
for set in container.ambiguity_sets() {
97+
ambiguity_sets.insert(ambiguity_set_labels[set]);
98+
}
99+
all_ambiguity_sets.push(ambiguity_sets);
100+
let mut dependencies = FixedBitSet::with_capacity(systems.len());
101+
for &dependency in container.dependencies() {
102+
dependencies.union_with(&all_dependencies[dependency]);
103+
dependencies.insert(dependency);
104+
all_dependants[dependency].insert(index);
105+
}
106+
107+
all_dependants.push(FixedBitSet::with_capacity(systems.len()));
108+
all_dependencies.push(dependencies);
109+
}
110+
for index in (0..systems.len()).rev() {
111+
let mut dependants = FixedBitSet::with_capacity(systems.len());
112+
for dependant in all_dependants[index].ones() {
113+
dependants.union_with(&all_dependants[dependant]);
114+
dependants.insert(dependant);
115+
}
116+
all_dependants[index] = dependants;
117+
}
118+
let mut all_relations = all_dependencies
119+
.drain(..)
120+
.zip(all_dependants.drain(..))
121+
.enumerate()
122+
.map(|(index, (dependencies, dependants))| {
123+
let mut relations = FixedBitSet::with_capacity(systems.len());
124+
relations.union_with(&dependencies);
125+
relations.union_with(&dependants);
126+
relations.insert(index);
127+
relations
128+
})
129+
.collect::<Vec<FixedBitSet>>();
130+
let mut ambiguities = Vec::new();
131+
let full_bitset: FixedBitSet = (0..systems.len()).collect();
132+
let mut processed = FixedBitSet::with_capacity(systems.len());
133+
for (index_a, relations) in all_relations.drain(..).enumerate() {
134+
// TODO: prove that `.take(index_a)` would be correct here, and uncomment it if so.
135+
for index_b in full_bitset.difference(&relations)
136+
// .take(index_a)
137+
{
138+
if !processed.contains(index_b)
139+
&& all_ambiguity_sets[index_a].is_disjoint(&all_ambiguity_sets[index_b])
140+
&& !should_ignore_ambiguity(systems, index_a, index_b, crates_filter)
141+
{
142+
let a_access = systems[index_a].component_access();
143+
let b_access = systems[index_b].component_access();
144+
if let (Some(a), Some(b)) = (a_access, b_access) {
145+
let conflicts = a.get_conflicts(b);
146+
if !conflicts.is_empty() {
147+
ambiguities.push((index_a, index_b, conflicts));
148+
}
149+
} else {
150+
ambiguities.push((index_a, index_b, Vec::new()));
151+
}
152+
}
153+
}
154+
processed.insert(index_a);
155+
}
156+
ambiguities
157+
}
158+
159+
impl ReportExecutionOrderAmbiguities {
160+
/// Disables all messages reported by the ambiguity checker.
161+
pub fn off() -> Self {
162+
Self {
163+
level: AmbiguityReportLevel::Off,
164+
..Default::default()
165+
}
166+
}
167+
168+
/// Displays only the number of unresolved ambiguities detected by the ambiguity checker. This is the default behavior.
169+
pub fn minimal() -> Self {
170+
Self {
171+
level: AmbiguityReportLevel::Minimal,
172+
..Default::default()
173+
}
174+
}
175+
176+
/// Displays a full report of ambiguities detected by the ambiguity checker.
177+
pub fn verbose() -> Self {
178+
Self {
179+
level: AmbiguityReportLevel::Verbose,
180+
..Default::default()
181+
}
182+
}
183+
184+
/// Adds the given crate to be ignored by ambiguity checker. Check [`ReportExecutionOrderAmbiguities`] for more details.
185+
pub fn ignore(mut self, crate_prefix: &str) -> Self {
186+
self.ignore_crates.push(crate_prefix.to_string());
187+
self
188+
}
189+
190+
/// Adds all the given crates to be ignored by ambiguity checker. Check [`ReportExecutionOrderAmbiguities`] for more details.
191+
pub fn ignore_all(mut self, crate_prefixes: &[&str]) -> Self {
192+
for s in crate_prefixes {
193+
self.ignore_crates.push(s.to_string());
194+
}
195+
self
196+
}
197+
}
198+
199+
impl Default for ReportExecutionOrderAmbiguities {
200+
fn default() -> Self {
201+
Self {
202+
level: AmbiguityReportLevel::Minimal,
203+
ignore_crates: vec![],
204+
}
205+
}
206+
}
207+
208+
impl SystemStage {
209+
/// Logs execution order ambiguities between systems. System orders must be fresh.
210+
pub fn report_ambiguities(&self, world: &mut World) {
211+
let ambiguity_report =
212+
world.get_resource_or_insert_with(ReportExecutionOrderAmbiguities::default);
213+
214+
if ambiguity_report.level == AmbiguityReportLevel::Off {
215+
return;
216+
}
217+
218+
debug_assert!(!self.systems_modified);
219+
220+
fn write_display_names_of_pairs(
221+
offset: usize,
222+
systems: &[impl SystemContainer],
223+
ambiguities: Vec<(usize, usize, Vec<ComponentId>)>,
224+
world: &World,
225+
) -> usize {
226+
for (i, (system_a_index, system_b_index, conflicting_indexes)) in
227+
ambiguities.iter().enumerate()
228+
{
229+
let _system_a_name = systems[*system_a_index].name();
230+
let _system_b_name = systems[*system_b_index].name();
231+
232+
let _conflicting_components = conflicting_indexes
233+
.iter()
234+
.map(|id| world.components().get_info(*id).unwrap().name())
235+
.collect::<Vec<_>>();
236+
237+
let _ambiguity_number = i + offset;
238+
239+
println!(
240+
"{_ambiguity_number}. {_system_a_name} conflicts with {_system_b_name} on {_conflicting_components:?}"
241+
);
242+
}
243+
244+
// We can't merge the SystemContainer arrays, so instead we manually keep track of how high we've counted :upside_down:
245+
ambiguities.len()
246+
}
247+
248+
let parallel = find_ambiguities(&self.parallel, &ambiguity_report.ignore_crates);
249+
let at_start = find_ambiguities(&self.exclusive_at_start, &ambiguity_report.ignore_crates);
250+
let before_commands = find_ambiguities(
251+
&self.exclusive_before_commands,
252+
&ambiguity_report.ignore_crates,
253+
);
254+
let at_end = find_ambiguities(&self.exclusive_at_end, &ambiguity_report.ignore_crates);
255+
256+
let mut unresolved_count = parallel.len();
257+
unresolved_count += at_start.len();
258+
unresolved_count += before_commands.len();
259+
unresolved_count += at_end.len();
260+
261+
if unresolved_count > 0 {
262+
println!("One of your stages contains {unresolved_count} pairs of systems with unknown order and conflicting data access. \
263+
You may want to add `.before()` or `.after()` constraints between some of these systems to prevent bugs.\n");
264+
265+
if ambiguity_report.level != AmbiguityReportLevel::Verbose {
266+
println!("Set the level of the `ReportExecutionOrderAmbiguities` resource to `AmbiguityReportLevel::Verbose` for more details.");
267+
} else {
268+
// TODO: clean up this logic once exclusive systems are more compatible with parallel systems
269+
// allowing us to merge these collections
270+
let mut offset = 1;
271+
offset = write_display_names_of_pairs(offset, &self.parallel, parallel, world);
272+
offset =
273+
write_display_names_of_pairs(offset, &self.exclusive_at_start, at_start, world);
274+
offset = write_display_names_of_pairs(
275+
offset,
276+
&self.exclusive_before_commands,
277+
before_commands,
278+
world,
279+
);
280+
write_display_names_of_pairs(offset, &self.exclusive_at_end, at_end, world);
281+
}
282+
}
283+
}
284+
}

crates/bevy_ecs/src/schedule/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! When using Bevy ECS, systems are usually not run directly, but are inserted into a
44
//! [`Stage`], which then lives within a [`Schedule`].
55
6+
mod ambiguity_detection;
67
mod executor;
78
mod executor_parallel;
89
pub mod graph_utils;
@@ -14,6 +15,7 @@ mod system_container;
1415
mod system_descriptor;
1516
mod system_set;
1617

18+
pub use ambiguity_detection::*;
1719
pub use executor::*;
1820
pub use executor_parallel::*;
1921
pub use graph_utils::GraphNode;

0 commit comments

Comments
 (0)