Skip to content

Add an opt-in warning on missed events #7092

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion crates/bevy_a11y/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ use accesskit::{NodeBuilder, NodeId};
use bevy_app::Plugin;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
prelude::{Component, Entity},
prelude::{Component, Entity, Event},
system::Resource,
};

/// Wrapper struct for [`accesskit::ActionRequest`]. Required to allow it to be used as an `Event`.
#[derive(Event, Deref, DerefMut)]
pub struct ActionRequest(pub accesskit::ActionRequest);

/// Resource that tracks whether an assistive technology has requested
/// accessibility information.
///
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ impl App {
/// # use bevy_app::prelude::*;
/// # use bevy_ecs::prelude::*;
/// #
/// # #[derive(Event)]
/// # struct MyEvent;
/// # let mut app = App::new();
/// #
Expand Down Expand Up @@ -1009,7 +1010,7 @@ fn run_once(mut app: App) {
/// If you don't require access to other components or resources, consider implementing the [`Drop`]
/// trait on components/resources for code that runs on exit. That saves you from worrying about
/// system schedule ordering, and is idiomatic Rust.
#[derive(Debug, Clone, Default)]
#[derive(Event, Debug, Clone, Default)]
pub struct AppExit;

#[cfg(test)]
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_asset/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use std::fmt::Debug;
///
/// Events sent via the [`Assets`] struct will always be sent with a _Weak_ handle, because the
/// asset may not exist by the time the event is handled.
#[derive(Event)]
pub enum AssetEvent<T: Asset> {
#[allow(missing_docs)]
Created { handle: Handle<T> },
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ecs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ Events offer a communication channel between one or more systems. Events can be
```rust
use bevy_ecs::prelude::*;

#[derive(Event)]
struct MyEvent {
message: String,
}
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ecs/examples/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fn main() {
}

// This is our event that we will send and receive in systems
#[derive(Event)]
struct MyEvent {
pub message: String,
pub random_value: f32,
Expand Down
107 changes: 107 additions & 0 deletions crates/bevy_ecs/macros/src/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use bevy_macro_utils::{get_lit_str, Symbol};
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, parse_quote, DeriveInput, Error, Path, Result};

pub fn derive_event(input: TokenStream) -> TokenStream {
let mut ast = parse_macro_input!(input as DeriveInput);
let bevy_ecs_path: Path = crate::bevy_ecs_path();

ast.generics
.make_where_clause()
.predicates
.push(parse_quote! { Self: Send + Sync + 'static });

let attrs = match parse_event_attr(&ast) {
Ok(attrs) => attrs,
Err(e) => return e.into_compile_error().into(),
};

let missed = attrs.missed;

let warn_missed = match missed {
Missed::Ignore => quote! { const WARN_MISSED: bool = false; },
Missed::DebugWarn => quote! {
#[cfg(debug_assertions)]
const WARN_MISSED: bool = true;
#[cfg(not(debug_assertions))]
const WARN_MISSED: bool = false;
},
Missed::Warn => quote! { const WARN_MISSED:bool = true; },
};

let struct_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();

TokenStream::from(quote! {
impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {
#warn_missed
}
})
}

struct Attrs {
missed: Missed,
}

enum Missed {
Ignore,
DebugWarn,
Warn,
}

pub const EVENT: Symbol = Symbol("event");
pub const MISSED: Symbol = Symbol("missed");

const IGNORE: &str = "ignore";
const DEBUG_WARN: &str = "debug_warn";
const WARN: &str = "warn";

fn parse_event_attr(ast: &DeriveInput) -> Result<Attrs> {
let meta_items = bevy_macro_utils::parse_attrs(ast, EVENT)?;

let mut attrs = Attrs {
missed: Missed::DebugWarn,
};

for meta in meta_items {
use syn::{
Meta::NameValue,
NestedMeta::{Lit, Meta},
};
match meta {
Meta(NameValue(m)) if m.path == MISSED => {
attrs.missed = match get_lit_str(MISSED, &m.lit)?.value().as_str() {
IGNORE => Missed::Ignore,
DEBUG_WARN => Missed::DebugWarn,
WARN => Missed::Warn,
e => {
return Err(Error::new_spanned(
m.lit,
format!(
"Invalid missed event behaviour `{e}`, expected '{IGNORE}', '{DEBUG_WARN}', or '{WARN}'.",
),
))
}
}
}
Meta(meta_item) => {
return Err(Error::new_spanned(
meta_item.path(),
format!(
"unknown event attribute `{}`",
meta_item.path().into_token_stream()
),
));
}
Lit(lit) => {
return Err(Error::new_spanned(
lit,
"unexpected literal in event attribute",
))
}
}
}

Ok(attrs)
}
6 changes: 6 additions & 0 deletions crates/bevy_ecs/macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
extern crate proc_macro;

mod component;
mod event;
mod fetch;
mod set;
mod states;
Expand Down Expand Up @@ -482,6 +483,11 @@ pub(crate) fn bevy_ecs_path() -> syn::Path {
BevyManifest::default().get_path("bevy_ecs")
}

#[proc_macro_derive(Event, attributes(event))]
pub fn derive_event(input: TokenStream) -> TokenStream {
event::derive_event(input)
}

#[proc_macro_derive(Resource)]
pub fn derive_resource(input: TokenStream) -> TokenStream {
component::derive_resource(input)
Expand Down
61 changes: 53 additions & 8 deletions crates/bevy_ecs/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@

use crate as bevy_ecs;
use crate::system::{Local, Res, ResMut, Resource, SystemParam};
use bevy_utils::tracing::trace;
pub use bevy_ecs_macros::Event;
use bevy_utils::tracing::{trace, warn};
use std::ops::{Deref, DerefMut};
use std::{fmt, hash::Hash, iter::Chain, marker::PhantomData, slice::Iter};
/// A type that can be stored in an [`Events<E>`] resource
/// You can conveniently access events using the [`EventReader`] and [`EventWriter`] system parameter.
///
/// Events must be thread-safe.
pub trait Event: Send + Sync + 'static {}
impl<T> Event for T where T: Send + Sync + 'static {}
pub trait Event: Send + Sync + 'static {
/// Whether a warning is emitted if an event is sent but never read
#[cfg(debug_assertions)]
const WARN_MISSED: bool = true;
#[cfg(not(debug_assertions))]
const WARN_MISSED: bool = false;
}

/// An `EventId` uniquely identifies an event.
///
Expand Down Expand Up @@ -77,8 +83,9 @@ struct EventInstance<E: Event> {
///
/// # Example
/// ```
/// use bevy_ecs::event::Events;
/// use bevy_ecs::event::{Event, Events};
///
/// #[derive(Event)]
/// struct MyEvent {
/// value: usize
/// }
Expand Down Expand Up @@ -123,6 +130,30 @@ struct EventInstance<E: Event> {
/// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/event.rs)
/// [Example usage standalone.](https://github.com/bevyengine/bevy/blob/latest/crates/bevy_ecs/examples/events.rs)
///
/// # Missed events
///
/// Obscure bugs may arise if an [`EventReader`] does not read all events before they are cleared.
/// By default, a warning will be emitted in debug mode, but will be silently ignored in release mode.
///
/// This behaviour can be overridden with an additional attribute.
/// ```
/// # use bevy_ecs::event::{Event, Events};
/// #
/// // This event will not emit warnings when one is missed.
/// #[derive(Event)]
/// #[event(missed = "ignore")]
/// struct EventA;
///
/// // This event will emit only emit warnings if one is missed in debug mode.
/// #[derive(Event)]
/// #[event(missed = "debug_warn")]
/// struct EventB;
///
/// // This event will always emit warnings when one is missed.
/// #[derive(Event)]
/// #[event(missed = "warn")]
/// struct EventC;
/// ```
#[derive(Debug, Resource)]
pub struct Events<E: Event> {
/// Holds the oldest still active events.
Expand Down Expand Up @@ -216,6 +247,8 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> {
///
/// ```
/// # use bevy_ecs::prelude::*;
/// #
/// #[derive(Event)]
/// struct CollisionEvent;
///
/// fn play_collision_sound(mut events: EventReader<CollisionEvent>) {
Expand Down Expand Up @@ -257,6 +290,7 @@ impl<'a, 'w, 's, E: Event> IntoIterator for &'a mut EventReader<'w, 's, E> {
/// ```
/// # use bevy_ecs::prelude::*;
///
/// #[derive(Event)]
/// pub struct MyEvent; // Custom event type.
/// fn my_system(mut writer: EventWriter<MyEvent>) {
/// writer.send(MyEvent);
Expand All @@ -273,7 +307,7 @@ impl<'a, 'w, 's, E: Event> IntoIterator for &'a mut EventReader<'w, 's, E> {
///
/// ```
/// # use bevy_ecs::{prelude::*, event::Events};
///
/// # #[derive(Event)]
/// # pub struct MyEvent;
/// fn send_untyped(mut commands: Commands) {
/// // Send an event of a specific type without having to declare that
Expand Down Expand Up @@ -343,6 +377,17 @@ impl<E: Event> ManualEventReader<E> {
&'a mut self,
events: &'a Events<E>,
) -> ManualEventIteratorWithId<'a, E> {
if E::WARN_MISSED {
// if the reader has seen some of the events in a buffer, find the proper index offset.
// otherwise read all events in the buffer
let missed = self.missed_events(events);
if missed > 0 {
let event_noun = if missed == 1 { "event" } else { "events" };
let type_name = std::any::type_name::<E>();
warn!("Missed {missed} `{type_name}` {event_noun}.");
}
}

ManualEventIteratorWithId::new(self, events)
}

Expand Down Expand Up @@ -671,7 +716,7 @@ mod tests {

use super::*;

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[derive(Event, Copy, Clone, PartialEq, Eq, Debug)]
struct TestEvent {
i: usize,
}
Expand Down Expand Up @@ -774,7 +819,7 @@ mod tests {
reader.iter(events).cloned().collect::<Vec<E>>()
}

#[derive(PartialEq, Eq, Debug)]
#[derive(Event, PartialEq, Eq, Debug)]
struct E(usize);

fn events_clear_and_read_impl(clear_func: impl FnOnce(&mut Events<E>)) {
Expand Down Expand Up @@ -981,7 +1026,7 @@ mod tests {
assert!(last.is_none(), "EventReader should be empty");
}

#[derive(Clone, PartialEq, Debug, Default)]
#[derive(Event, Clone, PartialEq, Debug, Default)]
struct EmptyTestEvent;

#[test]
Expand Down
6 changes: 4 additions & 2 deletions crates/bevy_ecs/src/removal_detection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use crate::{
self as bevy_ecs,
component::{Component, ComponentId, ComponentIdFor},
entity::Entity,
event::{EventId, Events, ManualEventIterator, ManualEventIteratorWithId, ManualEventReader},
event::{
Event, EventId, Events, ManualEventIterator, ManualEventIteratorWithId, ManualEventReader,
},
prelude::Local,
storage::SparseSet,
system::{ReadOnlySystemParam, SystemMeta, SystemParam},
Expand All @@ -21,7 +23,7 @@ use std::{

/// Wrapper around [`Entity`] for [`RemovedComponents`].
/// Internally, `RemovedComponents` uses these as an `Events<RemovedComponentEntity>`.
#[derive(Debug, Clone)]
#[derive(Event, Debug, Clone)]
pub struct RemovedComponentEntity(Entity);

impl From<RemovedComponentEntity> for Entity {
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_ecs/src/system/function_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ impl SystemMeta {
/// use bevy_ecs::{system::SystemState};
/// use bevy_ecs::event::Events;
///
/// #[derive(Event)]
/// struct MyEvent;
/// #[derive(Resource)]
/// struct MyResource(u32);
Expand Down Expand Up @@ -118,6 +119,7 @@ impl SystemMeta {
/// use bevy_ecs::{system::SystemState};
/// use bevy_ecs::event::Events;
///
/// #[derive(Event)]
/// struct MyEvent;
/// #[derive(Resource)]
/// struct CachedSystemState {
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/system/system_param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ fn assert_component_access_compatibility(
/// ```
/// # use bevy_ecs::prelude::*;
/// #
/// # #[derive(Event)]
/// # struct MyEvent;
/// # impl MyEvent {
/// # pub fn new() -> Self { Self }
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_hierarchy/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use bevy_ecs::prelude::Entity;
use bevy_ecs::{event::Event, prelude::Entity};

/// An [`Event`] that is fired whenever there is a change in the world's hierarchy.
///
/// [`Event`]: bevy_ecs::event::Event
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Event, Debug, Clone, PartialEq, Eq)]
pub enum HierarchyEvent {
/// Fired whenever an [`Entity`] is added as a child to a parent.
ChildAdded {
Expand Down
Loading