Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
eca5de6
Add len, iter on Bundles, add an example to show how to inspect a world
shuoli84 Feb 20, 2023
cb8f377
Add schedule diagnostic example
shuoli84 Feb 21, 2023
47f0860
fix clippy, remove unused code
shuoli84 Feb 21, 2023
f951e57
add a note explain why need a special label
shuoli84 Feb 21, 2023
4505730
clippy
shuoli84 Feb 21, 2023
a16f720
fix index mismatch between graph and executor. return a string repr f…
shuoli84 Feb 21, 2023
81802d7
fix clippy. add more details in diagnostic
shuoli84 Feb 21, 2023
81d22ed
fix clippy
shuoli84 Feb 21, 2023
bddb539
Merge branch 'main' into ecs-inspectability-adopted-conflict
AlephCubed Jan 7, 2025
54b046c
Fixed CI not passing.
AlephCubed Jan 9, 2025
14578c1
Updated schedule label code in example.
AlephCubed Jan 9, 2025
dc34cd4
Merge branch 'bevyengine:main' into ecs-inspectability-adopted-conflict
AlephCubed Jan 12, 2025
8619f8b
Fix spelling mistakes + format.
AlephCubed Jan 12, 2025
d14d70a
Replaced `component_display` with `ComponentId::diagnose`.
AlephCubed Jan 12, 2025
5a9b6a7
Replaced `diagnose_dag` with `Dag::diagnose`.
AlephCubed Jan 12, 2025
9d5bb88
Replaced `diagnostic_schedules` with `Schedules::diagnose`.
AlephCubed Jan 13, 2025
51b3098
Replaced `diagnostic_world` with `World::diagnose`.
AlephCubed Jan 13, 2025
159e607
Added `Systems::diagnose_flattened`.
AlephCubed Jan 13, 2025
a852d28
Added `World::diagnose_with_flattened`.
AlephCubed Jan 13, 2025
d891ef5
Replaced `"".to_string()` with `String::new()`.
AlephCubed Jan 15, 2025
84e29f1
Merge remote-tracking branch 'upstream/main' into ecs-inspectability-…
AlephCubed Jan 15, 2025
80e5e11
Merge branch 'ecs-inspectability-adopted-conflict' into diagnose-flat…
AlephCubed Jan 15, 2025
4f70216
Fix CI not passing due to new requirements.
AlephCubed Jan 15, 2025
3ae2ae7
Merge branch 'ecs-inspectability-adopted-conflict' into diagnose-flat…
AlephCubed Jan 15, 2025
9177b0f
Final Cleanup.
AlephCubed Jan 15, 2025
fa75f6f
Merge remote-tracking branch 'upstream/main' into ecs-inspectability-…
AlephCubed Jan 18, 2025
cb6611b
Added more documentation and comments to example.
AlephCubed Jan 19, 2025
644ab19
Update doc comment for `Dag::diagnose`.
AlephCubed Jan 19, 2025
03e0276
Made `Schedule::systems_for_each` private.
AlephCubed Jan 19, 2025
cdb53b3
Merge remote-tracking branch 'upstream/main' into ecs-inspectability-…
AlephCubed Jan 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ path = "examples/resources.rs"
name = "change_detection"
path = "examples/change_detection.rs"

[[example]]
name = "world_diagnostic"
path = "examples/world_diagnostic.rs"

[lints]
workspace = true

Expand Down
123 changes: 123 additions & 0 deletions crates/bevy_ecs/examples/world_diagnostic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! In this example, we use a system to print diagnostic information about the world.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be good to elaborate more on this. For example, what diagnostic info do we get and why is this useful to see?

//!
//! This includes information about which components, bundles, and systems are registered,
//! as well as the order that systems will run in.

#![expect(
missing_docs,
reason = "Trivial example types do not require documentation."
)]

use bevy_ecs::prelude::*;
use bevy_ecs_macros::{ScheduleLabel, SystemSet};

fn empty_system() {}

fn first_system() {}

fn second_system() {}

fn increase_game_state_count(mut state: ResMut<GameState>) {
state.counter += 1;
}

fn sync_counter(state: Res<GameState>, mut query: Query<&mut Counter>) {
for mut counter in query.iter_mut() {
counter.0 = state.counter;
}
}

#[derive(Resource, Default)]
struct GameState {
counter: usize,
}

#[derive(SystemSet, Hash, Clone, Copy, PartialEq, Eq, Debug)]
enum MySet {
Set1,
Set2,
}

#[derive(Component)]
struct Counter(usize);

#[derive(Component)]
struct Player;

#[derive(Component)]
#[component(storage = "SparseSet")]
struct HighlightFlag;

#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
pub enum ScheduleLabel {
Foo,
Bar,
}

/// A special label for diagnostic.
/// If a system has a commonly used label, like [`bevy_app::CoreSchedule`] it is not able to get
/// the corresponding [`Schedule`] instance and can't be inspected.
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
pub struct DiagnosticLabel;

/// World diagnostic example.
fn diagnostic_world_system(world: &mut World) {
println!("{}", world.diagnose_with_flattened().unwrap());
}

// If you do not have mutable access, you can also use [`World::diagnose`].
// This version will not include a flattened representation.
//
// fn diagnostic_world_system(world: &World) {
// println!("{}", world.diagnose().unwrap());
// }

// In this example, we add a counter resource and increase its value in one system,
// while a different system prints debug information about the world.
fn main() {
let mut world = World::new();
world.init_resource::<Schedules>();

{
let mut diagnostic_schedule = Schedule::new(DiagnosticLabel);
diagnostic_schedule.add_systems(diagnostic_world_system);
world.add_schedule(diagnostic_schedule);
}

let mut schedule = Schedule::new(ScheduleLabel::Bar);
schedule.configure_sets((MySet::Set1, MySet::Set2));

schedule.add_systems(empty_system.in_set(MySet::Set1));
schedule.add_systems(
increase_game_state_count
.in_set(MySet::Set1)
.before(sync_counter),
);
schedule.add_systems(sync_counter);
schedule.add_systems(first_system.before(second_system).in_set(MySet::Set2));
schedule.add_systems(second_system.in_set(MySet::Set2));
world.add_schedule(schedule);

world.init_resource::<GameState>();

world.run_schedule(ScheduleLabel::Bar);
world.run_schedule(DiagnosticLabel);

let player = world.spawn(Player).id();
// Create an archetype with one table component and one sparse set.
world.spawn((Counter(1), HighlightFlag));
world.run_schedule(ScheduleLabel::Bar);
world.run_schedule(DiagnosticLabel);

world.entity_mut(player).insert(Counter(100));
world.run_schedule(ScheduleLabel::Bar);
world.run_schedule(DiagnosticLabel);

world.entity_mut(player).insert(HighlightFlag);
world.run_schedule(ScheduleLabel::Bar);
world.run_schedule(DiagnosticLabel);

world.entity_mut(player).despawn();
world.run_schedule(ScheduleLabel::Bar);
world.run_schedule(DiagnosticLabel);
}
15 changes: 15 additions & 0 deletions crates/bevy_ecs/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1473,6 +1473,21 @@ pub struct Bundles {
}

impl Bundles {
/// The total number of [`Bundle`] registered in [`Storages`].
pub fn len(&self) -> usize {
self.bundle_infos.len()
}

/// Returns true if no [`Bundle`] registered in [`Storages`].
pub fn is_empty(&self) -> bool {
self.len() == 0
}

/// Iterate over [`BundleInfo`].
pub fn iter(&self) -> impl Iterator<Item = &BundleInfo> {
self.bundle_infos.iter()
}

/// Gets the metadata associated with a specific type of bundle.
/// Returns `None` if the bundle is not registered with the world.
#[inline]
Expand Down
8 changes: 7 additions & 1 deletion crates/bevy_ecs/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
};
#[cfg(feature = "bevy_reflect")]
use alloc::boxed::Box;
use alloc::{borrow::Cow, format, vec::Vec};
use alloc::{borrow::Cow, format, string::String, vec::Vec};
pub use bevy_ecs_macros::Component;
use bevy_platform_support::sync::Arc;
use bevy_ptr::{OwningPtr, UnsafeCellDeref};
Expand Down Expand Up @@ -855,6 +855,12 @@ impl ComponentId {
pub fn index(self) -> usize {
self.0
}

/// Returns a string containing information about the component in the context of the given [World].
pub fn diagnose(self, world: &World) -> String {
let component = world.components().get_info(self).unwrap();
format!("{:?}({})", self, component.name())
}
}

impl SparseSetIndex for ComponentId {
Expand Down
150 changes: 150 additions & 0 deletions crates/bevy_ecs/src/schedule/schedule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
reason = "This instance of module inception is being discussed; see #17344."
)]
use alloc::{
borrow::Cow,
boxed::Box,
collections::BTreeSet,
format,
Expand Down Expand Up @@ -211,6 +212,83 @@ impl Schedules {

self
}

/// Returns a string containing information about the systems.
pub fn diagnose(&self) -> Result<String, core::fmt::Error> {
let mut result = "Schedule:\n".to_string();

let label_with_schedules = self.iter().collect::<Vec<_>>();
writeln!(result, " schedules: {}", label_with_schedules.len())?;

for (label, schedule) in label_with_schedules {
let mut id_to_names = HashMap::<NodeId, Cow<'static, str>>::default();
schedule.systems_for_each(|node_id, system| {
id_to_names.insert(node_id, system.name());
});
for (node_id, set, _) in schedule.graph().system_sets() {
id_to_names.insert(node_id, format!("{:?}", set).into());
}

writeln!(
result,
" label: {:?} kind:{:?}",
label,
schedule.get_executor_kind(),
)?;

let schedule_graph = schedule.graph();

writeln!(
result,
"{}",
schedule_graph
.hierarchy()
.diagnose(" ", "hierarchy", &id_to_names)?
.trim_end()
)?;

writeln!(
result,
"{}",
schedule_graph
.dependency()
.diagnose(" ", "dependency", &id_to_names)?
.trim_end()
)?;
}

Ok(result)
}

/// Returns a string containing information about the systems, including flattened representation.
pub fn diagnose_flattened(&mut self) -> Result<String, core::fmt::Error> {
let mut result = self.diagnose()?;

let label_with_schedules = self.iter_mut().collect::<Vec<_>>();

for (_, schedule) in label_with_schedules {
let mut id_to_names = HashMap::<NodeId, Cow<'static, str>>::default();
schedule.systems_for_each(|node_id, system| {
id_to_names.insert(node_id, system.name());
});
for (node_id, set, _) in schedule.graph().system_sets() {
id_to_names.insert(node_id, format!("{:?}", set).into());
}

let schedule_graph = &mut schedule.graph;

writeln!(
result,
"{}",
schedule_graph
.dependency_flatten()
.diagnose(" ", "dependency flatten", &id_to_names,)?
.trim_end()
)?;
}

Ok(result)
}
}

fn make_executor(kind: ExecutorKind) -> Box<dyn SystemExecutor> {
Expand Down Expand Up @@ -346,6 +424,27 @@ impl Schedule {
self
}

/// Call function `f` on each pair of ([`NodeId`], [`ScheduleSystem`]).
fn systems_for_each(&self, mut f: impl FnMut(NodeId, &ScheduleSystem)) {
match self.executor_initialized {
true => {
for (id, system) in self
.executable
.system_ids
.iter()
.zip(self.executable.systems.iter())
{
f(*id, system);
}
}
false => {
for (node_id, system, _) in self.graph.systems() {
f(node_id, system);
}
}
}
}

/// Configures a collection of system sets in this schedule, adding them if they does not exist.
#[track_caller]
pub fn configure_sets(&mut self, sets: impl IntoSystemSetConfigs) -> &mut Self {
Expand Down Expand Up @@ -552,6 +651,41 @@ impl Dag {
pub fn cached_topsort(&self) -> &[NodeId] {
&self.topsort
}

/// Returns a string containing node and edge information about the [`Dag`].
pub fn diagnose(
&self,
prefix: &str,
name: &str,
id_to_names: &HashMap<NodeId, Cow<'static, str>>,
) -> Result<String, core::fmt::Error> {
let mut result = String::new();
writeln!(result, "{prefix}{name}:")?;

writeln!(result, "{prefix} nodes:")?;
for node_id in self.graph().nodes() {
let name = id_to_names.get(&node_id).unwrap();
writeln!(result, "{prefix} {node_id:?}({name})")?;
}

writeln!(result, "{prefix} edges:")?;
for (l, r) in self.graph().all_edges() {
let l_name = id_to_names.get(&l).unwrap();
let r_name = id_to_names.get(&r).unwrap();
writeln!(result, "{prefix} {l:?}({l_name}) -> {r:?}({r_name})")?;
}

writeln!(result, "{prefix} topsorted:")?;
for (node_id, node_name) in self
.cached_topsort()
.iter()
.map(|node_id| (node_id, id_to_names.get(node_id).unwrap()))
{
writeln!(result, "{prefix} {node_id:?}({node_name})")?;
}

Ok(result)
}
}

/// A [`SystemSet`] with metadata, stored in a [`ScheduleGraph`].
Expand Down Expand Up @@ -737,6 +871,22 @@ impl ScheduleGraph {
&self.dependency
}

/// Returns the [`Dag`] of the flattened dependencies in the schedule.
///
/// Nodes in this graph are systems and sets, and edges denote that
/// a system or set has to run before another system or set.
pub fn dependency_flatten(&mut self) -> Dag {
let (set_systems, _) =
self.map_sets_to_systems(&self.hierarchy.topsort, &self.hierarchy.graph);
let dependency_flattened = self.get_dependency_flattened(&set_systems);
Dag {
topsort: self
.topsort_graph(&dependency_flattened, ReportCycles::Dependency)
.unwrap(),
graph: dependency_flattened,
}
}

/// Returns the list of systems that conflict with each other, i.e. have ambiguities in their access.
///
/// If the `Vec<ComponentId>` is empty, the systems conflict on [`World`] access.
Expand Down
Loading
Loading