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 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)]
18/// A reflectable system.
19pub 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    /// Retrieves the name of the system.
28    pub fn name(&self) -> &str {
29        self.name.as_ref()
30    }
31
32    /// Retrieves the type id of the system.
33    pub fn type_id(&self) -> TypeId {
34        self.type_id
35    }
36
37    /// Retrieves the node id of the system.
38    pub fn node_id(&self) -> NodeId {
39        self.node_id.0
40    }
41
42    /// Retrieves the default system sets of the system.
43    pub fn default_system_sets(&self) -> &[InternedSystemSet] {
44        &self.default_system_sets
45    }
46    /// Creates a reflect system from a system specification
47    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    /// gets the short identifier of the system, i.e. just the function name
60    pub fn identifier(&self) -> &str {
61        // if it contains generics it might contain more than
62        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    /// gets the path of the system, i.e. the fully qualified function name
76    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/// A reflectable schedule.
86#[derive(Reflect, Clone, Debug)]
87pub struct ReflectSchedule {
88    /// The name of the schedule.
89    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    /// Retrieves the name of the schedule.
113    pub fn type_path(&self) -> &'static str {
114        self.type_path
115    }
116
117    /// Retrieves the short identifier of the schedule
118    pub fn identifier(&self) -> &'static str {
119        self.type_path.split("::").last().unwrap_or_default()
120    }
121
122    /// Retrieves the label of the schedule
123    pub fn label(&self) -> &InternedScheduleLabel {
124        &self.label
125    }
126
127    /// Creates a new reflect schedule from a schedule label
128    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)]
137/// A reflectable system set.
138pub struct ReflectSystemSet {
139    /// The node id of the system set.
140    node_id: ReflectNodeId,
141
142    /// The debug print of the system set
143    debug: String,
144
145    /// If this is a typed system set, the type id
146    type_id: Option<TypeId>,
147}
148
149impl ReflectSystemSet {
150    /// Creates a reflect system set from a system set
151    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
160/// Renders a schedule to a dot graph using the optimized schedule.
161pub fn schedule_to_dot_graph(schedule: &Schedule) -> String {
162    let graph = schedule_to_reflect_graph(schedule);
163    reflect_graph_to_dot(graph)
164}
165
166/// Renders a reflectable system graph to a dot graph
167pub fn reflect_graph_to_dot(graph: ReflectSystemGraph) -> String {
168    // create a dot graph with:
169    // - hierarchy represented by red composition arrows
170    // - dependencies represented by blue arrows
171    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                        // special sets, that pollute the graph, summize them as "system type set", each system gets one
188                        "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        // go through hierarchy edges
201        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        // go through dependency edges
224        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
251/// Converts a schedule to a reflectable system graph
252pub 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    // for some reason the graph doesn't contain these
267    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    // try find all uncovered nodes, and warn about them, or do something about them
278    // for now we just warn
279    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/// A graph of systems and system sets for a single schedule
312#[derive(Reflect)]
313pub struct ReflectSystemGraph {
314    /// The schedule that this graph represents
315    schedule: ReflectSchedule,
316    /// All of the included nodes
317    nodes: Vec<ReflectSystemGraphNode>,
318
319    /// The edges signifying the dependency relationship between each node.
320    ///
321    /// I.e. if there is an edge from A -> B, then A depends on B
322    dependencies: Vec<Edge>,
323
324    /// The edges signifying the hierarchy relationship between each node.
325    /// I.e. if there is an edge from A -> B, then A is a child of B
326    hierarchy: Vec<Edge>,
327}
328
329impl ReflectSystemGraph {
330    /// Sorts the graph nodes and edges.
331    ///
332    /// Useful if you want a stable order and deterministic graphs.
333    pub fn sort(&mut self) {
334        // sort everything in this graph
335        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    /// removes the set and bridges the edges connecting to it
346    fn absorb_set(&mut self, node_id: NodeId) {
347        // collect hierarchy parents and children in one pass
348        let mut hierarchy_parents = Vec::new();
349        let mut hierarchy_children = Vec::new();
350        // the relation ship expressed by edges is "is child of"
351        for edge in &self.hierarchy {
352            // these are children
353            if edge.to.0 == node_id {
354                hierarchy_children.push(edge.from.clone());
355            }
356            // these are parents
357            if edge.from.0 == node_id {
358                hierarchy_parents.push(edge.to.clone());
359            }
360        }
361
362        //
363        let mut dependencies = Vec::new();
364        let mut dependents = Vec::new();
365        // the relationship expressed is "runs before" i.e. "is depended on by"
366        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        // each parent, becomes a parent to the sets children
381        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            // bridge dependencies
392            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        // remove any edges involving the set to absorb
408        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        // remove the set from nodes
414        self.nodes.retain(|node| match node {
415            ReflectSystemGraphNode::SystemSet(system_set) => system_set.node_id.0 != node_id,
416            _ => true,
417        });
418
419        // add new bridging edges
420        self.hierarchy.extend(new_hierarchy_edges);
421        self.dependencies.extend(new_dependency_edges);
422    }
423
424    /// type system sets, are not really important to us, for all intents and purposes
425    /// they are one and the same as the underlying systems
426    /// Adapter and pipe systems might have multiple default system sets attached, but we want all them gone from the graph.
427    ///
428    /// Inlines every type system set into its children, replacing anything pointing to those sets by edges to every system contained in the set
429    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        // yes this is very inefficient, no this isn't a big hot path, these graphs mostly serve a debugging role.
446        for node_id in type_sets {
447            self.absorb_set(node_id);
448        }
449    }
450}
451
452/// A node in the reflectable system graph
453#[derive(Reflect)]
454pub enum ReflectSystemGraphNode {
455    /// A system node
456    System(ReflectSystem),
457    /// A system set node
458    SystemSet(ReflectSystemSet),
459}
460
461/// An edge in the graph
462#[derive(Reflect, PartialEq, Eq, PartialOrd, Ord, Hash)]
463pub struct Edge {
464    /// The id of the node this edge is coming from
465    from: ReflectNodeId,
466    /// The id of the node this edge is going to
467    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        // create empty schedule and graph it
491
492        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        // add a few systems that depend on each other, some via system sets
501
502        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            // trim each line individually from the start, and replace " = " with "=" to deal with formatters
519            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        // check that the dot graph is as expected
530        // the expected file is found next to the src/lib.rs folder
531        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}