1use std::{any::TypeId, borrow::Cow, ops::Deref};
3
4use bevy::{
5 ecs::{
6 schedule::{
7 InternedScheduleLabel, InternedSystemSet, NodeId, Schedule, ScheduleLabel, SystemSet,
8 },
9 system::{System, SystemInput},
10 },
11 platform::collections::{HashMap, HashSet},
12 reflect::Reflect,
13};
14use dot_writer::{Attributes, DotWriter};
15
16#[derive(Reflect, Debug, Clone)]
17#[reflect(opaque)]
18pub struct ReflectSystem {
20 pub(crate) name: Cow<'static, str>,
21 pub(crate) type_id: TypeId,
22 pub(crate) node_id: ReflectNodeId,
23 pub(crate) default_system_sets: Vec<InternedSystemSet>,
24}
25
26impl ReflectSystem {
27 pub fn name(&self) -> &str {
29 self.name.as_ref()
30 }
31
32 pub fn type_id(&self) -> TypeId {
34 self.type_id
35 }
36
37 pub fn node_id(&self) -> NodeId {
39 self.node_id.0
40 }
41
42 pub fn default_system_sets(&self) -> &[InternedSystemSet] {
44 &self.default_system_sets
45 }
46 pub fn from_system<In: SystemInput + 'static, Out: 'static>(
48 system: &dyn System<In = In, Out = Out>,
49 node_id: NodeId,
50 ) -> Self {
51 ReflectSystem {
52 name: system.name().clone(),
53 type_id: system.type_id(),
54 node_id: ReflectNodeId(node_id),
55 default_system_sets: system.default_system_sets(),
56 }
57 }
58
59 pub fn identifier(&self) -> &str {
61 if self.name.contains("<") {
63 self.name
64 .split("<")
65 .next()
66 .unwrap_or_default()
67 .split("::")
68 .last()
69 .unwrap_or_default()
70 } else {
71 self.name.split("::").last().unwrap_or_default()
72 }
73 }
74
75 pub fn path(&self) -> &str {
77 self.name.as_ref()
78 }
79}
80
81#[derive(Reflect, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
82#[reflect(opaque)]
83pub(crate) struct ReflectNodeId(pub NodeId);
84
85#[derive(Reflect, Clone, Debug)]
87pub struct ReflectSchedule {
88 type_path: &'static str,
90 label: ReflectableScheduleLabel,
91}
92
93#[derive(Reflect, Clone, Debug)]
94#[reflect(opaque)]
95struct ReflectableScheduleLabel(InternedScheduleLabel);
96
97impl Deref for ReflectableScheduleLabel {
98 type Target = InternedScheduleLabel;
99
100 fn deref(&self) -> &Self::Target {
101 &self.0
102 }
103}
104
105impl From<InternedScheduleLabel> for ReflectableScheduleLabel {
106 fn from(label: InternedScheduleLabel) -> Self {
107 Self(label)
108 }
109}
110
111impl ReflectSchedule {
112 pub fn type_path(&self) -> &'static str {
114 self.type_path
115 }
116
117 pub fn identifier(&self) -> &'static str {
119 self.type_path.split("::").last().unwrap_or_default()
120 }
121
122 pub fn label(&self) -> &InternedScheduleLabel {
124 &self.label
125 }
126
127 pub fn from_label<T: ScheduleLabel + 'static>(label: T) -> Self {
129 ReflectSchedule {
130 type_path: std::any::type_name::<T>(),
131 label: label.intern().into(),
132 }
133 }
134}
135
136#[derive(Reflect)]
137pub struct ReflectSystemSet {
139 node_id: ReflectNodeId,
141
142 debug: String,
144
145 type_id: Option<TypeId>,
147}
148
149impl ReflectSystemSet {
150 pub fn from_set(set: &dyn SystemSet, node_id: NodeId) -> Self {
152 ReflectSystemSet {
153 node_id: ReflectNodeId(node_id),
154 debug: format!("{set:?}"),
155 type_id: set.system_type(),
156 }
157 }
158}
159
160pub fn schedule_to_dot_graph(schedule: &Schedule) -> String {
162 let graph = schedule_to_reflect_graph(schedule);
163 reflect_graph_to_dot(graph)
164}
165
166pub fn reflect_graph_to_dot(graph: ReflectSystemGraph) -> String {
168 let mut output_bytes = Vec::new();
172 let mut writer = DotWriter::from(&mut output_bytes);
173 {
174 let mut writer = writer.digraph();
175
176 let mut node_id_map = HashMap::new();
177 for node in graph.nodes {
178 match node {
179 ReflectSystemGraphNode::System(reflect_system) => {
180 let mut node = writer.node_auto();
181
182 node.set_label(&reflect_system.name);
183 node_id_map.insert(reflect_system.node_id, node.id());
184 }
185 ReflectSystemGraphNode::SystemSet(reflect_system_set) => {
186 let name = if reflect_system_set.type_id.is_some() {
187 "SystemTypeSet".to_owned()
189 } else {
190 format!("SystemSet {}", reflect_system_set.debug)
191 };
192
193 let mut node = writer.node_auto();
194 node.set_label(&name);
195 node_id_map.insert(reflect_system_set.node_id, node.id());
196 }
197 }
198 }
199
200 for edge in graph.hierarchy {
202 let from = node_id_map.get(&edge.from).cloned().unwrap_or_else(|| {
203 let mut unknown = writer.node_auto();
204 unknown.set_label(&format!("unknown_parent {:?}", edge.from.0));
205 let id = unknown.id();
206 node_id_map.insert(edge.from, id.clone());
207 id
208 });
209 let to = node_id_map.get(&edge.to).cloned().unwrap_or_else(|| {
210 let mut unknown = writer.node_auto();
211 unknown.set_label(&format!("unknown_child {:?}", edge.to.0));
212 let id = unknown.id();
213 node_id_map.insert(edge.to, id.clone());
214 id
215 });
216 writer
217 .edge(to, from)
218 .attributes()
219 .set_color(dot_writer::Color::Red)
220 .set_label("child of")
221 .set_arrow_head(dot_writer::ArrowType::Diamond);
222 }
223 for edge in graph.dependencies {
225 let from = node_id_map.get(&edge.from).cloned().unwrap_or_else(|| {
226 let mut unknown = writer.node_auto();
227 unknown.set_label(&format!("unknown_dependant {:?}", edge.from.0));
228 let id = unknown.id();
229 node_id_map.insert(edge.from, id.clone());
230 id
231 });
232 let to = node_id_map.get(&edge.to).cloned().unwrap_or_else(|| {
233 let mut unknown = writer.node_auto();
234 unknown.set_label(&format!("unknown_dependency {:?}", edge.to.0));
235 let id = unknown.id();
236 node_id_map.insert(edge.to, id.clone());
237 id
238 });
239 writer
240 .edge(from, to)
241 .attributes()
242 .set_color(dot_writer::Color::Blue)
243 .set_label("runs before")
244 .set_arrow_head(dot_writer::ArrowType::Normal);
245 }
246 }
247
248 String::from_utf8(output_bytes).unwrap_or_default()
249}
250
251pub fn schedule_to_reflect_graph(schedule: &Schedule) -> ReflectSystemGraph {
253 let graph = schedule.graph();
254 let hierarchy = graph.hierarchy().graph();
255 let dependency = graph.dependency().graph();
256
257 let mut nodes = Vec::new();
258 let mut covered_nodes = HashSet::new();
259 for (node_id, system_set, _) in graph.system_sets() {
260 covered_nodes.insert(node_id);
261 nodes.push(ReflectSystemGraphNode::SystemSet(
262 ReflectSystemSet::from_set(system_set, node_id),
263 ));
264 }
265
266 if let Ok(systems) = schedule.systems() {
268 for (node_id, system) in systems {
269 covered_nodes.insert(node_id);
270 nodes.push(ReflectSystemGraphNode::System(ReflectSystem::from_system(
271 system.as_ref(),
272 node_id,
273 )));
274 }
275 }
276
277 for node_id in hierarchy.nodes() {
280 if covered_nodes.contains(&node_id) {
281 continue;
282 }
283
284 bevy::log::warn!("Found uncovered node {node_id:?}");
285 }
286
287 let dependencies = dependency
288 .all_edges()
289 .map(|(from, to)| Edge {
290 from: ReflectNodeId(from),
291 to: ReflectNodeId(to),
292 })
293 .collect();
294
295 let hierarchy = hierarchy
296 .all_edges()
297 .map(|(from, to)| Edge {
298 from: ReflectNodeId(from),
299 to: ReflectNodeId(to),
300 })
301 .collect();
302
303 ReflectSystemGraph {
304 schedule: ReflectSchedule::from_label(schedule.label()),
305 nodes,
306 dependencies,
307 hierarchy,
308 }
309}
310
311#[derive(Reflect)]
313pub struct ReflectSystemGraph {
314 schedule: ReflectSchedule,
316 nodes: Vec<ReflectSystemGraphNode>,
318
319 dependencies: Vec<Edge>,
323
324 hierarchy: Vec<Edge>,
327}
328
329impl ReflectSystemGraph {
330 pub fn sort(&mut self) {
334 self.nodes.sort_by_key(|node| match node {
336 ReflectSystemGraphNode::System(system) => system.node_id.0,
337 ReflectSystemGraphNode::SystemSet(system_set) => system_set.node_id.0,
338 });
339
340 self.dependencies.sort();
341
342 self.hierarchy.sort();
343 }
344
345 fn absorb_set(&mut self, node_id: NodeId) {
347 let mut hierarchy_parents = Vec::new();
349 let mut hierarchy_children = Vec::new();
350 for edge in &self.hierarchy {
352 if edge.to.0 == node_id {
354 hierarchy_children.push(edge.from.clone());
355 }
356 if edge.from.0 == node_id {
358 hierarchy_parents.push(edge.to.clone());
359 }
360 }
361
362 let mut dependencies = Vec::new();
364 let mut dependents = Vec::new();
365 for edge in &self.dependencies {
367 if edge.to.0 == node_id {
368 dependencies.push(edge.from.clone());
369 }
370 if edge.from.0 == node_id {
371 dependents.push(edge.to.clone());
372 }
373 }
374
375 let mut new_hierarchy_edges =
376 HashSet::with_capacity(hierarchy_parents.len() * hierarchy_children.len());
377 let mut new_dependency_edges =
378 HashSet::with_capacity(dependencies.len() * dependents.len());
379
380 for parent in hierarchy_parents.iter() {
382 for child in hierarchy_children.iter() {
383 new_hierarchy_edges.insert(Edge {
384 from: child.clone(),
385 to: parent.clone(),
386 });
387 }
388 }
389
390 for child in hierarchy_parents.iter() {
391 for dependency in dependencies.iter() {
393 new_dependency_edges.insert(Edge {
394 from: dependency.clone(),
395 to: child.clone(),
396 });
397 }
398
399 for dependent in dependents.iter() {
400 new_dependency_edges.insert(Edge {
401 from: child.clone(),
402 to: dependent.clone(),
403 });
404 }
405 }
406
407 self.hierarchy
409 .retain(|edge| edge.from.0 != node_id && edge.to.0 != node_id);
410 self.dependencies
411 .retain(|edge| edge.from.0 != node_id && edge.to.0 != node_id);
412
413 self.nodes.retain(|node| match node {
415 ReflectSystemGraphNode::SystemSet(system_set) => system_set.node_id.0 != node_id,
416 _ => true,
417 });
418
419 self.hierarchy.extend(new_hierarchy_edges);
421 self.dependencies.extend(new_dependency_edges);
422 }
423
424 pub fn absorb_type_system_sets(&mut self) {
430 let type_sets = self
431 .nodes
432 .iter()
433 .filter_map(|node| match node {
434 ReflectSystemGraphNode::SystemSet(system_set) => {
435 if system_set.type_id.is_some() {
436 Some(system_set.node_id.0)
437 } else {
438 None
439 }
440 }
441 _ => None,
442 })
443 .collect::<Vec<_>>();
444
445 for node_id in type_sets {
447 self.absorb_set(node_id);
448 }
449 }
450}
451
452#[derive(Reflect)]
454pub enum ReflectSystemGraphNode {
455 System(ReflectSystem),
457 SystemSet(ReflectSystemSet),
459}
460
461#[derive(Reflect, PartialEq, Eq, PartialOrd, Ord, Hash)]
463pub struct Edge {
464 from: ReflectNodeId,
466 to: ReflectNodeId,
468}
469
470#[cfg(test)]
471mod test {
472 use bevy::{app::Update, ecs::world::World, prelude::IntoScheduleConfigs};
473
474 use super::*;
475
476 fn system_a() {}
477
478 fn system_b() {}
479
480 fn system_c() {}
481
482 fn system_d() {}
483
484 fn system_e() {}
485
486 const BLESS_MODE: bool = false;
487
488 #[test]
489 fn test_graph_is_as_expected() {
490 let mut schedule = Schedule::new(Update);
493
494 #[derive(SystemSet, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
495 enum SystemSet {
496 SystemSetG,
497 SystemSetH,
498 }
499
500 schedule
503 .add_systems(system_a)
504 .add_systems(system_b.before(system_a))
505 .add_systems(system_c.after(system_b).before(SystemSet::SystemSetH))
506 .add_systems(system_d.in_set(SystemSet::SystemSetG))
507 .add_systems(system_e.in_set(SystemSet::SystemSetH))
508 .configure_sets(SystemSet::SystemSetG.after(SystemSet::SystemSetH));
509 let mut world = World::new();
510 schedule.initialize(&mut world).unwrap();
511
512 let mut graph = schedule_to_reflect_graph(&schedule);
513 graph.absorb_type_system_sets();
514 graph.sort();
515 let dot = reflect_graph_to_dot(graph);
516
517 let normalize = |s: &str| {
518 let lines: Vec<&str> = s.lines().map(|line| line.trim_start()).collect();
520 lines
521 .join("\n")
522 .replace(" = ", "")
523 .replace(";", "")
524 .replace(",", "")
525 .trim()
526 .to_string()
527 };
528
529 let expected = include_str!("../test_graph.dot");
532 let expected_path = manifest_dir_macros::file_path!("test_graph.dot");
533
534 if BLESS_MODE {
535 std::fs::write(expected_path, normalize(&dot)).unwrap();
536 panic!("Bless mode is active");
537 } else {
538 pretty_assertions::assert_eq!(normalize(&dot), normalize(expected));
539 }
540 }
541}