Skip to content

Commit bd54c4d

Browse files
committed
extract topsort logic to a new method, one pass to detect cycles and … (#7727)
…top sort. reduce mem alloc # Objective - Reduce alloc count. - Improve code quality. ## Solution - use `TarjanScc::run` directly, which calls a closure with each scc, in closure, we can detect cycles and flatten nodes
1 parent 5a51256 commit bd54c4d

File tree

1 file changed

+39
-42
lines changed

1 file changed

+39
-42
lines changed

crates/bevy_ecs/src/schedule/schedule.rs

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use bevy_utils::default;
77
#[cfg(feature = "trace")]
88
use bevy_utils::tracing::info_span;
99
use bevy_utils::{
10-
petgraph::{algo::tarjan_scc, prelude::*},
10+
petgraph::prelude::*,
1111
thiserror::Error,
1212
tracing::{error, warn},
1313
HashMap, HashSet,
@@ -941,15 +941,9 @@ impl ScheduleGraph {
941941
}
942942

943943
// check hierarchy for cycles
944-
let hier_scc = tarjan_scc(&self.hierarchy.graph);
945-
// PERF: in theory we can skip this contains_cycles because we've already detected cycles
946-
// using calculate_base_sets_and_detect_cycles
947-
if self.contains_cycles(&hier_scc) {
948-
self.report_cycles(&hier_scc);
949-
return Err(ScheduleBuildError::HierarchyCycle);
950-
}
951-
952-
self.hierarchy.topsort = hier_scc.into_iter().flatten().rev().collect::<Vec<_>>();
944+
self.hierarchy.topsort = self
945+
.topsort_graph(&self.hierarchy.graph)
946+
.map_err(|_| ScheduleBuildError::HierarchyCycle)?;
953947

954948
let hier_results = check_graph(&self.hierarchy.graph, &self.hierarchy.topsort);
955949
if self.settings.hierarchy_detection != LogLevel::Ignore
@@ -965,13 +959,9 @@ impl ScheduleGraph {
965959
self.hierarchy.graph = hier_results.transitive_reduction;
966960

967961
// check dependencies for cycles
968-
let dep_scc = tarjan_scc(&self.dependency.graph);
969-
if self.contains_cycles(&dep_scc) {
970-
self.report_cycles(&dep_scc);
971-
return Err(ScheduleBuildError::DependencyCycle);
972-
}
973-
974-
self.dependency.topsort = dep_scc.into_iter().flatten().rev().collect::<Vec<_>>();
962+
self.dependency.topsort = self
963+
.topsort_graph(&self.dependency.graph)
964+
.map_err(|_| ScheduleBuildError::DependencyCycle)?;
975965

976966
// check for systems or system sets depending on sets they belong to
977967
let dep_results = check_graph(&self.dependency.graph, &self.dependency.topsort);
@@ -1092,15 +1082,10 @@ impl ScheduleGraph {
10921082
}
10931083

10941084
// topsort
1095-
let flat_scc = tarjan_scc(&dependency_flattened);
1096-
if self.contains_cycles(&flat_scc) {
1097-
self.report_cycles(&flat_scc);
1098-
return Err(ScheduleBuildError::DependencyCycle);
1099-
}
1100-
1085+
self.dependency_flattened.topsort = self
1086+
.topsort_graph(&dependency_flattened)
1087+
.map_err(|_| ScheduleBuildError::DependencyCycle)?;
11011088
self.dependency_flattened.graph = dependency_flattened;
1102-
self.dependency_flattened.topsort =
1103-
flat_scc.into_iter().flatten().rev().collect::<Vec<_>>();
11041089

11051090
let flat_results = check_graph(
11061091
&self.dependency_flattened.graph,
@@ -1377,31 +1362,43 @@ impl ScheduleGraph {
13771362
error!("{}", message);
13781363
}
13791364

1380-
fn contains_cycles(&self, strongly_connected_components: &[Vec<NodeId>]) -> bool {
1381-
if strongly_connected_components
1382-
.iter()
1383-
.all(|scc| scc.len() == 1)
1384-
{
1385-
return false;
1386-
}
1365+
/// Get topology sorted [`NodeId`], also ensures the graph contains no cycle
1366+
/// returns Err(()) if there are cycles
1367+
fn topsort_graph(&self, graph: &DiGraphMap<NodeId, ()>) -> Result<Vec<NodeId>, ()> {
1368+
// tarjon_scc's run order is reverse topological order
1369+
let mut rev_top_sorted_nodes = Vec::<NodeId>::with_capacity(graph.node_count());
1370+
let mut tarjan_scc = bevy_utils::petgraph::algo::TarjanScc::new();
1371+
let mut sccs_with_cycle = Vec::<Vec<NodeId>>::new();
13871372

1388-
true
1373+
tarjan_scc.run(graph, |scc| {
1374+
// by scc's definition, each scc is the cluster of nodes that they can reach each other
1375+
// so scc with size larger than 1, means there is/are cycle(s).
1376+
if scc.len() > 1 {
1377+
sccs_with_cycle.push(scc.to_vec());
1378+
}
1379+
rev_top_sorted_nodes.extend_from_slice(scc);
1380+
});
1381+
1382+
if sccs_with_cycle.is_empty() {
1383+
// reverse the reverted to get topological order
1384+
let mut top_sorted_nodes = rev_top_sorted_nodes;
1385+
top_sorted_nodes.reverse();
1386+
Ok(top_sorted_nodes)
1387+
} else {
1388+
self.report_cycles(&sccs_with_cycle);
1389+
Err(())
1390+
}
13891391
}
13901392

1391-
fn report_cycles(&self, strongly_connected_components: &[Vec<NodeId>]) {
1392-
let components_with_cycles = strongly_connected_components
1393-
.iter()
1394-
.filter(|scc| scc.len() > 1)
1395-
.cloned()
1396-
.collect::<Vec<_>>();
1397-
1393+
/// Print detailed cycle messages
1394+
fn report_cycles(&self, sccs_with_cycles: &[Vec<NodeId>]) {
13981395
let mut message = format!(
13991396
"schedule contains at least {} cycle(s)",
1400-
components_with_cycles.len()
1397+
sccs_with_cycles.len()
14011398
);
14021399

14031400
writeln!(message, " -- cycle(s) found within:").unwrap();
1404-
for (i, scc) in components_with_cycles.into_iter().enumerate() {
1401+
for (i, scc) in sccs_with_cycles.iter().enumerate() {
14051402
let names = scc
14061403
.iter()
14071404
.map(|id| self.get_node_name(id))

0 commit comments

Comments
 (0)