Skip to content
164 changes: 162 additions & 2 deletions crates/bevy_remote/src/builtin_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ pub const BRP_REGISTRY_SCHEMA_METHOD: &str = "registry.schema";
/// The method path for a `schedule.list` request.
pub const BRP_SCHEDULE_LIST: &str = "schedule.list";

/// The method path for a `schedule.graph` request.
pub const BRP_SCHEDULE_GRAPH: &str = "schedule.graph";

/// The method path for a `rpc.discover` request.
pub const RPC_DISCOVER_METHOD: &str = "rpc.discover";

Expand Down Expand Up @@ -336,6 +339,18 @@ pub struct BrpWriteMessageParams {
pub value: Option<Value>,
}

/// `schedule.graph`:
Comment thread
Zeophlite marked this conversation as resolved.
///
/// The server responds with [`BrpScheduleGraphResponse`] if the schedule is found,
/// or a `resource_error` if not found.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
struct BrpScheduleGraphParams {
/// The schedule to describe.
Comment thread
Zeophlite marked this conversation as resolved.
///
/// A list of describable schedules can be fetched from the `schedule.list` endpoint.
pub schedule_label: String,
}

/// Describes the data that is to be fetched in a query.
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
pub struct BrpQuery {
Expand Down Expand Up @@ -498,6 +513,40 @@ pub struct BrpScheduleListResponse {
empty_schedule_labels: Vec<String>,
}

/// The response to a `schedule.graph` request.
///
/// In Bevy, systems are ordered in a graph structure, [`ScheduleGraph`](`bevy_ecs::schedule::ScheduleGraph`).
/// A system can be placed inside a systemset in order to organize them.
/// Relative ordering can be defined between systems and/or systemsets.
/// The graph can be thought of as two Directed Acylic Graph's (DAG's) overlaid.
///
/// Each system (f1) creates a corresponding system set (F1).
/// Both these "system derived" system sets and developer-created systemsets are in `systemsets`.
/// There is a hierarchy edge between the system set and the system (F1 -> f1).
/// If a system (f2) is placed in a developer-created set (S1), then there is a hierarchy edge (S1 -> f2).
///
/// If a schedule adds a condition f1.after(S1) , then a dependency edge is added (S1 -> f1)
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct BrpScheduleGraphResponse {
systemsets: Vec<BrpSystemSet>,

hierarchy_nodes: Vec<String>,
hierarchy_edges: Vec<(String, String)>,

dependency_nodes: Vec<String>,
dependency_edges: Vec<(String, String)>,
}

/// Details on a system set
///
/// The `key` is used in the nodes and edges in [`BrpScheduleGraphResponse`]
/// The `method` is either the fully qualified system name, or the system set name
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct BrpSystemSet {
key: String,
method: String,
}

/// One query match result: a single entity paired with the requested components.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct BrpQueryRow {
Expand Down Expand Up @@ -1570,6 +1619,60 @@ pub fn schedule_list(In(_params): In<Option<Value>>, world: &World) -> BrpResult
serde_json::to_value(response).map_err(BrpError::internal)
}

/// Handles a `schedule.graph` request coming from a client.
///
/// Bevy removes a schedule from the world before running it, meaning that not all Schedules are available.
Comment thread
Zeophlite marked this conversation as resolved.
pub fn schedule_graph(In(params): In<Option<Value>>, world: &mut World) -> BrpResult {
let BrpScheduleGraphParams { schedule_label } = parse_some(params)?;

let schedules = world.resource::<Schedules>();

let matching_schedule = schedules
.iter()
.find(|(label, _schedule)| format!("{:?}", label) == schedule_label);

if matching_schedule.is_none() {
return Err(BrpError::resource_error(format!(
"Schedule with label={:} not found",
schedule_label
)));
}

let mut response: BrpScheduleGraphResponse = BrpScheduleGraphResponse::default();

let (_label, schedule) = matching_schedule.unwrap();

let g = schedule.graph();
for (systemsetkey, method, _b) in g.system_sets.iter() {
response.systemsets.push(BrpSystemSet {
key: format!("{:?}", systemsetkey),
method: format!("{:?}", method),
});
}

let hie = g.hierarchy();
for node in hie.nodes() {
response.hierarchy_nodes.push(format!("{:?}", node));
}
for (n1, n2) in hie.all_edges() {
response
.hierarchy_edges
.push((format!("{:?}", n1), format!("{:?}", n2)));
}

let dep = g.dependency();
for node in dep.nodes() {
response.dependency_nodes.push(format!("{:?}", node));
}
for (n1, n2) in dep.all_edges() {
response
.dependency_edges
.push((format!("{:?}", n1), format!("{:?}", n2)));
}

serde_json::to_value(response).map_err(BrpError::internal)
}

/// Immutably retrieves an entity from the [`World`], returning an error if the
/// entity isn't present.
fn get_entity(world: &World, entity: Entity) -> Result<EntityRef<'_>, BrpError> {
Expand Down Expand Up @@ -1822,8 +1925,8 @@ mod tests {
message::{Message, Messages},
observer::On,
resource::Resource,
schedule::{Schedule, ScheduleLabel},
system::{Commands, ResMut},
schedule::{IntoScheduleConfigs as _, Schedule, ScheduleLabel, SystemSet},
system::{Commands, Res, ResMut},
};
use bevy_reflect::Reflect;
use serde_json::Value::Null;
Expand Down Expand Up @@ -2071,4 +2174,61 @@ mod tests {

world.run_schedule(ScheduleOuter);
}

#[test]
fn test_schedule_graph() {
#[derive(Resource)]
struct Resource1;

fn f1(mut commands: Commands) {
commands.insert_resource(Resource1);
}
fn f2(_r: Res<Resource1>) {}
fn f3(_s: Res<Resource1>) {}
fn f4(_t: Res<Resource1>) {}

#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
struct S1;

#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
struct S2;

#[derive(ScheduleLabel, Hash, Clone, PartialEq, Eq, Debug)]
struct MySchedule;

let mut schedule = Schedule::new(MySchedule);

schedule.add_systems((f1, f2).chain());
schedule.add_systems(f3.in_set(S1));
schedule.add_systems(f4.in_set(S2).after(S1));

let mut world = World::default();

let _ = schedule.initialize(&mut world);
world.add_schedule(schedule);

let params = serde_json::to_value(&BrpScheduleGraphParams {
schedule_label: "MySchedule".to_string(),
})
.expect("FAIL");

// Each system creates a corresponding system set.
// In the below notation we use f1 for the system, and F1 for the corresponding system set.
// Above schedule should have the following layout:
// - 4 systems (f1, f2, f3, f4)
// - 6 system sets (F1, F2, F3, F4, S1, S2)
// - 10 hierarchy nodes and 10 dependency nodes (4 + 6)
// - 6 hierarchy edges: F1 -> f1, F2 -> f2, F3 -> f3, F4 -> f4, S1 -> f3, S2 -> f4
// - 2 dependency edges: f1 -> f2, S1 -> f4

let res = schedule_graph(In(Some(params)), &mut world);
let res2 = res.expect("expect to work");
let res3 = serde_json::from_value::<BrpScheduleGraphResponse>(res2).unwrap();

assert_eq!(res3.systemsets.len(), 6);
assert_eq!(res3.hierarchy_nodes.len(), 10);
assert_eq!(res3.dependency_nodes.len(), 10);
assert_eq!(res3.hierarchy_edges.len(), 6);
assert_eq!(res3.dependency_edges.len(), 2);
}
}
4 changes: 4 additions & 0 deletions crates/bevy_remote/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,10 @@ impl Default for RemotePlugin {
builtin_methods::BRP_SCHEDULE_LIST,
builtin_methods::schedule_list,
)
.with_method(
builtin_methods::BRP_SCHEDULE_GRAPH,
builtin_methods::schedule_graph,
)
}
}

Expand Down