Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Stageless: prettier cycle reporting #7463

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 117 additions & 1 deletion crates/bevy_ecs/src/schedule/graph_utils.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::fmt::Debug;

use bevy_utils::{
petgraph::{graphmap::NodeTrait, prelude::*},
petgraph::{algo::TarjanScc, graphmap::NodeTrait, prelude::*},
HashMap, HashSet,
};
use fixedbitset::FixedBitSet;
Expand Down Expand Up @@ -274,3 +274,119 @@ where
transitive_closure,
}
}

/// Returns the simple cycles in a strongly-connected component of a directed graph.
///
/// The algorithm implemented comes from
/// ["Finding all the elementary circuits of a directed graph"][1] by D. B. Johnson.
///
/// [1]: https://doi.org/10.1137/0204007
pub fn simple_cycles_in_component<N>(graph: &DiGraphMap<N, ()>, scc: &[N]) -> Vec<Vec<N>>
where
N: NodeTrait + Debug,
{
let mut cycles = vec![];
let mut sccs = vec![scc.to_vec()];

while let Some(mut scc) = sccs.pop() {
maniwani marked this conversation as resolved.
Show resolved Hide resolved
// only look at nodes and edges in this strongly-connected component
let mut subgraph = DiGraphMap::new();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also set a capacity here?

for &node in &scc {
subgraph.add_node(node);
}

for &node in &scc {
for successor in graph.neighbors(node) {
if subgraph.contains_node(successor) {
subgraph.add_edge(node, successor, ());
}
}
}

// path of nodes that may form a cycle
let mut path = Vec::with_capacity(subgraph.node_count());
// we mark nodes as "blocked" to avoid finding permutations of the same cycles
let mut blocked = HashSet::with_capacity(subgraph.node_count());
// connects nodes along path segments that can't be part of a cycle (given current root)
// those nodes can be unblocked at the same time
let mut unblock_together: HashMap<N, HashSet<N>> =
HashMap::with_capacity(subgraph.node_count());
// stack for unblocking nodes
let mut unblock_stack = Vec::with_capacity(subgraph.node_count());
// nodes can be involved in multiple cycles
let mut maybe_in_more_cycles: HashSet<N> = HashSet::with_capacity(subgraph.node_count());
// stack for DFS
let mut stack = Vec::with_capacity(subgraph.node_count());

// we're going to look for all cycles that begin and end at this node
let root = scc.pop().unwrap();
// start a path at the root
path.clear();
path.push(root);
// mark this node as blocked
blocked.insert(root);

// DFS
stack.clear();
stack.push((root, subgraph.neighbors(root)));
while !stack.is_empty() {
let (ref node, successors) = stack.last_mut().unwrap();
if let Some(next) = successors.next() {
if next == root {
// found a cycle
maybe_in_more_cycles.extend(path.iter());
cycles.push(path.clone());
} else if !blocked.contains(&next) {
// first time seeing `next` on this path
maybe_in_more_cycles.remove(&next);
path.push(next);
blocked.insert(next);
stack.push((next, subgraph.neighbors(next)));
continue;
} else {
// not first time seeing `next` on this path
}
}

if successors.peekable().peek().is_none() {
if maybe_in_more_cycles.contains(node) {
unblock_stack.push(*node);
// unblock this node's ancestors
while let Some(n) = unblock_stack.pop() {
if blocked.remove(&n) {
let unblock_predecessors =
unblock_together.entry(n).or_insert_with(HashSet::new);
unblock_stack.extend(unblock_predecessors.iter());
unblock_predecessors.clear();
}
}
} else {
// if its descendants can be unblocked later, this node will be too
for successor in subgraph.neighbors(*node) {
unblock_together
.entry(successor)
.or_insert_with(HashSet::new)
.insert(*node);
}
}

// remove node from path and DFS stack
path.pop();
stack.pop();
}
}

// remove node from subgraph
subgraph.remove_node(root);

// divide remainder into smaller SCCs
let mut tarjan_scc = TarjanScc::new();
tarjan_scc.run(&subgraph, |scc| {
if scc.len() > 1 {
sccs.push(scc.to_vec());
}
});
}

cycles
}
121 changes: 87 additions & 34 deletions crates/bevy_ecs/src/schedule/schedule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use bevy_utils::default;
#[cfg(feature = "trace")]
use bevy_utils::tracing::info_span;
use bevy_utils::{
petgraph::prelude::*,
petgraph::{algo::TarjanScc, prelude::*},
thiserror::Error,
tracing::{error, warn},
HashMap, HashSet,
Expand Down Expand Up @@ -942,7 +942,7 @@ impl ScheduleGraph {

// check hierarchy for cycles
self.hierarchy.topsort = self
.topsort_graph(&self.hierarchy.graph)
.topsort_graph(&self.hierarchy.graph, ReportCycles::Hierarchy)
.map_err(|_| ScheduleBuildError::HierarchyCycle)?;

let hier_results = check_graph(&self.hierarchy.graph, &self.hierarchy.topsort);
Expand All @@ -960,7 +960,7 @@ impl ScheduleGraph {

// check dependencies for cycles
self.dependency.topsort = self
.topsort_graph(&self.dependency.graph)
.topsort_graph(&self.dependency.graph, ReportCycles::Dependency)
.map_err(|_| ScheduleBuildError::DependencyCycle)?;

// check for systems or system sets depending on sets they belong to
Expand Down Expand Up @@ -1083,7 +1083,7 @@ impl ScheduleGraph {

// topsort
self.dependency_flattened.topsort = self
.topsort_graph(&dependency_flattened)
.topsort_graph(&dependency_flattened, ReportCycles::Dependency)
.map_err(|_| ScheduleBuildError::DependencyCycle)?;
self.dependency_flattened.graph = dependency_flattened;

Expand Down Expand Up @@ -1318,6 +1318,12 @@ impl ScheduleGraph {
}
}

/// Used to select the appropriate reporting function.
enum ReportCycles {
Hierarchy,
Dependency,
}

// methods for reporting errors
impl ScheduleGraph {
fn get_node_name(&self, id: &NodeId) -> String {
Expand Down Expand Up @@ -1345,7 +1351,7 @@ impl ScheduleGraph {
name
}

fn get_node_kind(id: &NodeId) -> &'static str {
fn get_node_kind(&self, id: &NodeId) -> &'static str {
match id {
NodeId::System(_) => "system",
NodeId::Set(_) => "system set",
Expand All @@ -1366,7 +1372,7 @@ impl ScheduleGraph {
writeln!(
message,
" -- {:?} '{:?}' cannot be child of set '{:?}', longer path exists",
Self::get_node_kind(child),
self.get_node_kind(child),
self.get_node_name(child),
self.get_node_name(parent),
)
Expand All @@ -1376,48 +1382,95 @@ impl ScheduleGraph {
error!("{}", message);
}

/// Get topology sorted [`NodeId`], also ensures the graph contains no cycle
/// returns Err(()) if there are cycles
fn topsort_graph(&self, graph: &DiGraphMap<NodeId, ()>) -> Result<Vec<NodeId>, ()> {
// tarjan_scc's run order is reverse topological order
let mut rev_top_sorted_nodes = Vec::<NodeId>::with_capacity(graph.node_count());
let mut tarjan_scc = bevy_utils::petgraph::algo::TarjanScc::new();
let mut sccs_with_cycle = Vec::<Vec<NodeId>>::new();
/// Tries to topologically sort `graph`.
///
/// If the graph is acyclic, returns [`Ok`] with the list of [`NodeId`] in a valid
/// topological order. If the graph contains cycles, returns [`Err`] with the list of
/// strongly-connected components that contain cycles (also in a valid topological order).
///
/// # Errors
///
/// If the graph contain cycles, then an error is returned.
fn topsort_graph(
&self,
graph: &DiGraphMap<NodeId, ()>,
report: ReportCycles,
) -> Result<Vec<NodeId>, Vec<Vec<NodeId>>> {
// Tarjan's SCC algorithm returns elements in *reverse* topological order.
let mut tarjan_scc = TarjanScc::new();
let mut top_sorted_nodes = Vec::with_capacity(graph.node_count());
let mut sccs_with_cycles = Vec::new();

tarjan_scc.run(graph, |scc| {
// by scc's definition, each scc is the cluster of nodes that they can reach each other
// so scc with size larger than 1, means there is/are cycle(s).
// A strongly-connected component is a group of nodes who can all reach each other
// through one or more paths. If an SCC contains more than one node, there must be
// at least one cycle within them.
if scc.len() > 1 {
sccs_with_cycle.push(scc.to_vec());
sccs_with_cycles.push(scc.to_vec());
}
rev_top_sorted_nodes.extend_from_slice(scc);
top_sorted_nodes.extend_from_slice(scc);
});

if sccs_with_cycle.is_empty() {
// reverse the reverted to get topological order
let mut top_sorted_nodes = rev_top_sorted_nodes;
if sccs_with_cycles.is_empty() {
// reverse to get topological order
top_sorted_nodes.reverse();
Ok(top_sorted_nodes)
} else {
self.report_cycles(&sccs_with_cycle);
Err(())
let mut cycles = Vec::new();
for scc in &sccs_with_cycles {
cycles.append(&mut simple_cycles_in_component(graph, scc));
}

match report {
ReportCycles::Hierarchy => self.report_hierarchy_cycles(&cycles),
ReportCycles::Dependency => self.report_dependency_cycles(&cycles),
}

Err(sccs_with_cycles)
}
}

/// Print detailed cycle messages
fn report_cycles(&self, sccs_with_cycles: &[Vec<NodeId>]) {
let mut message = format!(
"schedule contains at least {} cycle(s)",
sccs_with_cycles.len()
);
/// Logs details of cycles in the hierarchy graph.
fn report_hierarchy_cycles(&self, cycles: &[Vec<NodeId>]) {
let mut message = format!("schedule has {} in_set cycle(s):\n", cycles.len());
for (i, cycle) in cycles.iter().enumerate() {
let mut names = cycle.iter().map(|id| self.get_node_name(id));
let first_name = names.next().unwrap();
writeln!(
message,
"cycle {}: set '{first_name}' contains itself",
i + 1,
)
.unwrap();
writeln!(message, "set '{first_name}'").unwrap();
for name in names.chain(std::iter::once(first_name)) {
writeln!(message, " ... which contains set '{name}'").unwrap();
}
writeln!(message).unwrap();
}

error!("{}", message);
}

writeln!(message, " -- cycle(s) found within:").unwrap();
for (i, scc) in sccs_with_cycles.iter().enumerate() {
let names = scc
/// Logs details of cycles in the dependency graph.
fn report_dependency_cycles(&self, cycles: &[Vec<NodeId>]) {
let mut message = format!("schedule has {} before/after cycle(s):\n", cycles.len());
for (i, cycle) in cycles.iter().enumerate() {
let mut names = cycle
.iter()
.map(|id| self.get_node_name(id))
.collect::<Vec<_>>();
writeln!(message, " ---- {i}: {names:?}").unwrap();
.map(|id| (self.get_node_kind(id), self.get_node_name(id)));
let (first_kind, first_name) = names.next().unwrap();
writeln!(
message,
"cycle {}: {first_kind} '{first_name}' must run before itself",
i + 1,
)
.unwrap();
writeln!(message, "{first_kind} '{first_name}'").unwrap();
for (kind, name) in names.chain(std::iter::once((first_kind, first_name))) {
writeln!(message, " ... which must run before {kind} '{name}'").unwrap();
}
writeln!(message).unwrap();
}

error!("{}", message);
Expand Down