Skip to content

Commit 002825c

Browse files
committed
improve dependency graph dot rendering and add more tests
1 parent c3c925b commit 002825c

File tree

8 files changed

+306
-8
lines changed

8 files changed

+306
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
function on_test()
3+
local post_update_schedule = world.get_schedule_by_name("PostUpdate")
4+
5+
local test_system = post_update_schedule:get_system_by_name("on_test_post_update")
6+
7+
local system_a = world.add_system(
8+
post_update_schedule,
9+
system_builder("custom_system_a", script_id)
10+
:after(test_system)
11+
)
12+
13+
local system_b = world.add_system(
14+
post_update_schedule,
15+
system_builder("custom_system_b", script_id)
16+
:after(test_system)
17+
)
18+
19+
-- generate a schedule graph and verify it's what we expect
20+
local dot_graph = post_update_schedule:render_dot()
21+
22+
local expected_dot_graph = [[
23+
digraph {
24+
node_0 [label="bevy_mod_scripting_core::bindings::allocator::garbage_collector"];
25+
node_1 [label="on_test_post_update"];
26+
node_2 [label="script_integration_test_harness::dummy_before_post_update_system"];
27+
node_3 [label="script_integration_test_harness::dummy_post_update_system"];
28+
node_4 [label="custom_system_a"];
29+
node_5 [label="custom_system_b"];
30+
node_6 [label="SystemSet GarbageCollection"];
31+
node_7 [label="SystemSet ScriptSystem(custom_system_a)"];
32+
node_8 [label="SystemSet ScriptSystem(custom_system_b)"];
33+
node_0 -> node_6 [color=red, label="child of", arrowhead=diamond];
34+
node_4 -> node_7 [color=red, label="child of", arrowhead=diamond];
35+
node_5 -> node_8 [color=red, label="child of", arrowhead=diamond];
36+
node_1 -> node_4 [color=blue, label="runs before", arrowhead=normal];
37+
node_1 -> node_5 [color=blue, label="runs before", arrowhead=normal];
38+
node_2 -> node_3 [color=blue, label="runs before", arrowhead=normal];
39+
}
40+
]]
41+
42+
assert_str_eq(dot_graph, expected_dot_graph, "Expected the schedule graph to match the expected graph")
43+
end

crates/bevy_mod_scripting_core/src/bindings/script_system.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use bevy::{
2626
entity::Entity,
2727
query::{Access, FilteredAccess, FilteredAccessSet, QueryState},
2828
reflect::AppTypeRegistry,
29-
schedule::{IntoSystemConfigs, IntoSystemSetConfigs, SystemSet},
29+
schedule::{IntoSystemConfigs, SystemSet},
3030
system::{IntoSystem, System},
3131
world::{unsafe_world_cell::UnsafeWorldCell, World},
3232
},
@@ -650,7 +650,7 @@ mod test {
650650

651651
make_test_plugin!(crate);
652652

653-
fn test_system_rust(world: &mut World) {}
653+
fn test_system_rust(_world: &mut World) {}
654654

655655
#[test]
656656
fn test_script_system_with_existing_system_dependency_can_execute() {

crates/bevy_mod_scripting_functions/src/core.rs

+27
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,33 @@ impl ReflectSchedule {
762762
.into_iter()
763763
.find_map(|s| (s.identifier() == name || s.path() == name).then_some(s.into())))
764764
}
765+
766+
/// Renders the schedule as a dot graph string.
767+
///
768+
/// Useful for debugging scheduling.
769+
///
770+
/// Arguments:
771+
/// * `ctxt`: The function call context
772+
/// * `self_`: The schedule to render.
773+
/// Returns:
774+
/// * `dot`: The dot graph string.
775+
fn render_dot(
776+
ctxt: FunctionCallContext,
777+
self_: Ref<ReflectSchedule>,
778+
) -> Result<String, InteropError> {
779+
profiling::function_scope!("render_dot");
780+
let world = ctxt.world()?;
781+
world.with_resource(|schedules: &Schedules| {
782+
let schedule = schedules
783+
.get(*self_.label())
784+
.ok_or_else(|| InteropError::missing_schedule(self_.identifier()))?;
785+
let mut graph = bevy_system_reflection::schedule_to_reflect_graph(schedule);
786+
graph.absorb_type_system_sets();
787+
graph.sort();
788+
let graph = bevy_system_reflection::reflect_graph_to_dot(graph);
789+
Ok(graph)
790+
})?
791+
}
765792
}
766793

767794
#[script_bindings(

crates/bevy_system_reflection/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,9 @@ bevy = { workspace = true, default-features = false }
88
dot-writer = "0.1.4"
99
petgraph = "*"
1010

11+
12+
[dev-dependencies]
13+
pretty_assertions = "1.4"
14+
manifest-dir-macros = "0.1.18"
1115
[lints]
1216
workspace = true

crates/bevy_system_reflection/src/lib.rs

+206-5
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ use bevy::ecs::schedule::{
77
};
88
use bevy::ecs::system::{System, SystemInput};
99
use bevy::reflect::Reflect;
10-
use bevy::utils::HashSet;
11-
use bevy::utils::hashbrown::HashMap;
10+
use bevy::utils::hashbrown::{HashMap, HashSet};
1211
use dot_writer::{Attributes, DotWriter};
1312

1413
#[derive(Reflect, Debug, Clone)]
@@ -76,7 +75,7 @@ impl ReflectSystem {
7675
}
7776
}
7877

79-
#[derive(Reflect, Clone, Debug, PartialEq, Eq, Hash)]
78+
#[derive(Reflect, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
8079
#[reflect(opaque)]
8180
pub(crate) struct ReflectNodeId(pub NodeId);
8281

@@ -158,7 +157,11 @@ impl ReflectSystemSet {
158157
/// Renders a schedule to a dot graph using the optimized schedule.
159158
pub fn schedule_to_dot_graph(schedule: &Schedule) -> String {
160159
let graph = schedule_to_reflect_graph(schedule);
160+
reflect_graph_to_dot(graph)
161+
}
161162

163+
/// Renders a reflectable system graph to a dot graph
164+
pub fn reflect_graph_to_dot(graph: ReflectSystemGraph) -> String {
162165
// create a dot graph with:
163166
// - hierarchy represented by red composition arrows
164167
// - dependencies represented by blue arrows
@@ -191,7 +194,6 @@ pub fn schedule_to_dot_graph(schedule: &Schedule) -> String {
191194
}
192195
}
193196

194-
// go through hierarchy edges
195197
// go through hierarchy edges
196198
for edge in graph.hierarchy {
197199
let from = node_id_map.get(&edge.from).cloned().unwrap_or_else(|| {
@@ -321,6 +323,132 @@ pub struct ReflectSystemGraph {
321323
hierarchy: Vec<Edge>,
322324
}
323325

326+
impl ReflectSystemGraph {
327+
/// Sorts the graph nodes and edges.
328+
///
329+
/// Useful if you want a stable order and deterministic graphs.
330+
pub fn sort(&mut self) {
331+
// sort everything in this graph
332+
self.nodes.sort_by_key(|node| match node {
333+
ReflectSystemGraphNode::System(system) => system.node_id.0,
334+
ReflectSystemGraphNode::SystemSet(system_set) => system_set.node_id.0,
335+
});
336+
337+
self.dependencies.sort();
338+
339+
self.hierarchy.sort();
340+
}
341+
342+
/// removes the set and bridges the edges connecting to it
343+
fn absorb_set(&mut self, node_id: NodeId) {
344+
// collect hierarchy parents and children in one pass
345+
let mut hierarchy_parents = Vec::new();
346+
let mut hierarchy_children = Vec::new();
347+
// the relation ship expressed by edges is "is child of"
348+
for edge in &self.hierarchy {
349+
// these are children
350+
if edge.to.0 == node_id {
351+
hierarchy_children.push(edge.from.clone());
352+
}
353+
// these are parents
354+
if edge.from.0 == node_id {
355+
hierarchy_parents.push(edge.to.clone());
356+
}
357+
}
358+
359+
//
360+
let mut dependencies = Vec::new();
361+
let mut dependents = Vec::new();
362+
// the relationship expressed is "runs before" i.e. "is depended on by"
363+
for edge in &self.dependencies {
364+
if edge.to.0 == node_id {
365+
dependencies.push(edge.from.clone());
366+
}
367+
if edge.from.0 == node_id {
368+
dependents.push(edge.to.clone());
369+
}
370+
}
371+
372+
let mut new_hierarchy_edges =
373+
HashSet::with_capacity(hierarchy_parents.len() * hierarchy_children.len());
374+
let mut new_dependency_edges =
375+
HashSet::with_capacity(dependencies.len() * dependents.len());
376+
377+
// each parent, becomes a parent to the sets children
378+
for parent in hierarchy_parents.iter() {
379+
for child in hierarchy_children.iter() {
380+
new_hierarchy_edges.insert(Edge {
381+
from: child.clone(),
382+
to: parent.clone(),
383+
});
384+
}
385+
}
386+
387+
for child in hierarchy_parents.iter() {
388+
// bridge dependencies
389+
for dependency in dependencies.iter() {
390+
new_dependency_edges.insert(Edge {
391+
from: dependency.clone(),
392+
to: child.clone(),
393+
});
394+
}
395+
396+
for dependent in dependents.iter() {
397+
new_dependency_edges.insert(Edge {
398+
from: child.clone(),
399+
to: dependent.clone(),
400+
});
401+
}
402+
}
403+
404+
405+
// remove any edges involving the set to absorb
406+
self.hierarchy
407+
.retain(|edge| edge.from.0 != node_id && edge.to.0 != node_id);
408+
self.dependencies
409+
.retain(|edge| edge.from.0 != node_id && edge.to.0 != node_id);
410+
411+
// remove the set from nodes
412+
self.nodes.retain(|node| match node {
413+
ReflectSystemGraphNode::SystemSet(system_set) => system_set.node_id.0 != node_id,
414+
_ => true,
415+
});
416+
417+
// add new bridging edges
418+
self.hierarchy.extend(new_hierarchy_edges);
419+
self.dependencies.extend(new_dependency_edges);
420+
}
421+
422+
/// type system sets, are not really important to us, for all intents and purposes
423+
/// they are one and the same as the underlying systems
424+
/// Adapter and pipe systems might have multiple default system sets attached, but we want all them gone from the graph.
425+
///
426+
/// Inlines every type system set into its children, replacing anything pointing to those sets by edges to every system contained in the set
427+
pub fn absorb_type_system_sets(&mut self) {
428+
429+
let type_sets = self
430+
.nodes
431+
.iter()
432+
.filter_map(|node| match node {
433+
ReflectSystemGraphNode::SystemSet(system_set) => {
434+
if system_set.type_id.is_some() {
435+
Some(system_set.node_id.0)
436+
} else {
437+
None
438+
}
439+
}
440+
_ => None,
441+
})
442+
.collect::<Vec<_>>();
443+
444+
// yes this is very inefficient, no this isn't a big hot path, these graphs mostly serve a debugging role.
445+
for node_id in type_sets {
446+
self.absorb_set(node_id);
447+
}
448+
449+
}
450+
}
451+
324452
/// A node in the reflectable system graph
325453
#[derive(Reflect)]
326454
pub enum ReflectSystemGraphNode {
@@ -331,10 +459,83 @@ pub enum ReflectSystemGraphNode {
331459
}
332460

333461
/// An edge in the graph
334-
#[derive(Reflect)]
462+
#[derive(Reflect, PartialEq, Eq, PartialOrd, Ord, Hash)]
335463
pub struct Edge {
336464
/// The id of the node this edge is coming from
337465
from: ReflectNodeId,
338466
/// The id of the node this edge is going to
339467
to: ReflectNodeId,
340468
}
469+
470+
#[cfg(test)]
471+
mod test {
472+
use bevy::{
473+
app::Update,
474+
ecs::{
475+
schedule::{IntoSystemConfigs, IntoSystemSetConfigs},
476+
world::World,
477+
},
478+
};
479+
480+
use super::*;
481+
482+
fn system_a() {}
483+
484+
fn system_b() {}
485+
486+
fn system_c() {}
487+
488+
fn system_d() {}
489+
490+
fn system_e() {}
491+
492+
const BLESS_MODE: bool = false;
493+
494+
#[test]
495+
fn test_graph_is_as_expected() {
496+
// create empty schedule and graph it
497+
498+
let mut schedule = Schedule::new(Update);
499+
500+
#[derive(SystemSet, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
501+
enum SystemSet {
502+
SystemSetG,
503+
SystemSetH,
504+
}
505+
506+
// add a few systems that depend on each other, some via system sets
507+
508+
schedule
509+
.add_systems(system_a)
510+
.add_systems(system_b.before(system_a))
511+
.add_systems(system_c.after(system_b).before(SystemSet::SystemSetH))
512+
.add_systems(system_d.in_set(SystemSet::SystemSetG))
513+
.add_systems(system_e.in_set(SystemSet::SystemSetH))
514+
.configure_sets(SystemSet::SystemSetG.after(SystemSet::SystemSetH));
515+
let mut world = World::new();
516+
schedule.initialize(&mut world).unwrap();
517+
518+
let mut graph = schedule_to_reflect_graph(&schedule);
519+
graph.absorb_type_system_sets();
520+
graph.sort();
521+
let dot = reflect_graph_to_dot(graph);
522+
523+
let normalize = |s: &str| {
524+
// trim each line individually from the start, and replace " = " with "=" to deal with formatters
525+
let lines: Vec<&str> = s.lines().map(|line| line.trim_start()).collect();
526+
lines.join("\n").replace(" = ", "").replace(";", "").replace(",", "").trim().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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
digraph {
2+
node_0 [label="bevy_system_reflection::test::system_a"]
3+
node_1 [label="bevy_system_reflection::test::system_b"]
4+
node_2 [label="bevy_system_reflection::test::system_c"]
5+
node_3 [label="bevy_system_reflection::test::system_d"]
6+
node_4 [label="bevy_system_reflection::test::system_e"]
7+
node_5 [label="SystemSet SystemSetH"]
8+
node_6 [label="SystemSet SystemSetG"]
9+
node_4 -> node_5 [color=red label="child of" arrowhead=diamond]
10+
node_3 -> node_6 [color=red label="child of" arrowhead=diamond]
11+
node_1 -> node_0 [color=blue label="runs before" arrowhead=normal]
12+
node_1 -> node_2 [color=blue label="runs before" arrowhead=normal]
13+
node_2 -> node_5 [color=blue label="runs before" arrowhead=normal]
14+
node_5 -> node_6 [color=blue label="runs before" arrowhead=normal]
15+
}

crates/testing_crates/script_integration_test_harness/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ bevy_mod_scripting_functions = { workspace = true, features = [
1515
"lua_bindings",
1616
] }
1717
regex = { version = "1.11" }
18+
pretty_assertions = "1.*"

0 commit comments

Comments
 (0)