diff options
38 files changed, 297 insertions, 88 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml index 21dfb83f..beb23be0 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,6 +3,8 @@ rustflags = ["--cfg", "docsrs_dep"] rustdocflags = [ "--html-after-content", "docs-rs/trait-tags.html", + "--html-after-content", + "docs-rs/arborium-header.html", "--cfg", "docsrs_dep", ] @@ -237,7 +237,6 @@ dependencies = [ name = "azalea" version = "0.15.0+mc1.21.11" dependencies = [ - "anyhow", "azalea-auth", "azalea-block", "azalea-brigadier", @@ -257,6 +256,7 @@ dependencies = [ "bevy_tasks", "criterion", "derive_more", + "eyre", "futures", "futures-lite", "indexmap", @@ -362,7 +362,6 @@ dependencies = [ name = "azalea-client" version = "0.15.0+mc1.21.11" dependencies = [ - "anyhow", "async-compat", "azalea-auth", "azalea-block", @@ -384,6 +383,7 @@ dependencies = [ "bevy_utils", "chrono", "derive_more", + "eyre", "indexmap", "minecraft_folder_path", "parking_lot", @@ -520,7 +520,6 @@ dependencies = [ name = "azalea-protocol" version = "0.15.0+mc1.21.11" dependencies = [ - "anyhow", "azalea-auth", "azalea-block", "azalea-brigadier", @@ -534,6 +533,8 @@ dependencies = [ "azalea-registry", "azalea-world", "bevy_ecs", + "criterion", + "eyre", "flate2", "futures", "futures-lite", @@ -1555,6 +1556,16 @@ dependencies = [ ] [[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2103,6 +2114,12 @@ dependencies = [ ] [[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] name = "indexmap" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -31,7 +31,6 @@ repository = "https://github.com/azalea-rs/azalea" [workspace.dependencies] aes = "0.8.4" -anyhow = "1.0.102" async-compat = "0.2.5" base64 = "0.22.1" bevy_app = "0.18.1" @@ -51,6 +50,7 @@ criterion = "0.8.2" derive_more = "2.1.1" enum-as-inner = "0.7.0" env_logger = "0.11.9" +eyre = "0.6.12" flate2 = { version = "1.1.9", features = ["zlib-rs"] } futures = "0.3.32" futures-lite = "2.6.1" @@ -123,6 +123,8 @@ rustdoc-args = [ "docsrs_dep", "--html-after-content", "docs-rs/trait-tags.html", + "--html-after-content", + "docs-rs/arborium-header.html", ] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] diff --git a/azalea-buf/src/impls/extra.rs b/azalea-buf/src/impls/extra.rs index f22ddb1a..8312e8fb 100644 --- a/azalea-buf/src/impls/extra.rs +++ b/azalea-buf/src/impls/extra.rs @@ -84,7 +84,7 @@ macro_rules! impl_for_list_type { } default fn azalea_write(&self, buf: &mut impl Write) -> io::Result<()> { (self.len() as u32).azalea_write_var(buf)?; - for item in self { + for item in self.iter() { T::azalea_write(item, buf)?; } Ok(()) @@ -101,7 +101,7 @@ macro_rules! impl_for_list_type { } fn azalea_write_var(&self, buf: &mut impl Write) -> io::Result<()> { (self.len() as u32).azalea_write_var(buf)?; - for item in self { + for item in self.iter() { T::azalea_write_var(item, buf)?; } Ok(()) @@ -132,6 +132,10 @@ macro_rules! impl_for_list_type { impl_for_list_type!(Vec<T>); impl_for_list_type!(Box<[T]>); +// `Arc<[T]>` is deliberately not implemented here, because converting a +// `Vec<T>` to `Arc<[T]>` results in allocations (since `Arc` stores the +// counters on the heap) and we'd like to avoid that. for us it's typically +// better to do `Arc<Box<[T]>>`. impl AzBuf for Vec<u8> { fn azalea_read(buf: &mut Cursor<&[u8]>) -> Result<Self, BufReadError> { diff --git a/azalea-client/Cargo.toml b/azalea-client/Cargo.toml index d260eca0..ef16dbbf 100644 --- a/azalea-client/Cargo.toml +++ b/azalea-client/Cargo.toml @@ -42,7 +42,7 @@ uuid.workspace = true bevy_utils = { workspace = true, features = ["debug"] } [dev-dependencies] -anyhow.workspace = true +eyre.workspace = true [features] default = ["log", "packet-event", "online-mode"] diff --git a/azalea-client/src/plugins/packet/config/mod.rs b/azalea-client/src/plugins/packet/config/mod.rs index f59b8c23..5d0532d5 100644 --- a/azalea-client/src/plugins/packet/config/mod.rs +++ b/azalea-client/src/plugins/packet/config/mod.rs @@ -128,8 +128,8 @@ impl ConfigPacketHandler<'_> { self.player ); - as_system::<(Commands, MessageWriter<_>)>(self.ecs, |(mut commands, mut events)| { - events.write(KeepAliveEvent { + as_system::<Commands>(self.ecs, |mut commands| { + commands.trigger(KeepAliveEvent { entity: self.player, id: p.id, }); diff --git a/azalea-client/src/plugins/packet/game/events.rs b/azalea-client/src/plugins/packet/game/events.rs index bc070ec8..02be26f3 100644 --- a/azalea-client/src/plugins/packet/game/events.rs +++ b/azalea-client/src/plugins/packet/game/events.rs @@ -113,7 +113,7 @@ pub struct DeathEvent { /// A KeepAlive packet is sent from the server to verify that the client is /// still connected. -#[derive(Clone, Debug, Message)] +#[derive(Clone, Debug, EntityEvent)] pub struct KeepAliveEvent { pub entity: Entity, /// The ID of the keepalive. diff --git a/azalea-client/src/plugins/packet/game/mod.rs b/azalea-client/src/plugins/packet/game/mod.rs index 81eac46b..e7e709e8 100644 --- a/azalea-client/src/plugins/packet/game/mod.rs +++ b/azalea-client/src/plugins/packet/game/mod.rs @@ -925,19 +925,16 @@ impl GamePacketHandler<'_> { pub fn keep_alive(&mut self, p: &ClientboundKeepAlive) { debug!("Got keep alive packet {p:?} for {:?}", self.player); - as_system::<(MessageWriter<KeepAliveEvent>, Commands)>( - self.ecs, - |(mut keepalive_events, mut commands)| { - keepalive_events.write(KeepAliveEvent { - entity: self.player, - id: p.id, - }); - commands.trigger(SendGamePacketEvent::new( - self.player, - ServerboundKeepAlive { id: p.id }, - )); - }, - ); + as_system::<Commands>(self.ecs, |mut commands| { + commands.trigger(KeepAliveEvent { + entity: self.player, + id: p.id, + }); + commands.trigger(SendGamePacketEvent::new( + self.player, + ServerboundKeepAlive { id: p.id }, + )); + }); } pub fn remove_entities(&mut self, p: &ClientboundRemoveEntities) { @@ -1674,11 +1671,14 @@ fn move_entity( let Some(entity) = entity else { // often triggered by hypixel :( - debug!("Got move move entity packet for unknown entity id {entity_id}"); + debug!("Got move entity packet for unknown entity id {entity_id}"); return; }; - let (mut physics, mut position, mut look_direction) = entity_query.get_mut(entity).unwrap(); + let Ok((mut physics, mut position, mut look_direction)) = entity_query.get_mut(entity) else { + debug!("Got move entity packet for entity with missing components {entity_id}"); + return; + }; if !should_apply_entity_update( &mut commands, diff --git a/azalea-client/src/plugins/packet/mod.rs b/azalea-client/src/plugins/packet/mod.rs index e758ae5f..d7b0d9df 100644 --- a/azalea-client/src/plugins/packet/mod.rs +++ b/azalea-client/src/plugins/packet/mod.rs @@ -48,7 +48,6 @@ impl Plugin for PacketPlugin { .add_message::<game::UpdatePlayerEvent>() .add_message::<ChatReceivedEvent>() .add_message::<game::DeathEvent>() - .add_message::<game::KeepAliveEvent>() .add_message::<game::ResourcePackEvent>() .add_message::<game::WorldLoadedEvent>() .add_message::<login::ReceiveCustomQueryEvent>(); diff --git a/azalea-protocol/Cargo.toml b/azalea-protocol/Cargo.toml index 193e153e..3ae65416 100644 --- a/azalea-protocol/Cargo.toml +++ b/azalea-protocol/Cargo.toml @@ -7,9 +7,10 @@ license.workspace = true repository.workspace = true [dev-dependencies] -anyhow.workspace = true +eyre.workspace = true tracing.workspace = true tracing-subscriber.workspace = true +criterion.workspace = true [dependencies] azalea-auth.workspace = true @@ -56,3 +57,7 @@ bevy_ecs = [ [lints] workspace = true + +[[bench]] +name = "read" +harness = false diff --git a/azalea-protocol/benches/read.rs b/azalea-protocol/benches/read.rs new file mode 100644 index 00000000..ad309bbc --- /dev/null +++ b/azalea-protocol/benches/read.rs @@ -0,0 +1,37 @@ +use std::{hint::black_box, io::Cursor}; + +use azalea_buf::AzBuf; +use azalea_core::position::Vec3i; +use azalea_protocol::packets::game::{ + ClientboundWaypoint, + c_waypoint::{ + TrackedWaypoint, WaypointData, WaypointIcon, WaypointIdentifier, WaypointOperation, + }, +}; +use criterion::{Criterion, criterion_group, criterion_main}; +use uuid::Uuid; + +fn benchmark(c: &mut Criterion) { + c.bench_function("c_waypoint", |b| { + let mut buf = Vec::new(); + ClientboundWaypoint { + operation: WaypointOperation::Update, + waypoint: TrackedWaypoint { + identifier: WaypointIdentifier::Uuid(Uuid::nil()), + icon: WaypointIcon { + style: "minecraft:default".into(), + color: None, + }, + data: WaypointData::Vec3i(Vec3i { x: 1, y: 67, z: 0 }), + }, + } + .azalea_write(&mut buf) + .unwrap(); + b.iter(|| { + black_box(ClientboundWaypoint::azalea_read(&mut Cursor::new(&buf)).unwrap()); + }); + }); +} + +criterion_group!(benches, benchmark); +criterion_main!(benches); diff --git a/azalea-protocol/examples/handshake_proxy.rs b/azalea-protocol/examples/handshake_proxy.rs index cfe4af52..0d04d526 100644 --- a/azalea-protocol/examples/handshake_proxy.rs +++ b/azalea-protocol/examples/handshake_proxy.rs @@ -49,7 +49,7 @@ const PROXY_PLAYERS: Players = Players { const PROXY_SECURE_CHAT: Option<bool> = Some(false); #[tokio::main] -async fn main() -> anyhow::Result<()> { +async fn main() -> eyre::Result<()> { tracing_subscriber::fmt().with_max_level(Level::INFO).init(); // Bind to an address and port @@ -64,7 +64,7 @@ async fn main() -> anyhow::Result<()> { } } -async fn handle_connection(stream: TcpStream) -> anyhow::Result<()> { +async fn handle_connection(stream: TcpStream) -> eyre::Result<()> { stream.set_nodelay(true)?; let ip = stream.peer_addr()?; let mut conn: Connection<ServerboundHandshakePacket, ClientboundHandshakePacket> = diff --git a/azalea-protocol/src/packets/game/c_level_chunk_with_light.rs b/azalea-protocol/src/packets/game/c_level_chunk_with_light.rs index 00489513..83fadd0b 100644 --- a/azalea-protocol/src/packets/game/c_level_chunk_with_light.rs +++ b/azalea-protocol/src/packets/game/c_level_chunk_with_light.rs @@ -22,8 +22,8 @@ pub struct ClientboundLevelChunkPacketData { pub heightmaps: Vec<(HeightmapKind, Box<[u64]>)>, /// The raw chunk sections. /// - /// We can't parse the data in azalea-protocol because it depends on context - /// from other packets + /// We can't parse the data in `azalea-protocol` because sometimes we want + /// to skip parsing this. /// /// This is an Arc because it's often very big and we want it to be cheap to /// clone. diff --git a/azalea-protocol/src/packets/game/c_light_update.rs b/azalea-protocol/src/packets/game/c_light_update.rs index 83dbda34..1a7027ca 100644 --- a/azalea-protocol/src/packets/game/c_light_update.rs +++ b/azalea-protocol/src/packets/game/c_light_update.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use azalea_buf::AzBuf; use azalea_core::bitset::BitSet; use azalea_protocol_macros::ClientboundGamePacket; @@ -17,6 +19,6 @@ pub struct ClientboundLightUpdatePacketData { pub block_y_mask: BitSet, pub empty_sky_y_mask: BitSet, pub empty_block_y_mask: BitSet, - pub sky_updates: Vec<Vec<u8>>, - pub block_updates: Vec<Vec<u8>>, + pub sky_updates: Arc<Box<[Box<[u8]>]>>, + pub block_updates: Arc<Box<[Box<[u8]>]>>, } diff --git a/azalea-registry/src/identifier.rs b/azalea-registry/src/identifier.rs index c7266bf4..912d7b32 100644 --- a/azalea-registry/src/identifier.rs +++ b/azalea-registry/src/identifier.rs @@ -26,6 +26,8 @@ pub struct Identifier { inner: Box<str>, } +const _: () = assert!(size_of::<Identifier>() == 24); + static DEFAULT_NAMESPACE: &str = "minecraft"; // static REALMS_NAMESPACE: &str = "realms"; diff --git a/azalea-world/src/chunk_storage.rs b/azalea-world/src/chunk_storage.rs index d92252af..282c7d51 100644 --- a/azalea-world/src/chunk_storage.rs +++ b/azalea-world/src/chunk_storage.rs @@ -373,7 +373,7 @@ impl Chunk { let mut heightmaps = HashMap::new(); for (kind, data) in heightmaps_data { - let data: Box<[u64]> = data.clone(); + let data = data.clone(); let heightmap = Heightmap::new(*kind, dimension_height, min_y, data); heightmaps.insert(*kind, heightmap); } diff --git a/azalea/Cargo.toml b/azalea/Cargo.toml index e4dd4d1c..595018e0 100644 --- a/azalea/Cargo.toml +++ b/azalea/Cargo.toml @@ -46,9 +46,9 @@ uuid.workspace = true [dev-dependencies] criterion.workspace = true +eyre.workspace = true parking_lot = { workspace = true, features = ["deadlock_detection"] } rand.workspace = true -anyhow.workspace = true bevy_log.workspace = true [features] diff --git a/azalea/README.md b/azalea/README.md index eb066839..158a9b31 100644 --- a/azalea/README.md +++ b/azalea/README.md @@ -34,7 +34,7 @@ pub struct State { pub messages_received: Arc<Mutex<usize>> } -async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { +async fn handle(bot: Client, event: Event, state: State) -> eyre::Result<()> { match event { Event::Chat(m) => { let mut messages_received = state.messages_received.lock(); @@ -67,8 +67,7 @@ For faster compile times, create a `.cargo/config.toml` file in your project and [this file](https://github.com/azalea-rs/azalea/blob/main/.cargo/config_fast_builds.toml) into it. You may have to install the LLD linker. -For faster performance in debug mode, add the following code to your -Cargo.toml: +For faster performance in debug mode, add the following code to your `Cargo.toml`: ```toml [profile.dev] @@ -77,6 +76,8 @@ opt-level = 1 opt-level = 3 ``` +For a lot more details on how to make Azalea faster, see the [Azalea performance guide](_docs::performance). + # Documentation The documentation for the latest Azalea crates.io release is available at [docs.rs/azalea](https://docs.rs/azalea/latest/azalea/) and the docs for the latest bleeding-edge (git) version are at [azalea.matdoes.dev](https://azalea.matdoes.dev/azalea/). diff --git a/azalea/examples/echo.rs b/azalea/examples/echo.rs index 40ceb90a..a12b9618 100644 --- a/azalea/examples/echo.rs +++ b/azalea/examples/echo.rs @@ -16,7 +16,7 @@ async fn main() -> AppExit { #[derive(Clone, Component, Default)] pub struct State {} -async fn handle(bot: Client, event: Event, _state: State) -> anyhow::Result<()> { +async fn handle(bot: Client, event: Event, _state: State) -> eyre::Result<()> { if let Event::Chat(m) = event && let (Some(sender), content) = m.split_sender_and_content() { diff --git a/azalea/examples/steal.rs b/azalea/examples/steal.rs index b5a26423..d5c0b913 100644 --- a/azalea/examples/steal.rs +++ b/azalea/examples/steal.rs @@ -24,7 +24,7 @@ struct State { pub checked_chests: Arc<Mutex<Vec<BlockPos>>>, } -async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { +async fn handle(bot: Client, event: Event, state: State) -> eyre::Result<()> { if let Event::Chat(m) = event { if m.sender() == Some(bot.username()) { return Ok(()); @@ -39,7 +39,7 @@ async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { Ok(()) } -async fn steal(bot: Client, state: State) -> anyhow::Result<()> { +async fn steal(bot: Client, state: State) -> eyre::Result<()> { { let mut is_stealing = state.is_stealing.lock(); if *is_stealing { diff --git a/azalea/examples/testbot/killaura.rs b/azalea/examples/testbot/killaura.rs index 4f29a0f2..136ae7dd 100644 --- a/azalea/examples/testbot/killaura.rs +++ b/azalea/examples/testbot/killaura.rs @@ -6,7 +6,7 @@ use azalea::{ use crate::State; -pub fn tick(bot: Client, state: State) -> anyhow::Result<()> { +pub fn tick(bot: Client, state: State) -> eyre::Result<()> { if !state.killaura { return Ok(()); } diff --git a/azalea/examples/testbot/main.rs b/azalea/examples/testbot/main.rs index f5f6c096..57bdcc72 100644 --- a/azalea/examples/testbot/main.rs +++ b/azalea/examples/testbot/main.rs @@ -73,8 +73,8 @@ async fn main() -> AppExit { builder .join_delay(Duration::from_millis(100)) .set_swarm_state(SwarmState { - args, - commands: Arc::new(commands), + args: args.into(), + commands: commands.into(), }) // .add_plugins(mspt::MsptPlugin) .start(join_address) @@ -128,11 +128,11 @@ impl State { #[derive(Clone, Default, Resource)] struct SwarmState { - pub args: Args, + pub args: Arc<Args>, pub commands: Arc<CommandDispatcher<Mutex<CommandSource>>>, } -async fn handle(bot: Client, event: azalea::Event, state: State) -> anyhow::Result<()> { +async fn handle(bot: Client, event: azalea::Event, state: State) -> eyre::Result<()> { let swarm = bot.resource::<SwarmState>(); match event { @@ -201,7 +201,7 @@ async fn handle(bot: Client, event: azalea::Event, state: State) -> anyhow::Resu Ok(()) } -async fn swarm_handle(_swarm: Swarm, event: SwarmEvent, _state: SwarmState) -> anyhow::Result<()> { +async fn swarm_handle(_swarm: Swarm, event: SwarmEvent, _state: SwarmState) -> eyre::Result<()> { match &event { SwarmEvent::Disconnect(account, _join_opts) => { println!("bot got kicked! {}", account.username()); diff --git a/azalea/examples/todo/craft_dig_straight_down.rs b/azalea/examples/todo/craft_dig_straight_down.rs index d8254aa3..3568b555 100644 --- a/azalea/examples/todo/craft_dig_straight_down.rs +++ b/azalea/examples/todo/craft_dig_straight_down.rs @@ -19,7 +19,7 @@ async fn main() -> AppExit { .await } -async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { +async fn handle(bot: Client, event: Event, state: State) -> eyre::Result<()> { match event { Event::Chat(m) => { if m.sender() == Some(bot.username()) { diff --git a/azalea/examples/todo/mine_a_chunk.rs b/azalea/examples/todo/mine_a_chunk.rs index 17fff5df..8ba33f5c 100644 --- a/azalea/examples/todo/mine_a_chunk.rs +++ b/azalea/examples/todo/mine_a_chunk.rs @@ -24,11 +24,11 @@ struct State {} #[derive(Clone, Default, Resource)] struct SwarmState {} -async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { +async fn handle(bot: Client, event: Event, state: State) -> eyre::Result<()> { Ok(()) } -async fn swarm_handle(swarm: Swarm, event: SwarmEvent, state: SwarmState) -> anyhow::Result<()> { +async fn swarm_handle(swarm: Swarm, event: SwarmEvent, state: SwarmState) -> eyre::Result<()> { match &event { SwarmEvent::Login => { swarm.goto(azalea::BlockPos::new(0, 70, 0)).await; diff --git a/azalea/examples/todo/pvp.rs b/azalea/examples/todo/pvp.rs index 8da55f3a..2b82fdfc 100644 --- a/azalea/examples/todo/pvp.rs +++ b/azalea/examples/todo/pvp.rs @@ -30,10 +30,10 @@ struct State {} #[derive(Clone, Default, Resource)] struct SwarmState {} -async fn handle(bot: Client, event: Event, state: State) -> anyhow::Result<()> { +async fn handle(bot: Client, event: Event, state: State) -> eyre::Result<()> { Ok(()) } -async fn swarm_handle(swarm: Swarm, event: SwarmEvent, state: SwarmState) -> anyhow::Result<()> { +async fn swarm_handle(swarm: Swarm, event: SwarmEvent, state: SwarmState) -> eyre::Result<()> { match event { SwarmEvent::Tick => { if let Some(target_entity) = diff --git a/azalea/src/_docs/mod.rs b/azalea/src/_docs/mod.rs new file mode 100644 index 00000000..170be079 --- /dev/null +++ b/azalea/src/_docs/mod.rs @@ -0,0 +1,5 @@ +//! Some extra documentation for Azalea users. + +pub mod performance { + #![doc = include_str!("./performance.md")] +} diff --git a/azalea/src/_docs/performance.md b/azalea/src/_docs/performance.md new file mode 100644 index 00000000..30483f08 --- /dev/null +++ b/azalea/src/_docs/performance.md @@ -0,0 +1,120 @@ +# Azalea performance guide + +Azalea is designed to have a reasonable trade-off between performance and ergonomics. +In some cases, performance is left on the table in exchange for simpler or more flexible interfaces. +This guide is for those who want to learn more about Azalea's performance characteristics, or for those who find that Azalea's default performance does not meet their needs. + +Typically, one Azalea bot with 8 chunk render distance (the default) will idle on about 20mb-40mb of memory on a normal world. +If you're using a swarm, the world information (chunks, entities, registries) is shared between bots. +This means that each new bot in a swarm should be relatively cheap -- usually about 1mb extra per bot -- though it may be more if they're spread apart. + +As for CPU usage, Azalea is meant to be able to run on weak servers, but it may struggle at large swarm sizes, especially if you're using the pathfinder. +By default, swarms should be able to handle up to a few hundred bots, depending on your hardware. +Azalea uses threads whenever it's beneficial, so performance should also scale with your number of CPU cores. + +<!-- note: we use higher headers here because otherwise they wouldn't all show up in the sidebar --> + +# Improved performance in debug mode + +This one is already mentioned in the `azalea` crate documentation, but is repeated here for completeness. + +As you likely know, Rust programs perform significantly better when run in release mode (like, `cargo run --release`), but this comes at the cost of slower compile times. +To have nearly the same performance as release mode while having similar incremental compile times as debug mode, you can ask Rust to compile only your dependencies as if they were in release mode. +To do this, add the following lines to your `Cargo.toml`: + +```toml +[profile.dev] +opt-level = 1 +[profile.dev.package."*"] +opt-level = 3 +``` + +For maximum performance, it is still recommended to compile your bot with release mode when you don't need a short feedback loop. + +# General optimizations + +## Compilation options + +An easy win is to enable LTO (not thin LTO) by putting the following line in your `Cargo.toml`: +```toml +[profile.release] +lto = true +``` +In certain cases (i.e. pathfinding) this can make Azalea about 20% faster, at the cost of substantially slower compile times. + +There are a few other options you can try setting, like `RUSTFLAGS="-C target-cpu=native"` and running with `panic = "abort"`, but these usually won't have a significant impact. + +If you're willing to go through the trouble, PGO is also usually another 10% win. +[`cargo-pgo`](https://github.com/Kobzol/cargo-pgo) streamlines the process of creating PGO builds. + +## Using a different allocator + +Your operating system's allocator is probably good, but it's not always optimal. +Using [mimalloc v3](https://docs.rs/mimalloc/latest/mimalloc/) or [snmalloc](https://docs.rs/snmalloc-rs/latest/snmalloc_rs/) is almost always another easy performance win. +Note that using these may increase your total memory usage. + +# Some advice + +If you're not profiling and benchmarking, then you're going in blind. +This means that you may inadvertently make your code slower, and you may waste time and miss potentially big optimization opportunities. + +Benchmarking just means having a way to measure speedups. +For instance, this can be a simple timer or an averaged CPU usage measurement. + +Profiling is to help you identify the slow parts of your code. +The recommended tool for this is [`cargo-flamegraph`](https://github.com/flamegraph-rs/flamegraph), just make sure to enable `force-frame-pointers` and `debuginfo`. + +Some specific suggestions will not be mentioned in this guide because they would become obvious from profiling, or because they're already mentioned in the relevant Azalea documentation. + +I would also recommend reading Nethercote's [Rust Performance Book](https://nnethercote.github.io/perf-book/title-page.html) for more performance tips. + +# Azalea-specific optimizations + +## Update Azalea + +Azalea (and its dependencies) are often updated with new performance improvements. +If you're on an old version of Azalea, then your bot may become slightly faster after updating or by switching to the unstable Git version. + +## Disabling packet events + +Azalea clones received packets to emit an `Event::Packet` for every packet that every client receives, but this can be wasteful if you're not actually using that event. +To avoid this cost, disable default features for Azalea and then enable the default ones except for `packet-event`. +This can be done with the following command: +```sh +cargo add azalea --no-default-features --features=log,online-mode,serde +``` + +If you want to disable `packet-event` but still have a way to watch for packets, you can do this by making a plugin and watching for ECS events such as `ReceiveGamePacketEvent`. + +## Lower your render distance + +If the client doesn't need a high view distance, then you can reduce the number of stored chunks/entities and received packets by lowering it. +This can be done by having something like the following in your handler function: +```rust +azalea::Event::Init => bot.set_client_information(ClientInformation { + view_distance: 2, + ..Default::default() +}), +``` + +## Disabling plugins + +This can be somewhat risky and isn't technically officially supported, but disabling default Azalea plugins like `PhysicsPlugin` can help with performance if you don't need them. +Look at the source code for `DefaultPlugins` and `DefaultBotPlugins` to find the full list of plugins that can be disabled. + +If you're insane enough, forking Azalea and commenting out code within packet handlers or other places may also be worthwhile. + +## Write your bot as a plugin + +It's rare for user code to be a bottleneck, but if you do happen to be interacting with the bot a lot, then implementing those parts of your code as a Bevy plugin might be a good idea. +Doing this can help avoid unnecessary clones and locks, and it'll allow for your code to run in parallel with Azalea's internal systems. + +Note that while it's somewhat possible to access a `Client` from within a plugin (which also requires creating a new thread or Tokio task), you should avoid this as it will negate most of the performance benefits that come from using a plugin. +Looking at the source code for how things are implemented in `Client` is a good idea, though. + +## Don't use Azalea + +At a certain point, it may be worth considering if you even need a bot library. +If Azalea's event loop is still too slow, and if your bot could be implemented by manually reading and writing packets, then perhaps you should think about using `azalea-protocol` directly. +There are some examples for how you can go about doing this in the documentation for that crate. +Hopefully, though, Azalea's performance will be good enough and you won't need to do this. diff --git a/azalea/src/builder.rs b/azalea/src/builder.rs index 28557b17..cc3b384a 100644 --- a/azalea/src/builder.rs +++ b/azalea/src/builder.rs @@ -25,7 +25,7 @@ use crate::{ /// # } /// # #[derive(Clone, Component, Default)] /// # pub struct State; -/// # async fn handle(mut bot: Client, event: Event, state: State) -> anyhow::Result<()> { +/// # async fn handle(mut bot: Client, event: Event, state: State) -> eyre::Result<()> { /// # Ok(()) /// # } /// ``` @@ -73,7 +73,7 @@ impl ClientBuilder<NoState, ()> { /// # client_builder.set_handler(handle); /// # #[derive(Clone, Component, Default)] /// # pub struct State; - /// # async fn handle(mut bot: Client, event: Event, state: State) -> anyhow::Result<()> { + /// # async fn handle(mut bot: Client, event: Event, state: State) -> eyre::Result<()> { /// # Ok(()) /// # } /// ``` @@ -102,7 +102,7 @@ impl ClientBuilder<NoState, ()> { /// /// # #[derive(Clone, Component, Default)] /// # pub struct State; - /// async fn handle(mut bot: Client, event: Event, state: State) -> anyhow::Result<()> { + /// async fn handle(mut bot: Client, event: Event, state: State) -> eyre::Result<()> { /// Ok(()) /// } /// ``` diff --git a/azalea/src/client_impl/chat.rs b/azalea/src/client_impl/chat.rs index 1a7359f7..2490e08c 100644 --- a/azalea/src/client_impl/chat.rs +++ b/azalea/src/client_impl/chat.rs @@ -37,7 +37,7 @@ impl Client { /// /// ```rust,no_run /// # use azalea::Client; - /// # async fn example(bot: Client) -> anyhow::Result<()> { + /// # async fn example(bot: Client) -> eyre::Result<()> { /// bot.chat("Hello, world!"); /// # Ok(()) /// # } diff --git a/azalea/src/client_impl/mod.rs b/azalea/src/client_impl/mod.rs index 91f91c0c..ba188307 100644 --- a/azalea/src/client_impl/mod.rs +++ b/azalea/src/client_impl/mod.rs @@ -284,7 +284,7 @@ impl Client { /// .await; /// println!("done!"); /// } - /// async fn handle(bot: Client, event: Event, _state: NoState) -> anyhow::Result<()> { + /// async fn handle(bot: Client, event: Event, _state: NoState) -> eyreResult<()> { /// match event { /// Event::Disconnect(_) | Event::ConnectionFailed(_) => { /// bot.exit(); diff --git a/azalea/src/events.rs b/azalea/src/events.rs index 7d625456..4bef3165 100644 --- a/azalea/src/events.rs +++ b/azalea/src/events.rs @@ -158,7 +158,6 @@ impl Plugin for EventsPlugin { add_player_listener, update_player_listener, remove_player_listener, - keepalive_listener, death_listener.after(azalea_client::packet::death_event_on_0_health), disconnect_listener, connection_failed_listener.after(azalea_client::join::poll_create_connection_task), @@ -169,7 +168,8 @@ impl Plugin for EventsPlugin { PreUpdate, init_listener.before(super::connection::read_packets), ) - .add_systems(GameTick, tick_listener); + .add_systems(GameTick, tick_listener) + .add_observer(keepalive_listener); } } @@ -290,14 +290,9 @@ pub fn dead_component_listener(query: Query<&LocalPlayerEvents, Added<Dead>>) { } } -pub fn keepalive_listener( - query: Query<&LocalPlayerEvents>, - mut events: MessageReader<KeepAliveEvent>, -) { - for event in events.read() { - if let Ok(local_player_events) = query.get(event.entity) { - let _ = local_player_events.send(Event::KeepAlive(event.id)); - } +pub fn keepalive_listener(keep_alive: On<KeepAliveEvent>, query: Query<&LocalPlayerEvents>) { + if let Ok(local_player_events) = query.get(keep_alive.entity) { + let _ = local_player_events.send(Event::KeepAlive(keep_alive.id)); } } diff --git a/azalea/src/lib.rs b/azalea/src/lib.rs index 332dc565..1ce7e5e6 100644 --- a/azalea/src/lib.rs +++ b/azalea/src/lib.rs @@ -1,6 +1,8 @@ #![doc = include_str!("../README.md")] #![feature(type_changing_struct_update)] +#[cfg(doc)] +pub mod _docs; pub mod accept_resource_packs; pub mod auto_reconnect; pub mod auto_respawn; diff --git a/azalea/src/pathfinder/debug.rs b/azalea/src/pathfinder/debug.rs index 0a5f583d..0117479c 100644 --- a/azalea/src/pathfinder/debug.rs +++ b/azalea/src/pathfinder/debug.rs @@ -19,7 +19,7 @@ use crate::pathfinder::moves::should_mine_block_state; /// # #[derive(Clone, Component, Default)] /// # pub struct State; /// -/// async fn handle(mut bot: Client, event: azalea::Event, state: State) -> anyhow::Result<()> { +/// async fn handle(mut bot: Client, event: azalea::Event, state: State) -> eyre::Result<()> { /// match event { /// azalea::Event::Init => { /// bot.ecs diff --git a/azalea/src/pathfinder/execute/patching.rs b/azalea/src/pathfinder/execute/patching.rs index 855aa73d..e1ca2a81 100644 --- a/azalea/src/pathfinder/execute/patching.rs +++ b/azalea/src/pathfinder/execute/patching.rs @@ -1,4 +1,10 @@ -use std::{cmp, collections::VecDeque, ops::RangeInclusive, sync::Arc}; +use std::{ + cmp, + collections::VecDeque, + ops::RangeInclusive, + sync::Arc, + time::{Duration, Instant}, +}; use azalea_core::position::BlockPos; use azalea_entity::inventory::Inventory; @@ -34,11 +40,9 @@ pub fn check_for_path_obstruction( )>, worlds: Res<Worlds>, ) { - for (entity, mut pathfinder, mut executing_path, world_name, inventory, custom_state) in - &mut query - { + query.par_iter_mut().for_each(|(entity, mut pathfinder, mut executing_path, world_name, inventory, custom_state)| { let Some(opts) = pathfinder.opts.clone() else { - continue; + return; }; let world_lock = worlds @@ -65,13 +69,17 @@ pub fn check_for_path_obstruction( ) }; + // don't bother spending more than 10ms per tick on this + let timeout = Duration::from_millis(10); + let Some(obstructed_index) = check_path_obstructed( origin, RelBlockPos::from_origin(origin, executing_path.last_reached_node), &executing_path.path, successors, + timeout ) else { - continue; + return; }; drop(custom_state_ref); @@ -89,12 +97,12 @@ pub fn check_for_path_obstruction( ); executing_path.path.truncate(obstructed_index); executing_path.is_path_partial = true; - continue; + return; } let Some(opts) = pathfinder.opts.clone() else { error!("got PatchExecutingPathEvent but the bot has no pathfinder opts"); - continue; + return; }; let world_lock = worlds @@ -114,7 +122,7 @@ pub fn check_for_path_obstruction( custom_state.clone(), opts, ); - } + }); } /// Update the given [`ExecutingPath`] to recalculate the path of the nodes in @@ -222,11 +230,18 @@ pub fn check_path_obstructed<SuccessorsFn>( mut current_position: RelBlockPos, path: &VecDeque<astar::Edge<BlockPos, moves::MoveData>>, successors_fn: SuccessorsFn, + timeout: Duration, ) -> Option<usize> where SuccessorsFn: Fn(RelBlockPos) -> Vec<astar::Edge<RelBlockPos, moves::MoveData>>, { + let start_time = Instant::now(); + for (i, edge) in path.iter().enumerate() { + if start_time.elapsed() > timeout { + break; + } + let movement_target = RelBlockPos::from_origin(origin, edge.movement.target); let mut found_edge = None; diff --git a/azalea/src/swarm/builder.rs b/azalea/src/swarm/builder.rs index 853324fa..42654206 100644 --- a/azalea/src/swarm/builder.rs +++ b/azalea/src/swarm/builder.rs @@ -105,10 +105,10 @@ impl SwarmBuilder<NoState, NoSwarmState, (), ()> { /// # swarm_builder.set_handler(handle).set_swarm_handler(swarm_handle); /// # #[derive(Clone, Component, Default, Resource)] /// # pub struct State; - /// # async fn handle(mut bot: Client, event: Event, state: State) -> anyhow::Result<()> { + /// # async fn handle(mut bot: Client, event: Event, state: State) -> eyre::Result<()> { /// # Ok(()) /// # } - /// # async fn swarm_handle(swarm: Swarm, event: SwarmEvent, state: State) -> anyhow::Result<()> { + /// # async fn swarm_handle(swarm: Swarm, event: SwarmEvent, state: State) -> eyre::Result<()> { /// # Ok(()) /// # } /// ``` @@ -157,7 +157,7 @@ where /// /// #[derive(Clone, Component, Default)] /// struct State {} - /// async fn handle(mut bot: Client, event: Event, state: State) -> anyhow::Result<()> { + /// async fn handle(mut bot: Client, event: Event, state: State) -> eyre::Result<()> { /// Ok(()) /// } /// @@ -167,7 +167,7 @@ where /// # mut swarm: Swarm, /// # event: SwarmEvent, /// # state: SwarmState, - /// # ) -> anyhow::Result<()> { + /// # ) -> eyre::Result<()> { /// # Ok(()) /// # } /// ``` @@ -214,7 +214,7 @@ where /// # #[derive(Clone, Component, Default)] /// # struct State {} /// - /// # async fn handle(mut bot: Client, event: Event, state: State) -> anyhow::Result<()> { + /// # async fn handle(mut bot: Client, event: Event, state: State) -> eyre::Result<()> { /// # Ok(()) /// # } /// @@ -224,7 +224,7 @@ where /// mut swarm: Swarm, /// event: SwarmEvent, /// state: SwarmState, - /// ) -> anyhow::Result<()> { + /// ) -> eyre::Result<()> { /// Ok(()) /// } /// ``` diff --git a/azalea/src/swarm/mod.rs b/azalea/src/swarm/mod.rs index de4d2ebe..616733f8 100644 --- a/azalea/src/swarm/mod.rs +++ b/azalea/src/swarm/mod.rs @@ -117,7 +117,7 @@ pub type BoxSwarmHandleFn<SS, R> = /// .await /// } /// -/// async fn handle(bot: Client, event: Event, _state: State) -> anyhow::Result<()> { +/// async fn handle(bot: Client, event: Event, _state: State) -> eyre::Result<()> { /// match &event { /// _ => {} /// } @@ -128,7 +128,7 @@ pub type BoxSwarmHandleFn<SS, R> = /// mut swarm: Swarm, /// event: SwarmEvent, /// _state: SwarmState, -/// ) -> anyhow::Result<()> { +/// ) -> eyre::Result<()> { /// match &event { /// SwarmEvent::Chat(m) => { /// println!("{}", m.message().to_ansi()); diff --git a/docs-rs/README.md b/docs-rs/README.md index 3be1087a..46a3f559 100644 --- a/docs-rs/README.md +++ b/docs-rs/README.md @@ -3,14 +3,14 @@ This directory includes some templates and styling to extend and modify [rustdoc]'s output for Azalea's documentation on [docs.rs]. -See [Bevy's documentation](https://github.com/bevyengine/bevy/tree/main/docs-rs) for more info. +See [Bevy's documentation](https://github.com/bevyengine/bevy/tree/main/docs-rs) for more info on `trait-tags`. ## Local Testing -Build the documentation with the extension enabled like this: +Building the documentation with the extensions enabled can be done like this: ```bash -RUSTDOCFLAGS="--html-after-content docs-rs/trait-tags.html --cfg docsrs_dep" RUSTFLAGS="--cfg docsrs_dep" cargo doc --no-deps --package <package_name> +RUSTDOCFLAGS="--html-after-content docs-rs/trait-tags.html --html-after-content docs-rs/arborium-header.html --cfg docsrs_dep" RUSTFLAGS="--cfg docsrs_dep" cargo doc --no-deps --package <package_name> ``` [rustdoc]: https://doc.rust-lang.org/rustdoc/what-is-rustdoc.html diff --git a/docs-rs/arborium-header.html b/docs-rs/arborium-header.html new file mode 100644 index 00000000..f0772b20 --- /dev/null +++ b/docs-rs/arborium-header.html @@ -0,0 +1 @@ +<script src="https://cdn.jsdelivr.net/npm/@arborium/arborium/dist/arborium.iife.js"></script>
\ No newline at end of file |
