bevy_system_reflection/
lib.rs

1//! A visualiser for bevy system schedules, as well as utilities for querying them via reflection
2use std::{any::TypeId, borrow::Cow, ops::Deref};
3
4use ::{
5    bevy_ecs::{
6        schedule::{
7            InternedScheduleLabel, InternedSystemSet, NodeId, Schedule, ScheduleLabel, SystemSet,
8        },
9        system::{System, SystemInput},
10    },
11    bevy_platform::collections::{HashMap, HashSet},
12    bevy_reflect::Reflect,
13};
14use bevy_log::warn;
15use dot_writer::{Attributes, DotWriter};
16
17#[derive(Reflect, Debug, Clone)]
18#[reflect(opaque)]
19/// A reflectable system.
20pub struct ReflectSystem {
21    pub(crate) name: Cow<'static, str>,
22    pub(crate) type_id: TypeId,
23    pub(crate) node_id: ReflectNodeId,
24    pub(crate) default_system_sets: Vec<InternedSystemSet>,
25}
26
27impl ReflectSystem {
28    /// Retrieves the name of the system.
29    pub fn name(&self) -> &str {
30        self.name.as_ref()
31    }
32
33    /// Retrieves the type id of the system.
34    pub fn type_id(&self) -> TypeId {
35        self.type_id
36    }
37
38    /// Retrieves the node id of the system.
39    pub fn node_id(&self) -> NodeId {
40        self.node_id.0
41    }
42
43    /// Retrieves the default system sets of the system.
44    pub fn default_system_sets(&self) -> &[InternedSystemSet] {
45        &self.default_system_sets
46    }
47    /// Creates a reflect system from a system specification
48    pub fn from_system<In: SystemInput + 'static, Out: 'static>(
49        system: &dyn System<In = In, Out = Out>,
50        node_id: NodeId,
51    ) -> Self {
52        ReflectSystem {
53            name: system.name().clone(),
54            type_id: system.type_id(),
55            node_id: ReflectNodeId(node_id),
56            default_system_sets: system.default_system_sets(),
57        }
58    }
59
60    /// gets the short identifier of the system, i.e. just the function name
61    pub fn identifier(&self) -> &str {
62        // if it contains generics it might contain more than
63        if self.name.contains("<") {
64            self.name
65                .split("<")
66                .next()
67                .unwrap_or_default()
68                .split("::")
69                .last()
70                .unwrap_or_default()
71        } else {
72            self.name.split("::").last().unwrap_or_default()
73        }
74    }
75
76    /// gets the path of the system, i.e. the fully qualified function name
77    pub fn path(&self) -> &str {
78        self.name.as_ref()
79    }
80}
81
82#[derive(Reflect, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
83#[reflect(opaque)]
84pub(crate) struct ReflectNodeId(pub NodeId);
85
86/// A reflectable schedule.
87#[derive(Reflect, Clone, Debug)]
88pub struct ReflectSchedule {
89    /// The name of the schedule.
90    type_path: &'static str,
91    label: ReflectableScheduleLabel,
92}
93
94#[derive(Reflect, Clone, Debug)]
95#[reflect(opaque)]
96struct ReflectableScheduleLabel(InternedScheduleLabel);
97
98impl Deref for ReflectableScheduleLabel {
99    type Target = InternedScheduleLabel;
100
101    fn deref(&self) -> &Self::Target {
102        &self.0
103    }
104}
105
106impl From<InternedScheduleLabel> for ReflectableScheduleLabel {
107    fn from(label: InternedScheduleLabel) -> Self {
108        Self(label)
109    }
110}
111
112impl ReflectSchedule {
113    /// Retrieves the name of the schedule.
114    pub fn type_path(&self) -> &'static str {
115        self.type_path
116    }
117
118    /// Retrieves the short identifier of the schedule
119    pub fn identifier(&self) -> &'static str {
120        self.type_path.split("::").last().unwrap_or_default()
121    }
122
123    /// Retrieves the label of the schedule
124    pub fn label(&self) -> &InternedScheduleLabel {
125        &self.label
126    }
127
128    /// Creates a new reflect schedule from a schedule label
129    pub fn from_label<T: ScheduleLabel + 'static>(label: T) -> Self {
130        ReflectSchedule {
131            type_path: std::any::type_name::<T>(),
132            label: label.intern().into(),
133        }
134    }
135}
136
137#[derive(Reflect)]
138/// A reflectable system set.
139pub struct ReflectSystemSet {
140    /// The node id of the system set.
141    node_id: ReflectNodeId,
142
143    /// The debug print of the system set
144    debug: String,
145
146    /// If this is a typed system set, the type id
147    type_id: Option<TypeId>,
148}
149
150impl ReflectSystemSet {
151    /// Creates a reflect system set from a system set
152    pub fn from_set(set: &dyn SystemSet, node_id: NodeId) -> Self {
153        ReflectSystemSet {
154            node_id: ReflectNodeId(node_id),
155            debug: format!("{set:?}"),
156            type_id: set.system_type(),
157        }
158    }
159}
160
161/// Renders a schedule to a dot graph using the optimized schedule.
162pub fn schedule_to_dot_graph(schedule: &Schedule) -> String {
163    let graph = schedule_to_reflect_graph(schedule);
164    reflect_graph_to_dot(graph)
165}
166
167/// Renders a reflectable system graph to a dot graph
168pub fn reflect_graph_to_dot(graph: ReflectSystemGraph) -> String {
169    // create a dot graph with:
170    // - hierarchy represented by red composition arrows
171    // - dependencies represented by blue arrows
172    let mut output_bytes = Vec::new();
173    let mut writer = DotWriter::from(&mut output_bytes);
174    {
175        let mut writer = writer.digraph();
176
177        let mut node_id_map = HashMap::new();
178        for node in graph.nodes {
179            match node {
180                ReflectSystemGraphNode::System(reflect_system) => {
181                    let mut node = writer.node_auto();
182
183                    node.set_label(&reflect_system.name);
184                    node_id_map.insert(reflect_system.node_id, node.id());
185                }
186                ReflectSystemGraphNode::SystemSet(reflect_system_set) => {
187                    let name = if reflect_system_set.type_id.is_some() {
188                        // special sets, that pollute the graph, summize them as "system type set", each system gets one
189                        "SystemTypeSet".to_owned()
190                    } else {
191                        format!("SystemSet {}", reflect_system_set.debug)
192                    };
193
194                    let mut node = writer.node_auto();
195                    node.set_label(&name);
196                    node_id_map.insert(reflect_system_set.node_id, node.id());
197                }
198            }
199        }
200
201        // go through hierarchy edges
202        for edge in graph.hierarchy {
203            let from = node_id_map.get(&edge.from).cloned().unwrap_or_else(|| {
204                let mut unknown = writer.node_auto();
205                unknown.set_label(&format!("unknown_parent {:?}", edge.from.0));
206                let id = unknown.id();
207                node_id_map.insert(edge.from, id.clone());
208                id
209            });
210            let to = node_id_map.get(&edge.to).cloned().unwrap_or_else(|| {
211                let mut unknown = writer.node_auto();
212                unknown.set_label(&format!("unknown_child {:?}", edge.to.0));
213                let id = unknown.id();
214                node_id_map.insert(edge.to, id.clone());
215                id
216            });
217            writer
218                .edge(to, from)
219                .attributes()
220                .set_color(dot_writer::Color::Red)
221                .set_label("child of")
222                .set_arrow_head(dot_writer::ArrowType::Diamond);
223        }
224        // go through dependency edges
225        for edge in graph.dependencies {
226            let from = node_id_map.get(&edge.from).cloned().unwrap_or_else(|| {
227                let mut unknown = writer.node_auto();
228                unknown.set_label(&format!("unknown_dependant {:?}", edge.from.0));
229                let id = unknown.id();
230                node_id_map.insert(edge.from, id.clone());
231                id
232            });
233            let to = node_id_map.get(&edge.to).cloned().unwrap_or_else(|| {
234                let mut unknown = writer.node_auto();
235                unknown.set_label(&format!("unknown_dependency {:?}", edge.to.0));
236                let id = unknown.id();
237                node_id_map.insert(edge.to, id.clone());
238                id
239            });
240            writer
241                .edge(from, to)
242                .attributes()
243                .set_color(dot_writer::Color::Blue)
244                .set_label("runs before")
245                .set_arrow_head(dot_writer::ArrowType::Normal);
246        }
247    }
248
249    String::from_utf8(output_bytes).unwrap_or_default()
250}
251
252/// Converts a schedule to a reflectable system graph
253pub fn schedule_to_reflect_graph(schedule: &Schedule) -> ReflectSystemGraph {
254    let graph = schedule.graph();
255    let hierarchy = graph.hierarchy().graph();
256    let dependency = graph.dependency().graph();
257
258    let mut nodes = Vec::new();
259    let mut covered_nodes = HashSet::new();
260    for (node_id, system_set, _) in graph.system_sets() {
261        covered_nodes.insert(node_id);
262        nodes.push(ReflectSystemGraphNode::SystemSet(
263            ReflectSystemSet::from_set(system_set, node_id),
264        ));
265    }
266
267    // for some reason the graph doesn't contain these
268    if let Ok(systems) = schedule.systems() {
269        for (node_id, system) in systems {
270            covered_nodes.insert(node_id);
271            nodes.push(ReflectSystemGraphNode::System(ReflectSystem::from_system(
272                system.as_ref(),
273                node_id,
274            )));
275        }
276    }
277
278    // try find all uncovered nodes, and warn about them, or do something about them
279    // for now we just warn
280    for node_id in hierarchy.nodes() {
281        if covered_nodes.contains(&node_id) {
282            continue;
283        }
284
285        warn!("Found uncovered node {node_id:?}");
286    }
287
288    let dependencies = dependency
289        .all_edges()
290        .map(|(from, to)| Edge {
291            from: ReflectNodeId(from),
292            to: ReflectNodeId(to),
293        })
294        .collect();
295
296    let hierarchy = hierarchy
297        .all_edges()
298        .map(|(from, to)| Edge {
299            from: ReflectNodeId(from),
300            to: ReflectNodeId(to),
301        })
302        .collect();
303
304    ReflectSystemGraph {
305        schedule: ReflectSchedule::from_label(schedule.label()),
306        nodes,
307        dependencies,
308        hierarchy,
309    }
310}
311
312/// A graph of systems and system sets for a single schedule
313#[derive(Reflect)]
314pub struct ReflectSystemGraph {
315    /// The schedule that this graph represents
316    schedule: ReflectSchedule,
317    /// All of the included nodes
318    nodes: Vec<ReflectSystemGraphNode>,
319
320    /// The edges signifying the dependency relationship between each node.
321    ///
322    /// I.e. if there is an edge from A -> B, then A depends on B
323    dependencies: Vec<Edge>,
324
325    /// The edges signifying the hierarchy relationship between each node.
326    /// I.e. if there is an edge from A -> B, then A is a child of B
327    hierarchy: Vec<Edge>,
328}
329
330impl ReflectSystemGraph {
331    /// Sorts the graph nodes and edges.
332    ///
333    /// Useful if you want a stable order and deterministic graphs.
334    pub fn sort(&mut self) {
335        // sort everything in this graph
336        self.nodes.sort_by_key(|node| match node {
337            ReflectSystemGraphNode::System(system) => system.node_id.0,
338            ReflectSystemGraphNode::SystemSet(system_set) => system_set.node_id.0,
339        });
340
341        self.dependencies.sort();
342
343        self.hierarchy.sort();
344    }
345
346    /// removes the set and bridges the edges connecting to it
347    fn absorb_set(&mut self, node_id: NodeId) {
348        // collect hierarchy parents and children in one pass
349        let mut hierarchy_parents = Vec::new();
350        let mut hierarchy_children = Vec::new();
351        // the relation ship expressed by edges is "is child of"
352        for edge in &self.hierarchy {
353            // these are children
354            if edge.to.0 == node_id {
355                hierarchy_children.push(edge.from.clone());
356            }
357            // these are parents
358            if edge.from.0 == node_id {
359                hierarchy_parents.push(edge.to.clone());
360            }
361        }
362
363        //
364        let mut dependencies = Vec::new();
365        let mut dependents = Vec::new();
366        // the relationship expressed is "runs before" i.e. "is depended on by"
367        for edge in &self.dependencies {
368            if edge.to.0 == node_id {
369                dependencies.push(edge.from.clone());
370            }
371            if edge.from.0 == node_id {
372                dependents.push(edge.to.clone());
373            }
374        }
375
376        let mut new_hierarchy_edges =
377            HashSet::with_capacity(hierarchy_parents.len() * hierarchy_children.len());
378        let mut new_dependency_edges =
379            HashSet::with_capacity(dependencies.len() * dependents.len());
380
381        // each parent, becomes a parent to the sets children
382        for parent in hierarchy_parents.iter() {
383            for child in hierarchy_children.iter() {
384                new_hierarchy_edges.insert(Edge {
385                    from: child.clone(),
386                    to: parent.clone(),
387                });
388            }
389        }
390
391        for child in hierarchy_parents.iter() {
392            // bridge dependencies
393            for dependency in dependencies.iter() {
394                new_dependency_edges.insert(Edge {
395                    from: dependency.clone(),
396                    to: child.clone(),
397                });
398            }
399
400            for dependent in dependents.iter() {
401                new_dependency_edges.insert(Edge {
402                    from: child.clone(),
403                    to: dependent.clone(),
404                });
405            }
406        }
407
408        // remove any edges involving the set to absorb
409        self.hierarchy
410            .retain(|edge| edge.from.0 != node_id && edge.to.0 != node_id);
411        self.dependencies
412            .retain(|edge| edge.from.0 != node_id && edge.to.0 != node_id);
413
414        // remove the set from nodes
415        self.nodes.retain(|node| match node {
416            ReflectSystemGraphNode::SystemSet(system_set) => system_set.node_id.0 != node_id,
417            _ => true,
418        });
419
420        // add new bridging edges
421        self.hierarchy.extend(new_hierarchy_edges);
422        self.dependencies.extend(new_dependency_edges);
423    }
424
425    /// type system sets, are not really important to us, for all intents and purposes
426    /// they are one and the same as the underlying systems
427    /// Adapter and pipe systems might have multiple default system sets attached, but we want all them gone from the graph.
428    ///
429    /// Inlines every type system set into its children, replacing anything pointing to those sets by edges to every system contained in the set
430    pub fn absorb_type_system_sets(&mut self) {
431        let type_sets = self
432            .nodes
433            .iter()
434            .filter_map(|node| match node {
435                ReflectSystemGraphNode::SystemSet(system_set) => {
436                    if system_set.type_id.is_some() {
437                        Some(system_set.node_id.0)
438                    } else {
439                        None
440                    }
441                }
442                _ => None,
443            })
444            .collect::<Vec<_>>();
445
446        // yes this is very inefficient, no this isn't a big hot path, these graphs mostly serve a debugging role.
447        for node_id in type_sets {
448            self.absorb_set(node_id);
449        }
450    }
451}
452
453/// A node in the reflectable system graph
454#[derive(Reflect)]
455pub enum ReflectSystemGraphNode {
456    /// A system node
457    System(ReflectSystem),
458    /// A system set node
459    SystemSet(ReflectSystemSet),
460}
461
462/// An edge in the graph
463#[derive(Reflect, PartialEq, Eq, PartialOrd, Ord, Hash)]
464pub struct Edge {
465    /// The id of the node this edge is coming from
466    from: ReflectNodeId,
467    /// The id of the node this edge is going to
468    to: ReflectNodeId,
469}
470
471#[cfg(test)]
472mod test {
473    use ::{
474        bevy_app::Update,
475        bevy_ecs::{schedule::IntoScheduleConfigs, world::World},
476    };
477
478    use super::*;
479
480    fn system_a() {}
481
482    fn system_b() {}
483
484    fn system_c() {}
485
486    fn system_d() {}
487
488    fn system_e() {}
489
490    const BLESS_MODE: bool = false;
491
492    #[test]
493    fn test_graph_is_as_expected() {
494        // create empty schedule and graph it
495
496        let mut schedule = Schedule::new(Update);
497
498        #[derive(SystemSet, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
499        enum SystemSet {
500            SystemSetG,
501            SystemSetH,
502        }
503
504        // add a few systems that depend on each other, some via system sets
505
506        schedule
507            .add_systems(system_a)
508            .add_systems(system_b.before(system_a))
509            .add_systems(system_c.after(system_b).before(SystemSet::SystemSetH))
510            .add_systems(system_d.in_set(SystemSet::SystemSetG))
511            .add_systems(system_e.in_set(SystemSet::SystemSetH))
512            .configure_sets(SystemSet::SystemSetG.after(SystemSet::SystemSetH));
513        let mut world = World::new();
514        schedule.initialize(&mut world).unwrap();
515
516        let mut graph = schedule_to_reflect_graph(&schedule);
517        graph.absorb_type_system_sets();
518        graph.sort();
519        let dot = reflect_graph_to_dot(graph);
520
521        let normalize = |s: &str| {
522            // trim each line individually from the start, and replace " = " with "=" to deal with formatters
523            let lines: Vec<&str> = s.lines().map(|line| line.trim_start()).collect();
524            lines
525                .join("\n")
526                .replace(" = ", "")
527                .replace(";", "")
528                .replace(",", "")
529                .trim()
530                .to_string()
531        };
532
533        // check that the dot graph is as expected
534        // the expected file is found next to the src/lib.rs folder
535        let expected = include_str!("../test_graph.dot");
536        let expected_path = manifest_dir_macros::file_path!("test_graph.dot");
537
538        if BLESS_MODE {
539            std::fs::write(expected_path, normalize(&dot)).unwrap();
540            panic!("Bless mode is active");
541        } else {
542            pretty_assertions::assert_eq!(normalize(&dot), normalize(expected));
543        }
544    }
545}