use crate::{dot, dot::DotGraph};
use bevy_app::{App, AppLabel, StartupSchedule};
use bevy_ecs::{
component::ComponentId,
prelude::*,
schedule::{GraphNode, StageLabelId, SystemContainer, SystemLabelId},
};
use pretty_type_name::pretty_type_name_str;
pub fn schedule_graph_dot(schedule: &App) -> String {
let default_style = ScheduleGraphStyle::dark();
schedule_graph_dot_styled(schedule, &default_style)
}
#[non_exhaustive]
pub struct SystemInfo<'a> {
pub name: &'a str,
}
pub struct ScheduleGraphStyle {
pub fontsize: f32,
pub fontname: String,
pub bgcolor: String,
pub bgcolor_nested_schedule: String,
pub bgcolor_stage: String,
pub color_system: String,
pub color_edge: String,
pub hide_startup_schedule: bool,
#[allow(clippy::type_complexity)]
pub system_filter: Option<Box<dyn Fn(&SystemInfo) -> bool>>,
}
impl ScheduleGraphStyle {
pub fn light() -> Self {
ScheduleGraphStyle {
fontsize: 16.0,
fontname: "Helvetica".into(),
bgcolor: "white".into(),
bgcolor_nested_schedule: "#d1d5da".into(),
bgcolor_stage: "#e1e5ea".into(),
color_system: "white".into(),
color_edge: "black".into(),
hide_startup_schedule: true,
system_filter: None,
}
}
pub fn dark() -> Self {
ScheduleGraphStyle {
fontsize: 16.0,
fontname: "Helvetica".into(),
bgcolor: "#35393F".into(),
bgcolor_nested_schedule: "#D0E1ED".into(),
bgcolor_stage: "#99aab5".into(),
color_system: "#eff1f3".into(),
color_edge: "white".into(),
hide_startup_schedule: true,
system_filter: None,
}
}
}
impl Default for ScheduleGraphStyle {
fn default() -> Self {
ScheduleGraphStyle::dark()
}
}
pub fn schedule_graph_dot_styled(app: &App, style: &ScheduleGraphStyle) -> String {
schedule_graph_dot_styled_inner(app, None, style)
}
pub fn schedule_graph_dot_sub_app_styled(
app: &App,
label: impl AppLabel,
stages_using_main_world: &[&dyn StageLabel],
style: &ScheduleGraphStyle,
) -> String {
schedule_graph_dot_styled_inner(
app.sub_app(label),
Some((&app.world, stages_using_main_world)),
style,
)
}
fn schedule_graph_dot_styled_inner(
app: &App,
use_world_info_for_stages: Option<(&World, &[&dyn StageLabel])>,
style: &ScheduleGraphStyle,
) -> String {
let mut graph = DotGraph::new(
"schedule",
"digraph",
&[
("fontsize", &style.fontsize.to_string()),
("fontname", &style.fontname),
("rankdir", "LR"),
("nodesep", "0.05"),
("bgcolor", &style.bgcolor),
("compound", "true"),
],
)
.node_attributes(&[("shape", "box"), ("margin", "0"), ("height", "0.4")])
.edge_attributes(&[("color", &style.color_edge)]);
build_schedule_graph(
&mut graph,
app,
&app.schedule,
"schedule",
None,
use_world_info_for_stages,
style,
);
graph.finish()
}
fn build_schedule_graph(
graph: &mut DotGraph,
app: &App,
schedule: &Schedule,
schedule_name: &str,
marker_node_id: Option<&str>,
use_world_info_for_stages: Option<(&World, &[&dyn StageLabel])>,
style: &ScheduleGraphStyle,
) {
if let Some(marker_id) = marker_node_id {
graph.add_invisible_node(marker_id);
}
let is_startup_schedule = |stage_name: StageLabelId| stage_name == StartupSchedule.as_label();
for (stage_name, stage) in schedule.iter_stages() {
if let Some(system_stage) = stage.downcast_ref::<SystemStage>() {
let subgraph = system_stage_subgraph(
&app.world,
schedule_name,
stage_name,
system_stage,
use_world_info_for_stages,
style,
);
graph.add_sub_graph(subgraph);
} else if let Some(schedule) = stage.downcast_ref::<Schedule>() {
if style.hide_startup_schedule && is_startup_schedule(stage_name) {
continue;
}
let name = format!("cluster_{:?}", stage_name);
let marker_id = marker_id(schedule_name, stage_name);
let stage_name_str = format!("{:?}", stage_name);
let mut schedule_sub_graph = DotGraph::new(
&name,
"subgraph",
&[
("label", &stage_name_str),
("fontsize", "20"),
("constraint", "false"),
("rankdir", "LR"),
("style", "rounded"),
("bgcolor", &style.bgcolor_nested_schedule),
],
)
.edge_attributes(&[("color", &style.color_edge)]);
build_schedule_graph(
&mut schedule_sub_graph,
app,
schedule,
&name,
Some(&marker_id),
use_world_info_for_stages,
style,
);
graph.add_sub_graph(schedule_sub_graph);
} else {
eprintln!("Missing downcast: {:?}", stage_name);
}
}
let iter_a = schedule
.iter_stages()
.filter(|(stage, _)| !style.hide_startup_schedule || !is_startup_schedule(*stage));
let iter_b = schedule
.iter_stages()
.filter(|(stage, _)| !style.hide_startup_schedule || !is_startup_schedule(*stage))
.skip(1);
for ((a, _), (b, _)) in iter_a.zip(iter_b) {
let a = marker_id(schedule_name, a);
let b = marker_id(schedule_name, b);
graph.add_edge(&a, &b, &[]);
}
}
fn marker_id(schedule_name: &str, stage_name: StageLabelId) -> String {
format!("MARKER_{}_{:?}", schedule_name, stage_name)
}
fn system_stage_subgraph(
world: &World,
schedule_name: &str,
stage_name: StageLabelId,
system_stage: &SystemStage,
use_world_info_for_stages: Option<(&World, &[&dyn StageLabel])>,
style: &ScheduleGraphStyle,
) -> DotGraph {
let stage_name_str = stage_name.as_str();
let mut sub = DotGraph::new(
&format!("cluster_{:?}", stage_name.as_str()),
"subgraph",
&[
("style", "rounded"),
("color", &style.bgcolor_stage),
("bgcolor", &style.bgcolor_stage),
("rankdir", "TD"),
("label", stage_name_str),
],
)
.node_attributes(&[
("style", "filled"),
("color", &style.color_system),
("bgcolor", &style.color_system),
]);
sub.add_invisible_node(&marker_id(schedule_name, stage_name));
let relevant_world = match use_world_info_for_stages {
Some((relevant_world, stages))
if stages.iter().any(|stage| stage.as_label() == stage_name) =>
{
relevant_world
}
_ => world,
};
add_systems_to_graph(
&mut sub,
relevant_world,
schedule_name,
SystemKind::ExclusiveStart,
system_stage.exclusive_at_start_systems(),
style,
);
add_systems_to_graph(
&mut sub,
relevant_world,
schedule_name,
SystemKind::ExclusiveBeforeCommands,
system_stage.exclusive_before_commands_systems(),
style,
);
add_systems_to_graph(
&mut sub,
relevant_world,
schedule_name,
SystemKind::Parallel,
system_stage.parallel_systems(),
style,
);
add_systems_to_graph(
&mut sub,
relevant_world,
schedule_name,
SystemKind::ExclusiveEnd,
system_stage.exclusive_at_end_systems(),
style,
);
sub
}
enum SystemKind {
ExclusiveStart,
ExclusiveEnd,
ExclusiveBeforeCommands,
Parallel,
}
fn add_systems_to_graph(
graph: &mut DotGraph,
world: &World,
schedule_name: &str,
kind: SystemKind,
systems: &[SystemContainer],
style: &ScheduleGraphStyle,
) {
let mut systems: Vec<_> = systems.iter().collect();
systems.sort_by_key(|system| system.name());
if systems.is_empty() {
return;
}
for (i, &system_container) in systems.iter().enumerate() {
let id = node_id(schedule_name, system_container, i);
let system_name = system_container.name();
if let Some(filter) = &style.system_filter {
let info = SystemInfo {
name: system_name.as_ref(),
};
if !filter(&info) {
continue;
}
}
let short_system_name = pretty_type_name_str(&system_container.name());
let kind = match kind {
SystemKind::ExclusiveStart => Some("Exclusive at start"),
SystemKind::ExclusiveEnd => Some("Exclusive at end"),
SystemKind::ExclusiveBeforeCommands => Some("Exclusive before commands"),
SystemKind::Parallel => None,
};
let label = match kind {
Some(kind) => {
format!(
r#"<{}<BR />{}>"#,
&dot::html_escape(&short_system_name),
dot::font_tag(kind, "red", 11),
)
}
None => short_system_name,
};
let tooltip = system_tooltip(system_container, world);
graph.add_node(&id, &[("label", &label), ("tooltip", &tooltip)]);
add_dependency_labels(
graph,
schedule_name,
&id,
SystemDirection::Before,
system_container.before(),
&systems,
);
add_dependency_labels(
graph,
schedule_name,
&id,
SystemDirection::After,
system_container.after(),
&systems,
);
}
}
fn system_tooltip(system_container: &SystemContainer, world: &World) -> String {
let mut tooltip = String::new();
let truncate_in_place =
|tooltip: &mut String, end: &str| tooltip.truncate(tooltip.trim_end_matches(end).len());
let components = world.components();
let name_of_component = |id| {
pretty_type_name_str(
components
.get_info(id)
.map_or_else(|| "<missing>", |info| info.name()),
)
};
let is_resource = |id: &ComponentId| world.storages().resources.get(*id).is_some();
let component_access = system_container.component_access();
let (read_resources, read_components): (Vec<_>, Vec<_>) =
component_access.reads().partition(is_resource);
let (write_resources, write_components): (Vec<_>, Vec<_>) =
component_access.writes().partition(is_resource);
let mut list = |name, components: &[ComponentId]| {
if components.is_empty() {
return;
}
tooltip.push_str(name);
tooltip.push_str(" [");
for read_resource in components {
tooltip.push_str(&name_of_component(*read_resource));
tooltip.push_str(", ");
}
truncate_in_place(&mut tooltip, ", ");
tooltip.push_str("]\\n");
};
list("Components", &read_components);
list("ComponentsMut", &write_components);
list("Res", &read_resources);
list("ResMut", &write_resources);
if tooltip.is_empty() {
pretty_type_name_str(&system_container.name())
} else {
tooltip
}
}
enum SystemDirection {
Before,
After,
}
fn add_dependency_labels(
graph: &mut DotGraph,
schedule_name: &str,
system_node_id: &str,
direction: SystemDirection,
requirements: &[SystemLabelId],
other_systems: &[&SystemContainer],
) {
for requirement in requirements {
let mut found = false;
for (i, &dependency) in other_systems
.iter()
.enumerate()
.filter(|(_, node)| node.labels().contains(requirement))
{
found = true;
let me = system_node_id;
let other = node_id(schedule_name, dependency, i);
match direction {
SystemDirection::Before => graph.add_edge(me, &other, &[("constraint", "false")]),
SystemDirection::After => graph.add_edge(&other, me, &[("constraint", "false")]),
}
}
assert!(found);
}
}
fn node_id(schedule_name: &str, system: &SystemContainer, i: usize) -> String {
format!("{}_{}_{}", schedule_name, system.name(), i)
}