Skip to content

Commit 7f14581

Browse files
authored
Use explicitly added ApplyDeferred stages when computing automatically inserted sync points. (#16782)
# Objective - The previous implementation of automatically inserting sync points did not consider explicitly added sync points. This created additional sync points. For example: ``` A-B C-D-E ``` If `A` and `B` needed a sync point, and `D` was an `ApplyDeferred`, an additional sync point would be generated between `A` and `B`. ``` A-D2-B C-D -E ``` This can result in the following system ordering: ``` A-D2-(B-C)-D-E ``` Where only `B` and `C` run in parallel. If we could reuse `D` as the sync point, we would get the following ordering: ``` (A-C)-D-(B-E) ``` Now we have two more opportunities for parallelism! ## Solution - In the first pass, we: - Compute the number of sync points before each node - This was already happening but now we consider `ApplyDeferred` nodes as creating a sync point. - Pick an arbitrary explicit `ApplyDeferred` node for each "sync point index" that we can (some required sync points may be missing!) - In the second pass, we: - For each edge, if two nodes have a different number of sync points before them then there must be a sync point between them. - Look for an explicit `ApplyDeferred`. If one exists, use it as the sync point. - Otherwise, generate a new sync point. I believe this should also gracefully handle changes to the `ScheduleGraph`. Since automatically inserted sync points are inserted as systems, they won't look any different to explicit sync points, so they are also candidates for "reusing" sync points. One thing this solution does not handle is "deduping" sync points. If you add 10 sync points explicitly, there will be at least 10 sync points. You could keep track of all the sync points at the same "distance" and then hack apart the graph to dedup those, but that could be a follow-up step (and it's more complicated since you have to worry about transferring edges between nodes). ## Testing - Added a test to test the feature. - The existing tests from all our crates still pass. ## Showcase - Automatically inserted sync points can now reuse explicitly inserted `ApplyDeferred` systems! Previously, Bevy would add new sync points between systems, ignoring the explicitly added sync points. This would reduce parallelism of systems in some situations. Now, the parallelism has been improved!
1 parent 7c7b1e9 commit 7f14581

File tree

2 files changed

+214
-24
lines changed

2 files changed

+214
-24
lines changed

crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,41 +80,109 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass {
8080
let mut sync_point_graph = dependency_flattened.clone();
8181
let topo = graph.topsort_graph(dependency_flattened, ReportCycles::Dependency)?;
8282

83+
fn set_has_conditions(graph: &ScheduleGraph, node: NodeId) -> bool {
84+
!graph.set_conditions_at(node).is_empty()
85+
|| graph
86+
.hierarchy()
87+
.graph()
88+
.edges_directed(node, Direction::Incoming)
89+
.any(|(parent, _)| set_has_conditions(graph, parent))
90+
}
91+
92+
fn system_has_conditions(graph: &ScheduleGraph, node: NodeId) -> bool {
93+
assert!(node.is_system());
94+
!graph.system_conditions[node.index()].is_empty()
95+
|| graph
96+
.hierarchy()
97+
.graph()
98+
.edges_directed(node, Direction::Incoming)
99+
.any(|(parent, _)| set_has_conditions(graph, parent))
100+
}
101+
102+
let mut system_has_conditions_cache = HashMap::default();
103+
104+
fn is_valid_explicit_sync_point(
105+
graph: &ScheduleGraph,
106+
system: NodeId,
107+
system_has_conditions_cache: &mut HashMap<usize, bool>,
108+
) -> bool {
109+
let index = system.index();
110+
is_apply_deferred(graph.systems[index].get().unwrap())
111+
&& !*system_has_conditions_cache
112+
.entry(index)
113+
.or_insert_with(|| system_has_conditions(graph, system))
114+
}
115+
83116
// calculate the number of sync points each sync point is from the beginning of the graph
84-
// use the same sync point if the distance is the same
85-
let mut distances: HashMap<usize, Option<u32>> =
117+
let mut distances: HashMap<usize, u32> =
86118
HashMap::with_capacity_and_hasher(topo.len(), Default::default());
119+
// Keep track of any explicit sync nodes for a specific distance.
120+
let mut distance_to_explicit_sync_node: HashMap<u32, NodeId> = HashMap::default();
87121
for node in &topo {
88-
let add_sync_after = graph.systems[node.index()].get().unwrap().has_deferred();
122+
let node_system = graph.systems[node.index()].get().unwrap();
123+
124+
let node_needs_sync =
125+
if is_valid_explicit_sync_point(graph, *node, &mut system_has_conditions_cache) {
126+
distance_to_explicit_sync_node.insert(
127+
distances.get(&node.index()).copied().unwrap_or_default(),
128+
*node,
129+
);
130+
131+
// This node just did a sync, so the only reason to do another sync is if one was
132+
// explicitly scheduled afterwards.
133+
false
134+
} else {
135+
node_system.has_deferred()
136+
};
89137

90138
for target in dependency_flattened.neighbors_directed(*node, Direction::Outgoing) {
91-
let add_sync_on_edge = add_sync_after
92-
&& !is_apply_deferred(graph.systems[target.index()].get().unwrap())
93-
&& !self.no_sync_edges.contains(&(*node, target));
94-
95-
let weight = if add_sync_on_edge { 1 } else { 0 };
96-
139+
let edge_needs_sync = node_needs_sync
140+
&& !self.no_sync_edges.contains(&(*node, target))
141+
|| is_valid_explicit_sync_point(
142+
graph,
143+
target,
144+
&mut system_has_conditions_cache,
145+
);
146+
147+
let weight = if edge_needs_sync { 1 } else { 0 };
148+
149+
// Use whichever distance is larger, either the current distance, or the distance to
150+
// the parent plus the weight.
97151
let distance = distances
98152
.get(&target.index())
99-
.unwrap_or(&None)
100-
.or(Some(0))
101-
.map(|distance| {
102-
distance.max(
103-
distances.get(&node.index()).unwrap_or(&None).unwrap_or(0) + weight,
104-
)
105-
});
153+
.copied()
154+
.unwrap_or_default()
155+
.max(distances.get(&node.index()).copied().unwrap_or_default() + weight);
106156

107157
distances.insert(target.index(), distance);
158+
}
159+
}
108160

109-
if add_sync_on_edge {
110-
let sync_point =
111-
self.get_sync_point(graph, distances[&target.index()].unwrap());
112-
sync_point_graph.add_edge(*node, sync_point);
113-
sync_point_graph.add_edge(sync_point, target);
161+
// Find any edges which have a different number of sync points between them and make sure
162+
// there is a sync point between them.
163+
for node in &topo {
164+
let node_distance = distances.get(&node.index()).copied().unwrap_or_default();
165+
for target in dependency_flattened.neighbors_directed(*node, Direction::Outgoing) {
166+
let target_distance = distances.get(&target.index()).copied().unwrap_or_default();
167+
if node_distance == target_distance {
168+
// These nodes are the same distance, so they don't need an edge between them.
169+
continue;
170+
}
114171

115-
// edge is now redundant
116-
sync_point_graph.remove_edge(*node, target);
172+
if is_apply_deferred(graph.systems[target.index()].get().unwrap()) {
173+
// We don't need to insert a sync point since ApplyDeferred is a sync point
174+
// already!
175+
continue;
117176
}
177+
let sync_point = distance_to_explicit_sync_node
178+
.get(&target_distance)
179+
.copied()
180+
.unwrap_or_else(|| self.get_sync_point(graph, target_distance));
181+
182+
sync_point_graph.add_edge(*node, sync_point);
183+
sync_point_graph.add_edge(sync_point, target);
184+
185+
sync_point_graph.remove_edge(*node, target);
118186
}
119187
}
120188

crates/bevy_ecs/src/schedule/schedule.rs

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,26 @@ impl ScheduleGraph {
758758
.unwrap()
759759
}
760760

761+
/// Returns the conditions for the set at the given [`NodeId`], if it exists.
762+
pub fn get_set_conditions_at(&self, id: NodeId) -> Option<&[BoxedCondition]> {
763+
if !id.is_set() {
764+
return None;
765+
}
766+
self.system_set_conditions
767+
.get(id.index())
768+
.map(Vec::as_slice)
769+
}
770+
771+
/// Returns the conditions for the set at the given [`NodeId`].
772+
///
773+
/// Panics if it doesn't exist.
774+
#[track_caller]
775+
pub fn set_conditions_at(&self, id: NodeId) -> &[BoxedCondition] {
776+
self.get_set_conditions_at(id)
777+
.ok_or_else(|| format!("set with id {id:?} does not exist in this Schedule"))
778+
.unwrap()
779+
}
780+
761781
/// Returns an iterator over all systems in this schedule, along with the conditions for each system.
762782
pub fn systems(&self) -> impl Iterator<Item = (NodeId, &ScheduleSystem, &[BoxedCondition])> {
763783
self.systems
@@ -2036,7 +2056,7 @@ mod tests {
20362056
use bevy_ecs_macros::ScheduleLabel;
20372057

20382058
use crate::{
2039-
prelude::{Res, Resource},
2059+
prelude::{ApplyDeferred, Res, Resource},
20402060
schedule::{
20412061
tests::ResMut, IntoSystemConfigs, IntoSystemSetConfigs, Schedule,
20422062
ScheduleBuildSettings, SystemSet,
@@ -2088,6 +2108,108 @@ mod tests {
20882108
assert_eq!(schedule.executable.systems.len(), 3);
20892109
}
20902110

2111+
#[test]
2112+
fn explicit_sync_point_used_as_auto_sync_point() {
2113+
let mut schedule = Schedule::default();
2114+
let mut world = World::default();
2115+
schedule.add_systems(
2116+
(
2117+
|mut commands: Commands| commands.insert_resource(Resource1),
2118+
|_: Res<Resource1>| {},
2119+
)
2120+
.chain(),
2121+
);
2122+
schedule.add_systems((|| {}, ApplyDeferred, || {}).chain());
2123+
schedule.run(&mut world);
2124+
2125+
// No sync point was inserted, since we can reuse the explicit sync point.
2126+
assert_eq!(schedule.executable.systems.len(), 5);
2127+
}
2128+
2129+
#[test]
2130+
fn conditional_explicit_sync_point_not_used_as_auto_sync_point() {
2131+
let mut schedule = Schedule::default();
2132+
let mut world = World::default();
2133+
schedule.add_systems(
2134+
(
2135+
|mut commands: Commands| commands.insert_resource(Resource1),
2136+
|_: Res<Resource1>| {},
2137+
)
2138+
.chain(),
2139+
);
2140+
schedule.add_systems((|| {}, ApplyDeferred.run_if(|| false), || {}).chain());
2141+
schedule.run(&mut world);
2142+
2143+
// A sync point was inserted, since the explicit sync point is not always run.
2144+
assert_eq!(schedule.executable.systems.len(), 6);
2145+
}
2146+
2147+
#[test]
2148+
fn conditional_explicit_sync_point_not_used_as_auto_sync_point_condition_on_chain() {
2149+
let mut schedule = Schedule::default();
2150+
let mut world = World::default();
2151+
schedule.add_systems(
2152+
(
2153+
|mut commands: Commands| commands.insert_resource(Resource1),
2154+
|_: Res<Resource1>| {},
2155+
)
2156+
.chain(),
2157+
);
2158+
schedule.add_systems((|| {}, ApplyDeferred, || {}).chain().run_if(|| false));
2159+
schedule.run(&mut world);
2160+
2161+
// A sync point was inserted, since the explicit sync point is not always run.
2162+
assert_eq!(schedule.executable.systems.len(), 6);
2163+
}
2164+
2165+
#[test]
2166+
fn conditional_explicit_sync_point_not_used_as_auto_sync_point_condition_on_system_set() {
2167+
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
2168+
struct Set;
2169+
2170+
let mut schedule = Schedule::default();
2171+
let mut world = World::default();
2172+
schedule.configure_sets(Set.run_if(|| false));
2173+
schedule.add_systems(
2174+
(
2175+
|mut commands: Commands| commands.insert_resource(Resource1),
2176+
|_: Res<Resource1>| {},
2177+
)
2178+
.chain(),
2179+
);
2180+
schedule.add_systems((|| {}, ApplyDeferred.in_set(Set), || {}).chain());
2181+
schedule.run(&mut world);
2182+
2183+
// A sync point was inserted, since the explicit sync point is not always run.
2184+
assert_eq!(schedule.executable.systems.len(), 6);
2185+
}
2186+
2187+
#[test]
2188+
fn conditional_explicit_sync_point_not_used_as_auto_sync_point_condition_on_nested_system_set()
2189+
{
2190+
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
2191+
struct Set1;
2192+
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
2193+
struct Set2;
2194+
2195+
let mut schedule = Schedule::default();
2196+
let mut world = World::default();
2197+
schedule.configure_sets(Set2.run_if(|| false));
2198+
schedule.configure_sets(Set1.in_set(Set2));
2199+
schedule.add_systems(
2200+
(
2201+
|mut commands: Commands| commands.insert_resource(Resource1),
2202+
|_: Res<Resource1>| {},
2203+
)
2204+
.chain(),
2205+
);
2206+
schedule.add_systems((|| {}, ApplyDeferred, || {}).chain().in_set(Set1));
2207+
schedule.run(&mut world);
2208+
2209+
// A sync point was inserted, since the explicit sync point is not always run.
2210+
assert_eq!(schedule.executable.systems.len(), 6);
2211+
}
2212+
20912213
#[test]
20922214
fn merges_sync_points_into_one() {
20932215
let mut schedule = Schedule::default();

0 commit comments

Comments
 (0)