Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4c0eec6
Create an initial setup for dumping out schedule data into a serializ…
andriyDev Jan 14, 2026
0e355d3
Create a plugin that can be added to automatically dump all schedules.
andriyDev Jan 14, 2026
13cadcc
Create an example to dump out all the schedule data.
andriyDev Jan 15, 2026
40209a5
Move build warnings into a more extensible `ScheduleBuildMetadata` st…
andriyDev Feb 15, 2026
594bac5
Make build passes record which edges they add and return that in the …
andriyDev Jan 15, 2026
6fbc10d
Send the schedule build metadata as an event instead of storing it.
andriyDev Feb 15, 2026
e35a02d
Add the edges added by build passes to the output schedule data.
andriyDev Feb 15, 2026
452ab3c
Merge "main" branch into dump-schedules.
andriyDev Apr 1, 2026
ce6d00c
Improve the docs.
andriyDev Apr 1, 2026
930f80b
Explain the default location for app_data in the example.
andriyDev Apr 1, 2026
9040f18
Use an enum to indicate world conflict instead of an empty Vec.
andriyDev Apr 2, 2026
9ba71e2
Remove deprecated method call.
andriyDev Apr 2, 2026
fab36e0
Remove unhelpful helper.
andriyDev Apr 2, 2026
32716f4
Change the severity of errors to Warning.
andriyDev Apr 2, 2026
f3563b6
Explain why we don't record removed edges.
andriyDev Apr 2, 2026
f88e34e
Use a newtype for hierarchy keys.
andriyDev Apr 2, 2026
68ca5f9
Add a test that the plugin code actually extracts the schedule data.
andriyDev Apr 2, 2026
1a68b21
Move the .gitignore to the root.
andriyDev Apr 2, 2026
543af4f
Propagate the schedule_data feature flag all the way up instead of us…
andriyDev Apr 2, 2026
426b0bd
Update feature list.
andriyDev Apr 2, 2026
a7b5295
Appease CI false positive.
andriyDev Apr 2, 2026
4787615
Simple suggestions from code review
alice-i-cecile Apr 6, 2026
a49718b
cargo fmt
alice-i-cecile Apr 6, 2026
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ assets/scenes/load_scene_example-new.scn.ron

# Generated by "examples/large_scenes"
compressed_texture_cache

# Generated by "examples/dev_tools/schedule_data.rs"
**/app_data.ron
15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,9 @@ bevy_debug_stepping = [
"bevy_internal/debug",
]

# Enable collecting schedule data from the app.
schedule_data = ["bevy_internal/schedule_data"]

# Enables the meshlet renderer for dense high-poly scenes (experimental)
meshlet = ["bevy_internal/meshlet"]

Expand Down Expand Up @@ -4609,6 +4612,18 @@ description = "Demonstrates FPS overlay"
category = "Dev tools"
wasm = true

[[example]]
name = "schedule_data"
path = "examples/dev_tools/schedule_data.rs"
doc-scrape-examples = true
required-features = ["debug", "schedule_data"]

[package.metadata.example.schedule_data]
name = "Extract Schedule Data"
description = "Extracts the schedule data from a default app and writes it to a file"
category = "Dev tools"
wasm = false

[[example]]
name = "infinite_grid"
path = "examples/dev_tools/infinite_grid.rs"
Expand Down
18 changes: 17 additions & 1 deletion crates/bevy_dev_tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,17 @@ license = "MIT OR Apache-2.0"
keywords = ["bevy"]

[features]
bevy_ci_testing = ["serde", "ron"]
bevy_ci_testing = ["dep:serde", "dep:ron"]
screenrecording = ["dep:x264"]
webgl = ["bevy_render/webgl"]
webgpu = ["bevy_render/webgpu"]
schedule_data = [
"dep:serde",
"dep:ron",
"dep:bevy_platform",
"dep:bevy_utils",
"dep:thiserror",
]

[dependencies]
# bevy
Expand All @@ -37,14 +44,23 @@ bevy_transform = { path = "../bevy_transform", version = "0.19.0-dev" }
bevy_shader = { path = "../bevy_shader", version = "0.19.0-dev" }
bevy_ui = { path = "../bevy_ui", version = "0.19.0-dev" }
bevy_ui_render = { path = "../bevy_ui_render", version = "0.19.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.19.0-dev", optional = true }
bevy_window = { path = "../bevy_window", version = "0.19.0-dev" }
bevy_state = { path = "../bevy_state", version = "0.19.0-dev" }
bevy_platform = { path = "../bevy_platform", version = "0.19.0-dev", optional = true }

# other
thiserror = { version = "2.0", default-features = false, optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
ron = { version = "0.12", optional = true }
tracing = { version = "0.1", default-features = false, features = ["std"] }

[dev-dependencies]
# Allow tests to depend on the names of systems.
bevy_ecs = { path = "../bevy_ecs", version = "0.19.0-dev", features = [
"debug",
] }

[target.'cfg(not(target_os = "windows"))'.dependencies]
x264 = { version = "0.5.0", optional = true }

Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_dev_tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub mod frame_time_graph;

pub mod picking_debug;

#[cfg(feature = "schedule_data")]
pub mod schedule_data;

pub mod states;

pub use easy_screenshot::*;
Expand Down
5 changes: 5 additions & 0 deletions crates/bevy_dev_tools/src/schedule_data/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Tools for extracting schedule data from an app, and interpreting that data for use with
//! visualization tools (for example).

pub mod plugin;
pub mod serde;
171 changes: 171 additions & 0 deletions crates/bevy_dev_tools/src/schedule_data/plugin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//! Convenience plugin for automatically performing serialization of schedules on boot.
use std::{fs::File, io::Write, path::PathBuf};

use bevy_app::{App, Main, Plugin};
use bevy_ecs::{
error::{BevyError, ResultSeverityExt, Severity},
intern::Interned,
resource::Resource,
schedule::{
common_conditions::run_once, IntoScheduleConfigs, ScheduleLabel, Schedules, SystemSet,
},
world::World,
};
use bevy_platform::collections::HashMap;
use ron::ser::PrettyConfig;

use crate::schedule_data::serde::AppData;

/// A plugin to automatically collect and write all schedule data on boot to a file that can later
/// be parsed.
///
/// By default, the schedule data is written to `<current working directory>/app_data.ron`. This can
/// be configured to a different path using [`SerializeSchedulesFilePath`].
pub struct SerializeSchedulesPlugin {
/// The schedule into which the systems for collecting/writing the schedule data are added.
///
/// This schedule **will not** have its schedule data collected, as well as any "parent"
/// schedules. In order to run a schedule, Bevy removes it from the world, meaning if this
/// system is added to schedule [`Update`](bevy_app::Update), that schedule and also [`Main`]
/// will not be included in the [`AppData`]. The default is the [`Main`] schedule since usually
/// there is only one system ([`Main::run_main`]), so there's very little data to collect.
///
/// Avoid changing this field. This is intended for power-users who might not use the [`Main`]
/// schedule at all. It may also be worth considering just calling [`AppData::from_schedules`]
/// manually to ensure a particular schedule is present.
///
/// Usually, this will be set using [`Self::in_schedule`].
pub schedule: Interned<dyn ScheduleLabel>,
}

impl Default for SerializeSchedulesPlugin {
fn default() -> Self {
Self {
schedule: Main.intern(),
}
}
}

impl SerializeSchedulesPlugin {
/// Creates an instance of [`Self`] that inserts into the specified schedule.
pub fn in_schedule(label: impl ScheduleLabel) -> Self {
Self {
schedule: label.intern(),
}
}
}

impl Plugin for SerializeSchedulesPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SerializeSchedulesFilePath>()
.add_systems(
self.schedule,
collect_system_data
.run_if(run_once)
.in_set(SerializeSchedulesSystems)
// While we may not be in the `Main` schedule at all, the default is that, so we
// should make this work properly in the default case.
.before(Main::run_main),
);
}
}

/// A system set for allowing users to configure scheduling properties of systems in
/// [`SerializeSchedulesPlugin`].
#[derive(SystemSet, Hash, PartialEq, Eq, Debug, Clone)]
pub struct SerializeSchedulesSystems;

/// The file path where schedules will be written to after collected by
/// [`SerializeSchedulesPlugin`].
#[derive(Resource)]
pub struct SerializeSchedulesFilePath(pub PathBuf);

impl Default for SerializeSchedulesFilePath {
fn default() -> Self {
Self("app_data.ron".into())
}
}

/// The inner part of [`collect_system_data`] that returns the [`AppData`] so we can write tests
/// without needing to write to disk.
fn collect_system_data_inner(world: &mut World) -> Result<AppData, BevyError> {
let schedules = world.resource::<Schedules>();
let labels = schedules
.iter()
.map(|schedule| schedule.1.label())
.collect::<Vec<_>>();
let mut label_to_build_metadata = HashMap::new();

for label in labels {
let mut schedules = world.resource_mut::<Schedules>();
let mut schedule = schedules.remove(label).unwrap();
let Some(build_metadata) = schedule.initialize(world)? else {
return Err(
"The schedule has already been built, so we can't collect its system data".into(),
);
};

label_to_build_metadata.insert(label, build_metadata);

let mut schedules = world.resource_mut::<Schedules>();
schedules.insert(schedule);
}

let schedules = world.resource::<Schedules>();
Ok(AppData::from_schedules(
schedules,
world.components(),
&label_to_build_metadata,
)?)
}

/// A system that collects all the schedule data and writes it to [`SerializeSchedulesFilePath`].
fn collect_system_data(world: &mut World) -> Result<(), BevyError> {
let app_data = collect_system_data_inner(world).with_severity(Severity::Warning)?;
let file_path = world
.get_resource::<SerializeSchedulesFilePath>()
.ok_or("Missing SerializeSchedulesFilePath resource")
.with_severity(Severity::Warning)?;
let mut file = File::create(&file_path.0).with_severity(Severity::Warning)?;
// Use \n unconditionally so that Windows formatting is predictable.
let serialized = ron::ser::to_string_pretty(&app_data, PrettyConfig::default().new_line("\n"))?;
file.write_all(serialized.as_bytes())
.with_severity(Severity::Warning)?;
Ok(())
}

#[cfg(test)]
mod tests {
use bevy_app::{App, PostUpdate, Update};

use crate::schedule_data::{
plugin::collect_system_data_inner,
serde::tests::{remove_module_paths, simple_system, sort_app_data},
};

#[test]
fn collects_all_schedules() {
// Start with an empty app so only our stuff gets added.
let mut app = App::empty();

fn a() {}
fn b() {}
fn c() {}
app.add_systems(Update, (a, b));
app.add_systems(PostUpdate, c);

// Normally users would use the plugin, but to avoid writing to disk in a test, we just call
// the inner part of the system directly.
let mut app_data = collect_system_data_inner(app.world_mut()).unwrap();
remove_module_paths(&mut app_data);
sort_app_data(&mut app_data);

assert_eq!(app_data.schedules.len(), 2);
let post_update = &app_data.schedules[0];
assert_eq!(post_update.name, "PostUpdate");
assert_eq!(post_update.systems, [simple_system("c")]);
let update = &app_data.schedules[1];
assert_eq!(update.name, "Update");
assert_eq!(update.systems, [simple_system("a"), simple_system("b")]);
}
}
Loading