Better warnings about invalid parameters (#15500)

# Objective

System param validation warnings should be configurable and default to
"warn once" (per system).

Fixes: #15391

## Solution

`SystemMeta` is given a new `ParamWarnPolicy` field.
The policy decides whether warnings will be emitted by each system param
when it fails validation.
The policy is updated by the system after param validation fails.

Example warning:
```
2024-09-30T18:10:04.740749Z  WARN bevy_ecs::system::function_system: System fallible_params::do_nothing_fail_validation will not run because it requested inaccessible system parameter Single<(), (With<Player>, With<Enemy>)>
```

Currently, only the first invalid parameter is displayed.

Warnings can be disabled on function systems using
`.param_never_warn()`.
(there is also `.with_param_warn_policy(policy)`)

## Testing

Ran `fallible_params` example.

---------

Co-authored-by: SpecificProtagonist <vincentjunge@posteo.net>
This commit is contained in:
MiniaczQ 2024-10-03 15:16:55 +02:00 committed by GitHub
parent ca8dd06146
commit acea4e7e6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 176 additions and 58 deletions

View file

@ -60,7 +60,7 @@ pub mod prelude {
Commands, Deferred, EntityCommand, EntityCommands, In, InMut, InRef, IntoSystem, Local,
NonSend, NonSendMut, ParallelCommands, ParamSet, Populated, Query, ReadOnlySystem, Res,
ResMut, Resource, Single, System, SystemIn, SystemInput, SystemParamBuilder,
SystemParamFunction,
SystemParamFunction, WithParamWarnPolicy,
},
world::{
Command, EntityMut, EntityRef, EntityWorldMut, FromWorld, OnAdd, OnInsert, OnRemove,

View file

@ -180,18 +180,6 @@ mod __rust_begin_short_backtrace {
}
}
#[macro_export]
/// Emits a warning about system being skipped.
macro_rules! warn_system_skipped {
($ty:literal, $sys:expr) => {
bevy_utils::tracing::warn!(
"{} {} was skipped due to inaccessible system parameters.",
$ty,
$sys
)
};
}
#[cfg(test)]
mod tests {
use crate::{

View file

@ -19,7 +19,6 @@ use crate::{
query::Access,
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
system::BoxedSystem,
warn_system_skipped,
world::{unsafe_world_cell::UnsafeWorldCell, World},
};
@ -524,7 +523,7 @@ impl ExecutorState {
unsafe fn should_run(
&mut self,
system_index: usize,
system: &BoxedSystem,
system: &mut BoxedSystem,
conditions: &mut Conditions,
world: UnsafeWorldCell,
) -> bool {
@ -575,7 +574,6 @@ impl ExecutorState {
// - `update_archetype_component_access` has been called for system.
let valid_params = unsafe { system.validate_param_unsafe(world) };
if !valid_params {
warn_system_skipped!("System", system.name());
self.skipped_systems.insert(system_index);
}
should_run &= valid_params;
@ -751,7 +749,6 @@ unsafe fn evaluate_and_fold_conditions(
// required by the condition.
// - `update_archetype_component_access` has been called for condition.
if !unsafe { condition.validate_param_unsafe(world) } {
warn_system_skipped!("Condition", condition.name());
return false;
}
// SAFETY:

View file

@ -7,7 +7,6 @@ use crate::{
schedule::{
executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule,
},
warn_system_skipped,
world::World,
};
@ -83,9 +82,6 @@ impl SystemExecutor for SimpleExecutor {
let system = &mut schedule.systems[system_index];
if should_run {
let valid_params = system.validate_param(world);
if !valid_params {
warn_system_skipped!("System", system.name());
}
should_run &= valid_params;
}
@ -139,7 +135,6 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W
.iter_mut()
.map(|condition| {
if !condition.validate_param(world) {
warn_system_skipped!("Condition", condition.name());
return false;
}
__rust_begin_short_backtrace::readonly_run(&mut **condition, world)

View file

@ -5,7 +5,6 @@ use fixedbitset::FixedBitSet;
use crate::{
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
warn_system_skipped,
world::World,
};
@ -89,9 +88,6 @@ impl SystemExecutor for SingleThreadedExecutor {
let system = &mut schedule.systems[system_index];
if should_run {
let valid_params = system.validate_param(world);
if !valid_params {
warn_system_skipped!("System", system.name());
}
should_run &= valid_params;
}
@ -171,7 +167,6 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W
.iter_mut()
.map(|condition| {
if !condition.validate_param(world) {
warn_system_skipped!("Condition", condition.name());
return false;
}
__rust_begin_short_backtrace::readonly_run(&mut **condition, world)

View file

@ -179,8 +179,9 @@ where
}
#[inline]
unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool {
self.system.validate_param_unsafe(world)
unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool {
// SAFETY: Delegate to other `System` implementations.
unsafe { self.system.validate_param_unsafe(world) }
}
fn initialize(&mut self, world: &mut crate::prelude::World) {

View file

@ -212,8 +212,9 @@ where
}
#[inline]
unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool {
self.a.validate_param_unsafe(world) && self.b.validate_param_unsafe(world)
unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool {
// SAFETY: Delegate to other `System` implementations.
unsafe { self.a.validate_param_unsafe(world) && self.b.validate_param_unsafe(world) }
}
fn initialize(&mut self, world: &mut World) {
@ -430,8 +431,9 @@ where
self.b.queue_deferred(world);
}
unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool {
self.a.validate_param_unsafe(world) && self.b.validate_param_unsafe(world)
unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool {
// SAFETY: Delegate to other `System` implementations.
unsafe { self.a.validate_param_unsafe(world) && self.b.validate_param_unsafe(world) }
}
fn validate_param(&mut self, world: &World) -> bool {

View file

@ -150,7 +150,8 @@ where
}
#[inline]
unsafe fn validate_param_unsafe(&self, _world: UnsafeWorldCell) -> bool {
unsafe fn validate_param_unsafe(&mut self, _world: UnsafeWorldCell) -> bool {
// All exclusive system params are always available.
true
}

View file

@ -43,6 +43,7 @@ pub struct SystemMeta {
is_send: bool,
has_deferred: bool,
pub(crate) last_run: Tick,
param_warn_policy: ParamWarnPolicy,
#[cfg(feature = "trace")]
pub(crate) system_span: Span,
#[cfg(feature = "trace")]
@ -59,6 +60,7 @@ impl SystemMeta {
is_send: true,
has_deferred: false,
last_run: Tick::new(0),
param_warn_policy: ParamWarnPolicy::Once,
#[cfg(feature = "trace")]
system_span: info_span!("system", name = name),
#[cfg(feature = "trace")]
@ -75,6 +77,7 @@ impl SystemMeta {
/// Sets the name of of this system.
///
/// Useful to give closure systems more readable and unique names for debugging and tracing.
#[inline]
pub fn set_name(&mut self, new_name: impl Into<Cow<'static, str>>) {
let new_name: Cow<'static, str> = new_name.into();
#[cfg(feature = "trace")]
@ -108,9 +111,94 @@ impl SystemMeta {
/// Marks the system as having deferred buffers like [`Commands`](`super::Commands`)
/// This lets the scheduler insert [`apply_deferred`](`crate::prelude::apply_deferred`) systems automatically.
#[inline]
pub fn set_has_deferred(&mut self) {
self.has_deferred = true;
}
/// Changes the warn policy.
#[inline]
pub(crate) fn set_param_warn_policy(&mut self, warn_policy: ParamWarnPolicy) {
self.param_warn_policy = warn_policy;
}
/// Advances the warn policy after validation failed.
#[inline]
pub(crate) fn advance_param_warn_policy(&mut self) {
self.param_warn_policy.advance();
}
/// Emits a warning about inaccessible system param if policy allows it.
#[inline]
pub fn try_warn_param<P>(&self)
where
P: SystemParam,
{
self.param_warn_policy.try_warn::<P>(&self.name);
}
}
/// State machine for emitting warnings when [system params are invalid](System::validate_param).
#[derive(Clone, Copy)]
pub enum ParamWarnPolicy {
/// No warning should ever be emitted.
Never,
/// The warning will be emitted once and status will update to [`Self::Never`].
Once,
}
impl ParamWarnPolicy {
/// Advances the warn policy after validation failed.
#[inline]
fn advance(&mut self) {
*self = Self::Never;
}
/// Emits a warning about inaccessible system param if policy allows it.
#[inline]
fn try_warn<P>(&self, name: &str)
where
P: SystemParam,
{
if matches!(self, Self::Never) {
return;
}
bevy_utils::tracing::warn!(
"{0} did not run because it requested inaccessible system parameter {1}",
name,
disqualified::ShortName::of::<P>()
);
}
}
/// Trait for manipulating warn policy of systems.
#[doc(hidden)]
pub trait WithParamWarnPolicy<M, F>
where
M: 'static,
F: SystemParamFunction<M>,
Self: Sized,
{
/// Set warn policy.
fn with_param_warn_policy(self, warn_policy: ParamWarnPolicy) -> FunctionSystem<M, F>;
/// Disable all param warnings.
fn never_param_warn(self) -> FunctionSystem<M, F> {
self.with_param_warn_policy(ParamWarnPolicy::Never)
}
}
impl<M, F> WithParamWarnPolicy<M, F> for F
where
M: 'static,
F: SystemParamFunction<M>,
{
fn with_param_warn_policy(self, param_warn_policy: ParamWarnPolicy) -> FunctionSystem<M, F> {
let mut system = IntoSystem::into_system(self);
system.system_meta.set_param_warn_policy(param_warn_policy);
system
}
}
// TODO: Actually use this in FunctionSystem. We should probably only do this once Systems are constructed using a World reference
@ -657,9 +745,18 @@ where
}
#[inline]
unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool {
unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool {
let param_state = self.param_state.as_ref().expect(Self::PARAM_MESSAGE);
F::Param::validate_param(param_state, &self.system_meta, world)
// SAFETY:
// - The caller has invoked `update_archetype_component_access`, which will panic
// if the world does not match.
// - All world accesses used by `F::Param` have been registered, so the caller
// will ensure that there are no data access conflicts.
let is_valid = unsafe { F::Param::validate_param(param_state, &self.system_meta, world) };
if !is_valid {
self.system_meta.advance_param_warn_policy();
}
is_valid
}
#[inline]

View file

@ -117,7 +117,7 @@ pub trait System: Send + Sync + 'static {
/// - The method [`System::update_archetype_component_access`] must be called at some
/// point before this one, with the same exact [`World`]. If [`System::update_archetype_component_access`]
/// panics (or otherwise does not return for any reason), this method must not be called.
unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool;
unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool;
/// Safe version of [`System::validate_param_unsafe`].
/// that runs on exclusive, single-threaded `world` pointer.

View file

@ -421,7 +421,11 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam fo
world.change_tick(),
)
};
result.is_ok()
let is_valid = result.is_ok();
if !is_valid {
system_meta.try_warn_param::<Self>();
}
is_valid
}
}
@ -483,7 +487,11 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam
world.change_tick(),
)
};
!matches!(result, Err(QuerySingleError::MultipleEntities(_)))
let is_valid = !matches!(result, Err(QuerySingleError::MultipleEntities(_)));
if !is_valid {
system_meta.try_warn_param::<Self>();
}
is_valid
}
}
@ -773,14 +781,18 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> {
#[inline]
unsafe fn validate_param(
&component_id: &Self::State,
_system_meta: &SystemMeta,
system_meta: &SystemMeta,
world: UnsafeWorldCell,
) -> bool {
// SAFETY: Read-only access to resource metadata.
unsafe { world.storages() }
let is_valid = unsafe { world.storages() }
.resources
.get(component_id)
.is_some_and(ResourceData::is_present)
.is_some_and(ResourceData::is_present);
if !is_valid {
system_meta.try_warn_param::<Self>();
}
is_valid
}
#[inline]
@ -883,14 +895,18 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> {
#[inline]
unsafe fn validate_param(
&component_id: &Self::State,
_system_meta: &SystemMeta,
system_meta: &SystemMeta,
world: UnsafeWorldCell,
) -> bool {
// SAFETY: Read-only access to resource metadata.
unsafe { world.storages() }
let is_valid = unsafe { world.storages() }
.resources
.get(component_id)
.is_some_and(ResourceData::is_present)
.is_some_and(ResourceData::is_present);
if !is_valid {
system_meta.try_warn_param::<Self>();
}
is_valid
}
#[inline]
@ -1429,14 +1445,18 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> {
#[inline]
unsafe fn validate_param(
&component_id: &Self::State,
_system_meta: &SystemMeta,
system_meta: &SystemMeta,
world: UnsafeWorldCell,
) -> bool {
// SAFETY: Read-only access to resource metadata.
unsafe { world.storages() }
let is_valid = unsafe { world.storages() }
.non_send_resources
.get(component_id)
.is_some_and(ResourceData::is_present)
.is_some_and(ResourceData::is_present);
if !is_valid {
system_meta.try_warn_param::<Self>();
}
is_valid
}
#[inline]
@ -1536,14 +1556,18 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> {
#[inline]
unsafe fn validate_param(
&component_id: &Self::State,
_system_meta: &SystemMeta,
system_meta: &SystemMeta,
world: UnsafeWorldCell,
) -> bool {
// SAFETY: Read-only access to resource metadata.
unsafe { world.storages() }
let is_valid = unsafe { world.storages() }
.non_send_resources
.get(component_id)
.is_some_and(ResourceData::is_present)
.is_some_and(ResourceData::is_present);
if !is_valid {
system_meta.try_warn_param::<Self>();
}
is_valid
}
#[inline]

View file

@ -77,12 +77,13 @@ where
#[inline]
unsafe fn validate_param(
state: &Self::State,
_system_meta: &SystemMeta,
system_meta: &SystemMeta,
world: UnsafeWorldCell,
) -> bool {
// SAFETY: Read-only access to world data registered in `init_state`.
let result = unsafe { world.get_resource_by_id(state.main_world_state) };
let Some(main_world) = result else {
system_meta.try_warn_param::<&World>();
return false;
};
// SAFETY: Type is guaranteed by `SystemState`.

View file

@ -20,9 +20,22 @@ fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
// We add all the systems one after another.
// We don't need to use run conditions here.
.add_systems(Update, (user_input, move_targets, move_pointer).chain())
// Systems that fail parameter validation will emit warnings.
// The default policy is to emit a warning once per system.
// This is good for catching unexpected behavior, but can
// lead to spam. You can disable invalid param warnings
// per system using the `.never_param_warn()` method.
.add_systems(
Update,
(
user_input,
move_targets.never_param_warn(),
move_pointer.never_param_warn(),
)
.chain(),
)
// We will leave this systems with default warning policy.
.add_systems(Update, do_nothing_fail_validation)
.run();
}
@ -67,9 +80,9 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
));
}
// System that reads user input.
// If user presses 'A' we spawn a new random enemy.
// If user presses 'R' we remove a random enemy (if any exist).
/// System that reads user input.
/// If user presses 'A' we spawn a new random enemy.
/// If user presses 'R' we remove a random enemy (if any exist).
fn user_input(
mut commands: Commands,
enemies: Query<Entity, With<Enemy>>,
@ -146,3 +159,7 @@ fn move_pointer(
player_transform.rotate_axis(Dir3::Z, player.rotation_speed * time.delta_seconds());
}
}
/// This system always fails param validation, because we never
/// create an entity with both [`Player`] and [`Enemy`] components.
fn do_nothing_fail_validation(_: Single<(), (With<Player>, With<Enemy>)>) {}