From 2756eb419af2210eed3d0574e20a620918e4e577 Mon Sep 17 00:00:00 2001 From: mat Date: Sun, 22 Feb 2026 11:35:41 +0700 Subject: optimizations at high entity counts --- CONTRIBUTING.md | 6 +- Cargo.toml | 2 +- azalea-client/src/plugins/packet/game/mod.rs | 142 ++++++++++-------- azalea-client/src/plugins/packet/mod.rs | 50 ++++--- .../src/plugins/packet/relative_updates.rs | 163 +++++++++++++++++++++ azalea-entity/src/lib.rs | 4 + azalea-entity/src/plugin/components.rs | 4 +- azalea-entity/src/plugin/mod.rs | 52 ++++--- azalea-entity/src/plugin/relative_updates.rs | 115 --------------- azalea/examples/testbot/main.rs | 2 + azalea/examples/testbot/mspt.rs | 69 +++++++++ 11 files changed, 388 insertions(+), 221 deletions(-) create mode 100644 azalea-client/src/plugins/packet/relative_updates.rs delete mode 100644 azalea-entity/src/plugin/relative_updates.rs create mode 100644 azalea/examples/testbot/mspt.rs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf26bf5a..589a113d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,6 +18,8 @@ If you're working with low-level physics or packet related code, it's quite easy The second major thing to watch out for is accidentally introducing performance regressions. Certain parts of Azalea are highly performance sensitive (notably, the pathfinder), so most changes in these areas should be benchmarked to avoid accidentally hurting performance. +You're encouraged to write relevant tests and benchmarks. + ## Profiling Please see [the chapter about profiling in the Rust performance book](https://nnethercote.github.io/perf-book/profiling.html). @@ -29,12 +31,10 @@ cargo install flamegraph RUSTFLAGS="-C force-frame-pointers=yes" cargo r -r --example testbot # wait a few seconds so chunks being loaded doesn't affect the flamegraph, and # then run this in a separate window: -flamegraph -p $(pidof testbot) +flamegraph -p $(pidof testbot) --deterministic # wait about 15 seconds, then ctrl+c, and view the flamegraph.svg ``` ## AI Policy Please avoid using generative AI to make contributions to Azalea. We do not enjoy working with code that wasn't written by people. - - diff --git a/Cargo.toml b/Cargo.toml index a254da3e..9f050d7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,7 +135,7 @@ cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] # --- Profile Settings --- [profile.release] -debug = true +debug = "line-tables-only" # decoding packets takes forever if we don't do this [profile.dev.package.azalea-crypto] diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs index 9a1c022b..9f16c204 100644 --- a/azalea-client/src/plugins/packet/game/mod.rs +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -9,7 +9,7 @@ use azalea_core::{ }; use azalea_entity::{ ActiveEffects, Dead, EntityBundle, EntityKindComponent, HasClientLoaded, LoadedBy, LocalEntity, - LookDirection, Physics, PlayerAbilities, Position, RelativeEntityUpdate, + LookDirection, Physics, PlayerAbilities, Position, indexing::{EntityIdIndex, EntityUuidIndex}, inventory::Inventory, metadata::{Health, apply_metadata}, @@ -36,7 +36,10 @@ use crate::{ inventory::{ClientsideCloseContainerEvent, MenuOpenedEvent, SetContainerContentEvent}, local_player::{Experience, Hunger, LocalGameMode, TabList, WorldHolder}, movement::{KnockbackData, KnockbackEvent}, - packet::{as_system, declare_packet_handlers}, + packet::{ + as_system, declare_packet_handlers, + relative_updates::{EntityUpdateQuery, RelativeEntityUpdate, should_apply_entity_update}, + }, player::{GameProfileComponent, PlayerInfo}, tick_counter::TicksConnected, }; @@ -716,36 +719,37 @@ impl GamePacketHandler<'_> { // vanilla servers use this packet for knockback, but note that the Explode // packet is also sometimes used by servers for knockback - as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>( - self.ecs, - |(mut commands, query)| { - let (entity_id_index, world_holder) = query.get(self.player).unwrap(); - - let Some(entity) = entity_id_index.get_by_minecraft_entity(p.id) else { - // note that this log (and some other ones like the one in RemoveEntities) - // sometimes happens when killing mobs. it seems to be a vanilla bug, which is - // why it's a debug log instead of a warning - debug!( - "Got set entity motion packet for unknown entity id {}", - p.id - ); - return; - }; - - // this is to make sure the same entity velocity update doesn't get sent - // multiple times when in swarms + as_system::<( + Commands, + Query<(&EntityIdIndex, &WorldHolder)>, + EntityUpdateQuery, + )>(self.ecs, |(mut commands, query, entity_update_query)| { + let (entity_id_index, world_holder) = query.get(self.player).unwrap(); - let data = KnockbackData::Set(p.delta.to_vec3()); + let Some(entity) = entity_id_index.get_by_minecraft_entity(p.id) else { + // note that this log (and some other ones like the one in RemoveEntities) + // sometimes happens when killing mobs. it seems to be a vanilla bug, which is + // why it's a debug log instead of a warning + debug!( + "Got set entity motion packet for unknown entity id {}", + p.id + ); + return; + }; - commands.entity(entity).queue(RelativeEntityUpdate::new( - world_holder.partial.clone(), - move |entity_mut| { - entity_mut - .world_scope(|world| world.trigger(KnockbackEvent { entity, data })); - }, - )); - }, - ); + let data = KnockbackData::Set(p.delta.to_vec3()); + + // this is to make sure the same entity velocity update doesn't get sent + // multiple times when in swarms + if should_apply_entity_update( + &mut commands, + &mut world_holder.partial.write(), + entity, + entity_update_query, + ) { + commands.trigger(KnockbackEvent { entity, data }); + } + }); } pub fn set_entity_link(&mut self, p: &ClientboundSetEntityLink) { @@ -795,8 +799,8 @@ impl GamePacketHandler<'_> { as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>( self.ecs, - |(mut commands, mut query)| { - let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap(); + |(mut commands, query)| { + let (entity_id_index, world_holder) = query.get(self.player).unwrap(); let Some(entity) = entity_id_index.get_by_minecraft_entity(p.id) else { warn!("Got teleport entity packet for unknown entity id {}", p.id); @@ -840,8 +844,8 @@ impl GamePacketHandler<'_> { pub fn move_entity_pos(&mut self, p: &ClientboundMoveEntityPos) { as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>( self.ecs, - |(mut commands, mut query)| { - let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap(); + |(mut commands, query)| { + let (entity_id_index, world_holder) = query.get(self.player).unwrap(); debug!("Got move entity pos packet {p:?}"); @@ -879,8 +883,8 @@ impl GamePacketHandler<'_> { pub fn move_entity_pos_rot(&mut self, p: &ClientboundMoveEntityPosRot) { as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>( self.ecs, - |(mut commands, mut query)| { - let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap(); + |(mut commands, query)| { + let (entity_id_index, world_holder) = query.get(self.player).unwrap(); debug!("Got move entity pos rot packet {p:?}"); @@ -929,8 +933,8 @@ impl GamePacketHandler<'_> { pub fn move_entity_rot(&mut self, p: &ClientboundMoveEntityRot) { as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>( self.ecs, - |(mut commands, mut query)| { - let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap(); + |(mut commands, query)| { + let (entity_id_index, world_holder) = query.get(self.player).unwrap(); let entity = entity_id_index.get_by_minecraft_entity(p.entity_id); if let Some(entity) = entity { @@ -1527,10 +1531,28 @@ impl GamePacketHandler<'_> { } pub fn entity_position_sync(&mut self, p: &ClientboundEntityPositionSync) { - as_system::<(Commands, Query<(&EntityIdIndex, &WorldHolder)>)>( + as_system::<( + Commands, + Query<( + &EntityIdIndex, + &WorldHolder, + Option<&LocalEntity>, + &mut Physics, + &mut Position, + &mut LookDirection, + )>, + EntityUpdateQuery, + )>( self.ecs, - |(mut commands, mut query)| { - let (entity_id_index, world_holder) = query.get_mut(self.player).unwrap(); + |(mut commands, mut query, entity_update_query)| { + let ( + entity_id_index, + world_holder, + local_entity, + mut physics, + mut position, + mut look_direction, + ) = query.get_mut(self.player).unwrap(); let Some(entity) = entity_id_index.get_by_minecraft_entity(p.id) else { debug!("Got teleport entity packet for unknown entity id {}", p.id); @@ -1541,28 +1563,32 @@ impl GamePacketHandler<'_> { let new_on_ground = p.on_ground; let new_look_direction = p.values.look_direction; - commands.entity(entity).queue(RelativeEntityUpdate::new( - world_holder.partial.clone(), - move |entity_mut| { - let is_local_entity = entity_mut.get::().is_some(); - let mut physics = entity_mut.get_mut::().unwrap(); + if !should_apply_entity_update( + &mut commands, + &mut world_holder.partial.write(), + entity, + entity_update_query, + ) { + return; + } + let is_local_entity = local_entity.is_some(); - physics.vec_delta_codec.set_base(new_position); + physics.vec_delta_codec.set_base(new_position); - if is_local_entity { - debug!("Ignoring entity position sync packet for local player"); - return; - } + if is_local_entity { + debug!("Ignoring entity position sync packet for local player"); + return; + } - physics.set_on_ground(new_on_ground); + physics.set_on_ground(new_on_ground); - let mut position = entity_mut.get_mut::().unwrap(); - **position = new_position; + if **position != new_position { + **position = new_position; + } - let mut look_direction = entity_mut.get_mut::().unwrap(); - *look_direction = new_look_direction; - }, - )); + if *look_direction != new_look_direction { + *look_direction = new_look_direction; + } }, ); } diff --git a/azalea-client/src/plugins/packet/mod.rs b/azalea-client/src/plugins/packet/mod.rs index 63a94ee0..4309850e 100644 --- a/azalea-client/src/plugins/packet/mod.rs +++ b/azalea-client/src/plugins/packet/mod.rs @@ -11,6 +11,7 @@ use crate::chat::ChatReceivedEvent; pub mod config; pub mod game; pub mod login; +pub mod relative_updates; pub struct PacketPlugin; @@ -30,23 +31,27 @@ pub fn death_event_on_0_health( impl Plugin for PacketPlugin { fn build(&self, app: &mut App) { - app.add_observer(game::handle_outgoing_packets_observer) - .add_observer(config::handle_outgoing_packets_observer) - .add_observer(login::handle_outgoing_packets_observer) - .add_systems(Update, death_event_on_0_health) - .add_message::() - .add_message::() - .add_message::() - // - .add_message::() - .add_message::() - .add_message::() - .add_message::() - .add_message::() - .add_message::() - .add_message::() - .add_message::() - .add_message::(); + app.add_systems( + Update, + relative_updates::debug_detect_updates_received_on_local_entities, + ) + .add_observer(game::handle_outgoing_packets_observer) + .add_observer(config::handle_outgoing_packets_observer) + .add_observer(login::handle_outgoing_packets_observer) + .add_systems(Update, death_event_on_0_health) + .add_message::() + .add_message::() + .add_message::() + // + .add_message::() + .add_message::() + .add_message::() + .add_message::() + .add_message::() + .add_message::() + .add_message::() + .add_message::() + .add_message::(); } } @@ -70,12 +75,21 @@ macro_rules! __declare_packet_handlers { pub(crate) use __declare_packet_handlers as declare_packet_handlers; +#[derive(Resource)] +struct CachedSystemState(SystemState); + pub(crate) fn as_system(ecs: &mut World, f: impl FnOnce(T::Item<'_, '_>)) where T: SystemParam + 'static, { - let mut system_state = SystemState::::new(ecs); + // creating a new SystemState is expensive, so we save them as a Resource in the + // ecs + let mut system_state = match ecs.remove_resource::>() { + Some(s) => s.0, + None => SystemState::::new(ecs), + }; let values = system_state.get_mut(ecs); f(values); system_state.apply(ecs); + ecs.insert_resource(CachedSystemState(system_state)); } diff --git a/azalea-client/src/plugins/packet/relative_updates.rs b/azalea-client/src/plugins/packet/relative_updates.rs new file mode 100644 index 00000000..2f7112b8 --- /dev/null +++ b/azalea-client/src/plugins/packet/relative_updates.rs @@ -0,0 +1,163 @@ +// How entity updates are processed (to avoid issues with shared worlds) +// - each bot contains a map of { entity id: updates received } +// - the shared world also contains a canonical "true" updates received for each +// entity +// - when a client loads an entity, its "updates received" is set to the same as +// the global "updates received" +// - when the shared world sees an entity for the first time, the "updates +// received" is set to 1. +// - clients can force the shared "updates received" to 0 to make it so certain +// entities (i.e. other bots in our swarm) don't get confused and updated by +// other bots +// - when a client gets an update to an entity, we check if our "updates +// received" is the same as the shared world's "updates received": if it is, +// then process the update and increment the client's and shared world's +// "updates received" if not, then we simply increment our local "updates +// received" and do nothing else + +use std::sync::Arc; + +use azalea_core::entity_id::MinecraftEntityId; +use azalea_entity::LocalEntity; +use azalea_world::PartialWorld; +use bevy_ecs::prelude::*; +use derive_more::{Deref, DerefMut}; +use parking_lot::RwLock; +use tracing::warn; + +use crate::packet::as_system; + +/// An [`EntityCommand`] that applies a "relative update" to an entity, which +/// means this update won't be run multiple times by different clients in the +/// same world. +/// +/// This is used to avoid a bug where when there's multiple clients in the same +/// world and an entity sends a relative move packet to all clients, its +/// position gets desynced since the relative move is applied multiple times. +/// +/// Don't use this unless you actually got an entity update packet that all +/// other clients within render distance will get too. You usually don't need +/// this when the change isn't relative either. +pub struct RelativeEntityUpdate { + pub partial_world: Arc>, + // a function that takes the entity and updates it + pub update: Box, +} +impl RelativeEntityUpdate { + pub fn new( + partial_world: Arc>, + update: impl FnOnce(&mut EntityWorldMut) + Send + Sync + 'static, + ) -> Self { + Self { + partial_world, + update: Box::new(update), + } + } +} + +/// A component that counts the number of times this entity has been modified. +/// +/// This is used for making sure two clients don't do the same relative update +/// on an entity. +/// +/// If an entity is local (i.e. it's a client/LocalEntity), this component +/// should NOT be present in the entity. +#[derive(Component, Debug, Deref, DerefMut)] +pub struct UpdatesReceived(u32); + +pub type EntityUpdateQuery<'world, 'state, 'a> = Query< + 'world, + 'state, + ( + &'a MinecraftEntityId, + Option<&'a UpdatesReceived>, + Option<&'a LocalEntity>, + ), +>; + +/// See [`RelativeEntityUpdate`] for details. +/// +/// Calling this function will have the same effect as using the Command, but +/// it's more performant than the Command. +pub fn should_apply_entity_update( + commands: &mut Commands, + partial_world: &mut PartialWorld, + entity: Entity, + entity_update_query: EntityUpdateQuery, +) -> bool { + let partial_entity_infos = &mut partial_world.entity_infos; + + if Some(entity) == partial_entity_infos.owner_entity { + // if the entity owns this partial world, it's always allowed to update itself + return true; + }; + + let Ok((minecraft_entity_id, updates_received, local_entity)) = entity_update_query.get(entity) + else { + warn!("called should_apply_entity_update on an entity with missing components"); + return false; + }; + + if local_entity.is_some() { + // a client tried to update another client, which isn't allowed + return false; + } + + let this_client_updates_received = partial_entity_infos + .updates_received + .get(&minecraft_entity_id) + .copied(); + + let can_update = if let Some(updates_received) = updates_received { + this_client_updates_received.unwrap_or(1) == **updates_received + } else { + // no UpdatesReceived means the entity was just spawned + true + }; + if can_update { + let new_updates_received = this_client_updates_received.unwrap_or(0) + 1; + partial_entity_infos + .updates_received + .insert(*minecraft_entity_id, new_updates_received); + + commands + .entity(entity) + .insert(UpdatesReceived(new_updates_received)); + + return true; + } + false +} + +impl EntityCommand for RelativeEntityUpdate { + fn apply(self, mut entity_mut: EntityWorldMut) { + let partial_world = self.partial_world.clone(); + let mut should_update = false; + let entity = entity_mut.id(); + + entity_mut.world_scope(|ecs| { + as_system::<(Commands, EntityUpdateQuery)>(ecs, |(mut commands, query)| { + should_update = should_apply_entity_update( + &mut commands, + &mut partial_world.write(), + entity, + query, + ); + }); + }); + + if should_update { + (self.update)(&mut entity_mut); + } + } +} + +/// A system that logs a warning if an entity has both [`UpdatesReceived`] +/// and [`LocalEntity`]. +pub fn debug_detect_updates_received_on_local_entities( + query: Query, With)>, +) { + for entity in &query { + warn!("Entity {entity:?} has both LocalEntity and UpdatesReceived"); + } +} diff --git a/azalea-entity/src/lib.rs b/azalea-entity/src/lib.rs index fa4feffb..aacc3f54 100644 --- a/azalea-entity/src/lib.rs +++ b/azalea-entity/src/lib.rs @@ -424,6 +424,10 @@ pub struct PlayerAbilities { /// The type of fluid that is at an entity's eye position, while also accounting /// for fluid height. +/// +/// This is only updated for [`AbstractLiving`] entities. +/// +/// [`AbstractLiving`]: metadata::AbstractLiving #[cfg_attr(feature = "bevy_ecs", derive(bevy_ecs::component::Component))] #[derive(Clone, Copy, Debug, Deref, DerefMut, PartialEq)] pub struct FluidOnEyes(FluidKind); diff --git a/azalea-entity/src/plugin/components.rs b/azalea-entity/src/plugin/components.rs index 4698a808..eaeeb0d0 100644 --- a/azalea-entity/src/plugin/components.rs +++ b/azalea-entity/src/plugin/components.rs @@ -94,8 +94,8 @@ pub struct OnClimbable(bool); /// A component that indicates whether the player is currently sneaking. /// -/// If the entity isn't a local player, then this is just a shortcut for -/// checking if the [`Pose`] is `Crouching`. +/// If the entity is a player but isn't a local player, then this is just a +/// shortcut for checking if the [`Pose`] is `Crouching`. /// /// If you need to modify this value, use /// `azalea_client::PhysicsState::trying_to_crouch` or `Client::set_crouching` diff --git a/azalea-entity/src/plugin/mod.rs b/azalea-entity/src/plugin/mod.rs index e40c24f2..b86c6b7d 100644 --- a/azalea-entity/src/plugin/mod.rs +++ b/azalea-entity/src/plugin/mod.rs @@ -1,6 +1,5 @@ mod components; pub mod indexing; -mod relative_updates; use std::collections::HashSet; @@ -17,13 +16,12 @@ use bevy_ecs::prelude::*; pub use components::*; use derive_more::{Deref, DerefMut}; use indexing::EntityUuidIndex; -pub use relative_updates::RelativeEntityUpdate; use tracing::debug; use crate::{ FluidOnEyes, LookDirection, Physics, Pose, Position, dimensions::{EntityDimensions, calculate_dimensions}, - metadata::Health, + metadata::{self, Health, Player}, }; /// A Bevy [`SystemSet`] for various types of entity updates. @@ -57,7 +55,6 @@ impl Plugin for EntityPlugin { .chain() .in_set(EntityUpdateSystems::Index), ( - relative_updates::debug_detect_updates_received_on_local_entities, debug_new_entity, add_dead, clamp_look_direction, @@ -97,27 +94,32 @@ pub fn add_dead(mut commands: Commands, query: Query<(Entity, &Health), Changed< } pub fn update_fluid_on_eyes( - mut query: Query<(&mut FluidOnEyes, &Position, &EntityDimensions, &WorldName)>, + mut query: Query< + (&mut FluidOnEyes, &Position, &EntityDimensions, &WorldName), + With, + >, worlds: Res, ) { - for (mut fluid_on_eyes, position, dimensions, world_name) in query.iter_mut() { - let Some(world) = worlds.get(world_name) else { - continue; - }; - - let adjusted_eye_y = position.y + (dimensions.eye_height as f64) - 0.1111111119389534; - let eye_block_pos = BlockPos::from(position.with_y(adjusted_eye_y)); - let fluid_at_eye = world - .read() - .get_fluid_state(eye_block_pos) - .unwrap_or_default(); - let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64; - if fluid_cutoff_y > adjusted_eye_y { - **fluid_on_eyes = fluid_at_eye.kind; - } else { - **fluid_on_eyes = FluidKind::Empty; - } - } + query + .par_iter_mut() + .for_each(|(mut fluid_on_eyes, position, dimensions, world_name)| { + let Some(world) = worlds.get(world_name) else { + return; + }; + + let adjusted_eye_y = position.y + (dimensions.eye_height as f64) - 0.1111111119389534; + let eye_block_pos = BlockPos::from(position.with_y(adjusted_eye_y)); + let fluid_at_eye = world + .read() + .get_fluid_state(eye_block_pos) + .unwrap_or_default(); + let fluid_cutoff_y = (eye_block_pos.y as f32 + fluid_at_eye.height()) as f64; + if fluid_cutoff_y > adjusted_eye_y { + **fluid_on_eyes = fluid_at_eye.kind; + } else { + **fluid_on_eyes = FluidKind::Empty; + } + }); } pub fn update_on_climbable( @@ -229,7 +231,9 @@ pub fn update_dimensions( } } -pub fn update_crouching(query: Query<(&mut Crouching, &Pose), Without>) { +pub fn update_crouching( + query: Query<(&mut Crouching, &Pose), (Without, With)>, +) { for (mut crouching, pose) in query { let new_crouching = *pose == Pose::Crouching; // avoid triggering change detection diff --git a/azalea-entity/src/plugin/relative_updates.rs b/azalea-entity/src/plugin/relative_updates.rs deleted file mode 100644 index 53eb4c95..00000000 --- a/azalea-entity/src/plugin/relative_updates.rs +++ /dev/null @@ -1,115 +0,0 @@ -// How entity updates are processed (to avoid issues with shared worlds) -// - each bot contains a map of { entity id: updates received } -// - the shared world also contains a canonical "true" updates received for each -// entity -// - when a client loads an entity, its "updates received" is set to the same as -// the global "updates received" -// - when the shared world sees an entity for the first time, the "updates -// received" is set to 1. -// - clients can force the shared "updates received" to 0 to make it so certain -// entities (i.e. other bots in our swarm) don't get confused and updated by -// other bots -// - when a client gets an update to an entity, we check if our "updates -// received" is the same as the shared world's "updates received": if it is, -// then process the update and increment the client's and shared world's -// "updates received" if not, then we simply increment our local "updates -// received" and do nothing else - -use std::sync::Arc; - -use azalea_core::entity_id::MinecraftEntityId; -use azalea_world::PartialWorld; -use bevy_ecs::prelude::*; -use derive_more::{Deref, DerefMut}; -use parking_lot::RwLock; -use tracing::warn; - -use crate::LocalEntity; - -/// An [`EntityCommand`] that applies a "relative update" to an entity, which -/// means this update won't be run multiple times by different clients in the -/// same world. -/// -/// This is used to avoid a bug where when there's multiple clients in the same -/// world and an entity sends a relative move packet to all clients, its -/// position gets desynced since the relative move is applied multiple times. -/// -/// Don't use this unless you actually got an entity update packet that all -/// other clients within render distance will get too. You usually don't need -/// this when the change isn't relative either. -pub struct RelativeEntityUpdate { - pub partial_world: Arc>, - // a function that takes the entity and updates it - pub update: Box, -} -impl RelativeEntityUpdate { - pub fn new( - partial_world: Arc>, - update: impl FnOnce(&mut EntityWorldMut) + Send + Sync + 'static, - ) -> Self { - Self { - partial_world, - update: Box::new(update), - } - } -} - -/// A component that counts the number of times this entity has been modified. -/// -/// This is used for making sure two clients don't do the same relative update -/// on an entity. -/// -/// If an entity is local (i.e. it's a client/LocalEntity), this component -/// should NOT be present in the entity. -#[derive(Component, Debug, Deref, DerefMut)] -pub struct UpdatesReceived(u32); - -impl EntityCommand for RelativeEntityUpdate { - fn apply(self, mut entity: EntityWorldMut) { - let partial_entity_infos = &mut self.partial_world.write().entity_infos; - - if Some(entity.id()) == partial_entity_infos.owner_entity { - // if the entity owns this partial world, it's always allowed to update itself - (self.update)(&mut entity); - return; - }; - - let entity_id = *entity.get::().unwrap(); - if entity.contains::() { - // a client tried to update another client, which isn't allowed - return; - } - - let this_client_updates_received = partial_entity_infos - .updates_received - .get(&entity_id) - .copied(); - - let can_update = if let Some(updates_received) = entity.get::() { - this_client_updates_received.unwrap_or(1) == **updates_received - } else { - // no UpdatesReceived means the entity was just spawned - true - }; - if can_update { - let new_updates_received = this_client_updates_received.unwrap_or(0) + 1; - partial_entity_infos - .updates_received - .insert(entity_id, new_updates_received); - - entity.insert(UpdatesReceived(new_updates_received)); - - (self.update)(&mut entity); - } - } -} - -/// A system that logs a warning if an entity has both [`UpdatesReceived`] -/// and [`LocalEntity`]. -pub fn debug_detect_updates_received_on_local_entities( - query: Query, With)>, -) { - for entity in &query { - warn!("Entity {entity:?} has both LocalEntity and UpdatesReceived"); - } -} diff --git a/azalea/examples/testbot/main.rs b/azalea/examples/testbot/main.rs index c74dcee7..63d83c8a 100644 --- a/azalea/examples/testbot/main.rs +++ b/azalea/examples/testbot/main.rs @@ -24,6 +24,7 @@ mod commands; pub mod killaura; +pub mod mspt; use std::{env, process, sync::Arc, thread, time::Duration}; @@ -75,6 +76,7 @@ async fn main() -> AppExit { args, commands: Arc::new(commands), }) + // .add_plugins(mspt::MsptPlugin) .start(join_address) .await } diff --git a/azalea/examples/testbot/mspt.rs b/azalea/examples/testbot/mspt.rs new file mode 100644 index 00000000..b4931d71 --- /dev/null +++ b/azalea/examples/testbot/mspt.rs @@ -0,0 +1,69 @@ +//! A debugging plugin that logs the duration of ECS `Update`s every tick. + +use std::time::{Duration, Instant}; + +use azalea::prelude::Resource; +use bevy_app::Plugin; +use bevy_ecs::{schedule::IntoScheduleConfigs, system::ResMut}; + +pub struct MsptPlugin; +impl Plugin for MsptPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.insert_resource(MsptData { + last_log_time: Instant::now(), + this_update_start_time: Instant::now(), + update_times: Vec::new(), + }) + .add_systems(bevy_app::PreUpdate, on_update_start) + .add_systems( + bevy_app::PostUpdate, + (on_update_end, log_mspt_stats).chain(), + ); + } +} + +#[derive(Resource)] +struct MsptData { + this_update_start_time: Instant, + last_log_time: Instant, + + update_times: Vec, +} +fn log_mspt_stats(mut stats: ResMut) { + if stats.last_log_time.elapsed() < Duration::from_secs(1) { + return; + } + stats.last_log_time = Instant::now(); + + let mut fastest_update_duration = None; + let mut summed_update_durations = Duration::ZERO; + let mut num_updates = 0; + for update in stats.update_times.drain(..) { + summed_update_durations += update; + num_updates += 1; + let Some(fastest_update) = &mut fastest_update_duration else { + fastest_update_duration = Some(update); + continue; + }; + if update < *fastest_update { + *fastest_update = update; + } + } + + if num_updates > 0 + && let Some(fastest_update_duration) = fastest_update_duration + { + let avg_update_duration = summed_update_durations / num_updates; + + println!(); + println!("Average update duration: {avg_update_duration:?}"); + println!("Fastest update duration: {fastest_update_duration:?}"); + } +} +fn on_update_start(mut data: ResMut) { + data.this_update_start_time = Instant::now(); +} +fn on_update_end(mut data: ResMut) { + let elapsed = data.this_update_start_time.elapsed(); + data.update_times.push(elapsed); +} -- cgit v1.2.3