use std::{any::TypeId, borrow::Cow, ops::Deref};
use ::{
bevy_ecs::{
schedule::{
InternedScheduleLabel, InternedSystemSet, NodeId, Schedule, ScheduleLabel, SystemSet,
},
system::{System, SystemInput},
},
bevy_platform::collections::{HashMap, HashSet},
bevy_reflect::Reflect,
};
use bevy_ecs::schedule::SystemKey;
use bevy_log::warn;
use dot_writer::{Attributes, DotWriter};
#[derive(Reflect, Debug, Clone)]
#[reflect(opaque)]
pub struct ReflectSystem {
pub(crate) name: Cow<'static, str>,
pub(crate) type_id: TypeId,
pub(crate) node_id: ReflectNodeId,
pub(crate) default_system_sets: Vec<InternedSystemSet>,
}
impl ReflectSystem {
pub fn name(&self) -> &str {
self.name.as_ref()
}
pub fn type_id(&self) -> TypeId {
self.type_id
}
pub fn node_id(&self) -> NodeId {
self.node_id.0
}
pub fn default_system_sets(&self) -> &[InternedSystemSet] {
&self.default_system_sets
}
pub fn from_system<In: SystemInput + 'static, Out: 'static>(
system: &dyn System<In = In, Out = Out>,
node_id: SystemKey,
) -> Self {
ReflectSystem {
name: system.name().into(),
type_id: system.type_id(),
node_id: ReflectNodeId(NodeId::System(node_id)),
default_system_sets: system.default_system_sets(),
}
}
pub fn identifier(&self) -> &str {
if self.name.contains("<") {
self.name
.split("<")
.next()
.unwrap_or_default()
.split("::")
.last()
.unwrap_or_default()
} else {
self.name.split("::").last().unwrap_or_default()
}
}
pub fn path(&self) -> &str {
self.name.as_ref()
}
}
#[derive(Reflect, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[reflect(opaque)]
pub(crate) struct ReflectNodeId(pub NodeId);
#[derive(Reflect, Clone, Debug)]
pub struct ReflectSchedule {
type_path: &'static str,
label: ReflectableScheduleLabel,
}
#[derive(Reflect, Clone, Debug)]
#[reflect(opaque)]
struct ReflectableScheduleLabel(InternedScheduleLabel);
impl Deref for ReflectableScheduleLabel {
type Target = InternedScheduleLabel;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<InternedScheduleLabel> for ReflectableScheduleLabel {
fn from(label: InternedScheduleLabel) -> Self {
Self(label)
}
}
impl ReflectSchedule {
pub fn type_path(&self) -> &'static str {
self.type_path
}
pub fn identifier(&self) -> &'static str {
self.type_path.split("::").last().unwrap_or_default()
}
pub fn label(&self) -> &InternedScheduleLabel {
&self.label
}
pub fn from_label<T: ScheduleLabel + 'static>(label: T) -> Self {
ReflectSchedule {
type_path: std::any::type_name::<T>(),
label: label.intern().into(),
}
}
}
#[derive(Reflect)]
pub struct ReflectSystemSet {
node_id: ReflectNodeId,
debug: String,
type_id: Option<TypeId>,
}
impl ReflectSystemSet {
pub fn from_set(set: &dyn SystemSet, node_id: NodeId) -> Self {
ReflectSystemSet {
node_id: ReflectNodeId(node_id),
debug: format!("{set:?}"),
type_id: set.system_type(),
}
}
}
pub fn schedule_to_dot_graph(schedule: &Schedule) -> String {
let graph = schedule_to_reflect_graph(schedule);
reflect_graph_to_dot(graph)
}
pub fn reflect_graph_to_dot(graph: ReflectSystemGraph) -> String {
let mut output_bytes = Vec::new();
let mut writer = DotWriter::from(&mut output_bytes);
{
let mut writer = writer.digraph();
let mut node_id_map = HashMap::new();
for node in graph.nodes {
match node {
ReflectSystemGraphNode::System(reflect_system) => {
let mut node = writer.node_auto();
node.set_label(&reflect_system.name);
node_id_map.insert(reflect_system.node_id, node.id());
}
ReflectSystemGraphNode::SystemSet(reflect_system_set) => {
let name = if reflect_system_set.type_id.is_some() {
"SystemTypeSet".to_owned()
} else {
format!("SystemSet {}", reflect_system_set.debug)
};
let mut node = writer.node_auto();
node.set_label(&name);
node_id_map.insert(reflect_system_set.node_id, node.id());
}
}
}
for edge in graph.hierarchy {
let from = node_id_map.get(&edge.from).cloned().unwrap_or_else(|| {
let mut unknown = writer.node_auto();
unknown.set_label(&format!("unknown_parent {:?}", edge.from.0));
let id = unknown.id();
node_id_map.insert(edge.from, id.clone());
id
});
let to = node_id_map.get(&edge.to).cloned().unwrap_or_else(|| {
let mut unknown = writer.node_auto();
unknown.set_label(&format!("unknown_child {:?}", edge.to.0));
let id = unknown.id();
node_id_map.insert(edge.to, id.clone());
id
});
writer
.edge(to, from)
.attributes()
.set_color(dot_writer::Color::Red)
.set_label("child of")
.set_arrow_head(dot_writer::ArrowType::Diamond);
}
for edge in graph.dependencies {
let from = node_id_map.get(&edge.from).cloned().unwrap_or_else(|| {
let mut unknown = writer.node_auto();
unknown.set_label(&format!("unknown_dependant {:?}", edge.from.0));
let id = unknown.id();
node_id_map.insert(edge.from, id.clone());
id
});
let to = node_id_map.get(&edge.to).cloned().unwrap_or_else(|| {
let mut unknown = writer.node_auto();
unknown.set_label(&format!("unknown_dependency {:?}", edge.to.0));
let id = unknown.id();
node_id_map.insert(edge.to, id.clone());
id
});
writer
.edge(from, to)
.attributes()
.set_color(dot_writer::Color::Blue)
.set_label("runs before")
.set_arrow_head(dot_writer::ArrowType::Normal);
}
}
String::from_utf8(output_bytes).unwrap_or_default()
}
pub fn schedule_to_reflect_graph(schedule: &Schedule) -> ReflectSystemGraph {
let graph = schedule.graph();
let hierarchy = graph.hierarchy().graph();
let dependency = graph.dependency().graph();
let mut nodes = Vec::new();
let mut covered_nodes = HashSet::new();
for (node_id, system_set, _) in graph.system_sets.iter() {
covered_nodes.insert(NodeId::Set(node_id));
nodes.push(ReflectSystemGraphNode::SystemSet(
ReflectSystemSet::from_set(system_set, NodeId::Set(node_id)),
));
}
if let Ok(systems) = schedule.systems() {
for (node_id, system) in systems {
covered_nodes.insert(NodeId::System(node_id));
nodes.push(ReflectSystemGraphNode::System(ReflectSystem::from_system(
system.as_ref(),
node_id,
)));
}
}
for node_id in hierarchy.nodes() {
if covered_nodes.contains(&node_id) {
continue;
}
warn!("Found uncovered node {node_id:?}");
}
let dependencies = dependency
.all_edges()
.map(|(from, to)| Edge {
from: ReflectNodeId(from),
to: ReflectNodeId(to),
})
.collect();
let hierarchy = hierarchy
.all_edges()
.map(|(from, to)| Edge {
from: ReflectNodeId(from),
to: ReflectNodeId(to),
})
.collect();
ReflectSystemGraph {
schedule: ReflectSchedule::from_label(schedule.label()),
nodes,
dependencies,
hierarchy,
}
}
#[derive(Reflect)]
pub struct ReflectSystemGraph {
schedule: ReflectSchedule,
nodes: Vec<ReflectSystemGraphNode>,
dependencies: Vec<Edge>,
hierarchy: Vec<Edge>,
}
impl ReflectSystemGraph {
pub fn sort(&mut self) {
self.nodes.sort_by_key(|node| match node {
ReflectSystemGraphNode::System(system) => system.node_id.0,
ReflectSystemGraphNode::SystemSet(system_set) => system_set.node_id.0,
});
self.dependencies.sort();
self.hierarchy.sort();
}
fn absorb_set(&mut self, node_id: NodeId) {
let mut hierarchy_parents = Vec::new();
let mut hierarchy_children = Vec::new();
for edge in &self.hierarchy {
if edge.to.0 == node_id {
hierarchy_children.push(edge.from.clone());
}
if edge.from.0 == node_id {
hierarchy_parents.push(edge.to.clone());
}
}
let mut dependencies = Vec::new();
let mut dependents = Vec::new();
for edge in &self.dependencies {
if edge.to.0 == node_id {
dependencies.push(edge.from.clone());
}
if edge.from.0 == node_id {
dependents.push(edge.to.clone());
}
}
let mut new_hierarchy_edges =
HashSet::with_capacity(hierarchy_parents.len() * hierarchy_children.len());
let mut new_dependency_edges =
HashSet::with_capacity(dependencies.len() * dependents.len());
for parent in hierarchy_parents.iter() {
for child in hierarchy_children.iter() {
new_hierarchy_edges.insert(Edge {
from: child.clone(),
to: parent.clone(),
});
}
}
for child in hierarchy_parents.iter() {
for dependency in dependencies.iter() {
new_dependency_edges.insert(Edge {
from: dependency.clone(),
to: child.clone(),
});
}
for dependent in dependents.iter() {
new_dependency_edges.insert(Edge {
from: child.clone(),
to: dependent.clone(),
});
}
}
self.hierarchy
.retain(|edge| edge.from.0 != node_id && edge.to.0 != node_id);
self.dependencies
.retain(|edge| edge.from.0 != node_id && edge.to.0 != node_id);
self.nodes.retain(|node| match node {
ReflectSystemGraphNode::SystemSet(system_set) => system_set.node_id.0 != node_id,
_ => true,
});
self.hierarchy.extend(new_hierarchy_edges);
self.dependencies.extend(new_dependency_edges);
}
pub fn absorb_type_system_sets(&mut self) {
let type_sets = self
.nodes
.iter()
.filter_map(|node| match node {
ReflectSystemGraphNode::SystemSet(system_set) => {
if system_set.type_id.is_some() {
Some(system_set.node_id.0)
} else {
None
}
}
_ => None,
})
.collect::<Vec<_>>();
for node_id in type_sets {
self.absorb_set(node_id);
}
}
}
#[derive(Reflect)]
pub enum ReflectSystemGraphNode {
System(ReflectSystem),
SystemSet(ReflectSystemSet),
}
#[derive(Reflect, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Edge {
from: ReflectNodeId,
to: ReflectNodeId,
}
#[cfg(test)]
mod test {
use ::{
bevy_app::Update,
bevy_ecs::{schedule::IntoScheduleConfigs, world::World},
};
use super::*;
fn system_a() {}
fn system_b() {}
fn system_c() {}
fn system_d() {}
fn system_e() {}
#[test]
fn test_graph_is_as_expected() {
let mut schedule = Schedule::new(Update);
#[derive(SystemSet, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
enum SystemSet {
SystemSetG,
SystemSetH,
}
schedule
.add_systems(system_a)
.add_systems(system_b.before(system_a))
.add_systems(system_c.after(system_b).before(SystemSet::SystemSetH))
.add_systems(system_d.in_set(SystemSet::SystemSetG))
.add_systems(system_e.in_set(SystemSet::SystemSetH))
.configure_sets(SystemSet::SystemSetG.after(SystemSet::SystemSetH));
let mut world = World::new();
schedule.initialize(&mut world).unwrap();
let mut graph = schedule_to_reflect_graph(&schedule);
graph.absorb_type_system_sets();
graph.sort();
let dot = reflect_graph_to_dot(graph);
let normalize = |s: &str| {
let lines: Vec<&str> = s.lines().map(|line| line.trim_start()).collect();
lines
.join("\n")
.replace(" = ", "")
.replace(";", "")
.replace(",", "")
.trim()
.to_string()
};
let expected = include_str!("../test_graph.dot");
let expected_path = manifest_dir_macros::file_path!("test_graph.dot");
if std::env::var("BLESS_MODE").is_ok() {
std::fs::write(expected_path, normalize(&dot)).unwrap();
panic!("Bless mode is active");
} else {
pretty_assertions::assert_eq!(normalize(&dot), normalize(expected));
}
}
}