From da192c31a80efb665b615492b58c32d38cc2ee34 Mon Sep 17 00:00:00 2001 From: Andrew Gazelka Date: Sun, 24 Mar 2024 18:34:34 -0500 Subject: [PATCH] ci: bench --- .github/workflows/bench.yml | 103 ++++++ Cargo.lock | 258 +++++++++++++- Cargo.toml | 8 +- Dockerfile | 31 +- docker-compose.yml | 10 + server/Cargo.toml | 8 + server/benches/many_zombies.rs | 55 +++ server/src/bounding_box.rs | 2 +- server/src/lib.rs | 317 ++++++++++++++++++ server/src/main.rs | 307 +---------------- server/src/system/entity_detect_collisions.rs | 2 +- server/src/system/entity_move_logic.rs | 4 +- server/src/system/init_entity.rs | 1 + server/src/system/kill_all.rs | 4 +- server/src/system/tps_message.rs | 7 +- 15 files changed, 780 insertions(+), 337 deletions(-) create mode 100644 .github/workflows/bench.yml create mode 100644 server/benches/many_zombies.rs create mode 100644 server/src/lib.rs diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml new file mode 100644 index 00000000..be56c7d8 --- /dev/null +++ b/.github/workflows/bench.yml @@ -0,0 +1,103 @@ +name: Build + +on: + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + runBenchmark: + name: run benchmark + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + lfs: true + + - uses: boa-dev/criterion-compare-action@v3 + +#jobs: +# machete: +# runs-on: ubuntu-latest +# +# steps: +# - name: Checkout repository +# uses: actions/checkout@v4 +# +# - name: Install cargo-machete +# uses: baptiste0928/cargo-install@v3 +# with: +# crate: cargo-machete +# +# - name: Run cargo machete +# run: | +# cargo machete +# +# test: +# runs-on: ubuntu-latest +# +# steps: +# - name: Checkout repository +# uses: actions/checkout@v4 +# with: +# lfs: true +# +# - name: Setup Rust toolchain and cache +# uses: actions-rust-lang/setup-rust-toolchain@v1 +# +# - name: Run cargo test +# run: cargo test --workspace --all-features +# +# deny: +# runs-on: ubuntu-latest +# +# steps: +# - name: Checkout repository +# uses: actions/checkout@v4 +# +# - name: Install cargo-deny +# uses: baptiste0928/cargo-install@v3 +# with: +# crate: cargo-deny +# +# - name: Run cargo deny +# run: | +# cargo deny check +# +# fmt: +# runs-on: ubuntu-latest +# +# steps: +# - name: Checkout repository +# uses: actions/checkout@v4 +# +# - name: Setup Rust toolchain and cache +# uses: actions-rust-lang/setup-rust-toolchain@v1 +# with: +# toolchain: nightly +# components: rustfmt +# +# - name: Run rustfmt +# run: | +# cargo +nightly fmt --all -- --check +# clippy: +# runs-on: ubuntu-latest +# +# steps: +# - name: Checkout repository +# uses: actions/checkout@v4 +# with: +# lfs: true +# +# - name: Setup Rust toolchain and cache +# uses: actions-rust-lang/setup-rust-toolchain@v1 +# with: +# toolchain: nightly +# components: clippy +# +# - name: Clippy check +# run: cargo clippy --workspace --tests --examples --all-features +# +# diff --git a/Cargo.lock b/Cargo.lock index 2aa12783..694b2036 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,15 @@ dependencies = [ "zerocopy 0.7.32", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.16" @@ -62,6 +71,18 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + [[package]] name = "anyhow" version = "1.0.81" @@ -354,6 +375,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.0.90" @@ -399,6 +426,33 @@ dependencies = [ "valence_registry", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.4.4" @@ -409,6 +463,31 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + [[package]] name = "concurrent-queue" version = "2.4.0" @@ -448,6 +527,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -473,6 +588,12 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -720,7 +841,7 @@ version = "0.1.0" dependencies = [ "anyhow", "heck", - "itertools", + "itertools 0.12.1", "prettyplease", "proc-macro2", "quote", @@ -764,6 +885,16 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bebe52f2c58f1eea4d6fd3307058cf818c74181435a3620502d1651db07ff018" +[[package]] +name = "half" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -871,6 +1002,26 @@ dependencies = [ "libc", ] +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -1106,6 +1257,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + [[package]] name = "overload" version = "0.1.1" @@ -1175,6 +1332,34 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "plotters" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" + +[[package]] +name = "plotters-svg" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +dependencies = [ + "plotters-backend", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1306,6 +1491,35 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1324,6 +1538,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1369,15 +1592,17 @@ dependencies = [ "base64 0.22.0", "bytes", "chunk", + "criterion", "evenio", "flume", "fnv", "generator", - "itertools", + "itertools 0.12.1", "mimalloc", "monoio", "rand 0.8.5", "rand_distr", + "rayon", "serde_json", "sha2", "signal-hook", @@ -1553,6 +1778,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1859,6 +2094,16 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1957,6 +2202,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index fe3738e2..09a0354d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,12 @@ opt-level = 3 [profile.release] lto = "fat" codegen-units = 1 -strip = "debuginfo" -panic = "abort" +#panic = "abort" + +[profile.bench] +lto = "fat" +codegen-units = 1 +#panic = "abort" [workspace.dependencies] chunk = { path = "chunk" } diff --git a/Dockerfile b/Dockerfile index d0658d25..ad264d06 100644 --- a/Dockerfile +++ b/Dockerfile @@ -67,28 +67,17 @@ RUN --mount=type=cache,target=/app/target \ cp target/debug/server /build/server && \ cp target/cargo-timings/cargo-timing.html /build/cargo-timing.html -#FROM scratch -#FROM alpine:3.19 +FROM rust as cli -# debian -#FROM rust -# -#RUN apt-get update && apt-get install -y linux-perf -# -#RUN cargo install flamegraph -# -## Copy the built executable into the final image -#COPY --from=builder /build/server / -# -#EXPOSE 25565 -# -# -## ENTRYPOINT ["flamegraph", "-o", "/app/profiling/flamegraph.svg", "--", "./server"] -# -## entrypoint is bash -##ENTRYPOINT ["bash"] -# -#ENTRYPOINT ["./server"] +RUN apt-get update && apt-get install -y linux-perf + +RUN cargo install flamegraph + +COPY --from=release /build/server / + +EXPOSE 25565 + +ENTRYPOINT ["bash"] FROM scratch as debug-bin COPY --from=debug /build/server / diff --git a/docker-compose.yml b/docker-compose.yml index de7d8905..48e45ec5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,3 +27,13 @@ services: # source: ./profiling # target: /app/profiling + cli: + build: + context: . + target: cli + environment: + - RUST_LOG=${RUST_LOG:-info} + volumes: + - type: bind + source: ./profiling + target: /app/profiling diff --git a/server/Cargo.toml b/server/Cargo.toml index 9ee8b4ae..ab143495 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -37,6 +37,7 @@ uuid = "1.8.0" rand_distr = "0.4.3" smallvec = { git = "https://github.com/servo/rust-smallvec" } fnv = "1.0.7" +rayon = "1.10.0" #pprof = { version = "0.13.0", features = ["flamegraph"] } @@ -112,3 +113,10 @@ style = "deny" suspicious = { level = "deny", priority = -1 } blanket_clippy_restriction_lints = "allow" + +[dev-dependencies] +criterion = { version = "0.5.1", features = ["html_reports"] } + +[[bench]] +name = "many_zombies" +harness = false \ No newline at end of file diff --git a/server/benches/many_zombies.rs b/server/benches/many_zombies.rs new file mode 100644 index 00000000..e91b9931 --- /dev/null +++ b/server/benches/many_zombies.rs @@ -0,0 +1,55 @@ +use std::time::Duration; + +use criterion::{criterion_group, criterion_main, Criterion}; +use server::{bounding_box::BoundingBox, FullEntityPose, Game, InitEntity}; +use valence_protocol::math::DVec3; + +fn criterion_benchmark(c: &mut Criterion) { + // so we can have reliable benchmarks even when we are using our laptop for other + // things + rayon::ThreadPoolBuilder::new() + .num_threads(4) + .build_global() + .unwrap(); + + let mut game = Game::init().unwrap(); + + let count = 100_000; + + const BASE_RADIUS: f64 = 4.0; + + // normalize over the number + let radius = BASE_RADIUS * (count as f64).sqrt(); + + let loc = DVec3::new(0.0, 10.0, 0.0); + + for _ in 0..count { + // spawn in 100 block radius + let x = (rand::random::() - 0.5).mul_add(radius, loc.x); + let y = loc.y; + let z = (rand::random::() - 0.5).mul_add(radius, loc.z); + + game.world_mut().send(InitEntity { + pose: FullEntityPose { + position: DVec3::new(x, y, z), + yaw: 0.0, + pitch: 0.0, + bounding: BoundingBox::create(DVec3::new(x, y, z), 0.6, 1.8), + }, + }); + } + + // just a tick to setup + game.tick(); + + c.bench_function("world", |b| b.iter(|| game.tick())); +} + +criterion_group! { + name = benches; + config = Criterion::default().measurement_time(Duration::from_secs(20)); + targets = criterion_benchmark +} + +// criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/server/src/bounding_box.rs b/server/src/bounding_box.rs index 34e7d930..98c964d1 100644 --- a/server/src/bounding_box.rs +++ b/server/src/bounding_box.rs @@ -127,7 +127,7 @@ impl EntityBoundingBoxes { self.query.clear(); } - pub fn get_collisions( + #[must_use] pub fn get_collisions( &self, current: CollisionContext, fetcher: &Fetcher<(EntityId, &FullEntityPose, &EntityReaction)>, diff --git a/server/src/lib.rs b/server/src/lib.rs new file mode 100644 index 00000000..6345984c --- /dev/null +++ b/server/src/lib.rs @@ -0,0 +1,317 @@ +#![allow(clippy::many_single_char_names)] + +extern crate core; +mod chunk; + +#[global_allocator] +static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; + +use std::{ + cell::UnsafeCell, + collections::VecDeque, + sync::atomic::AtomicU32, + time::{Duration, Instant}, +}; + +use anyhow::Context; +use evenio::prelude::*; +use signal_hook::iterator::Signals; +use tracing::{info, warn}; +use valence_protocol::math::DVec3; + +use crate::{ + bounding_box::BoundingBox, + handshake::{server, ClientConnection, Packets}, +}; + +mod global; +mod handshake; + +mod packets; +mod system; + +mod bits; + +mod quad_tree; + +pub mod bounding_box; + +// A zero-sized component, often called a "marker" or "tag". +#[derive(Component)] +struct Player { + packets: Packets, + name: Box, + last_keep_alive_sent: Instant, + locale: Option, +} + +#[derive(Event)] +struct InitPlayer { + entity: EntityId, + io: Packets, + name: Box, + pos: FullEntityPose, +} + +#[derive(Component, Copy, Clone)] +struct Uuid(uuid::Uuid); + +#[derive(Event)] +pub struct InitEntity { + pub pose: FullEntityPose, +} + +#[derive(Event)] +struct PlayerJoinWorld { + #[event(target)] + target: EntityId, +} + +#[derive(Component, Debug)] +pub struct MinecraftEntity; + +#[derive(Component, Debug, Copy, Clone)] +pub struct RunningSpeed(f64); + +impl Default for RunningSpeed { + fn default() -> Self { + Self(0.1) + } +} + +#[derive(Event)] +struct KickPlayer { + #[event(target)] // Works on tuple struct fields as well. + target: EntityId, + reason: String, +} + +#[derive(Event)] +struct KillAllEntities; + +#[derive(Event)] +struct TpsEvent { + ms_per_tick: f64, +} + +#[derive(Event)] +struct Gametick; + +static GLOBAL: global::Global = global::Global { + player_count: AtomicU32::new(0), +}; + +pub struct Game { + world: World, + last_ticks: VecDeque, + last_ms_per_tick: VecDeque, + + incoming: flume::Receiver, +} + +impl Game { + pub const fn world(&self) -> &World { + &self.world + } + + pub fn world_mut(&mut self) -> &mut World { + &mut self.world + } + + pub fn init() -> anyhow::Result { + info!("Starting mc-server"); + + let current_threads = evenio::rayon::current_num_threads(); + let max_threads = evenio::rayon::max_num_threads(); + + info!("rayon\tcurrent threads: {current_threads}, max threads: {max_threads}"); + + let mut signals = Signals::new([signal_hook::consts::SIGINT, signal_hook::consts::SIGTERM]) + .context("failed to create signal handler")?; + + let (shutdown_tx, shutdown_rx) = flume::bounded(1); + + std::thread::spawn(move || { + for _ in signals.forever() { + warn!("Shutting down..."); + SHUTDOWN.store(true, std::sync::atomic::Ordering::Relaxed); + let _ = shutdown_tx.send(()); + } + }); + + let server = server(shutdown_rx); + + let mut world = World::new(); + + world.add_handler(system::init_player); + world.add_handler(system::player_join_world); + world.add_handler(system::player_kick); + world.add_handler(system::entity_spawn); + world.add_handler(system::entity_move_logic); + world.add_handler(system::entity_detect_collisions); + world.add_handler(system::reset_bounding_boxes); + + world.add_handler(system::keep_alive); + world.add_handler(process_packets); + world.add_handler(system::tps_message); + world.add_handler(system::kill_all); + + let bounding_boxes = world.spawn(); + world.insert(bounding_boxes, bounding_box::EntityBoundingBoxes::default()); + + let mut game = Self { + world, + last_ticks: VecDeque::default(), + last_ms_per_tick: VecDeque::default(), + incoming: server, + }; + + game.last_ticks.push_back(Instant::now()); + + Ok(game) + } + + fn wait_duration(&self) -> Option { + let &first_tick = self.last_ticks.front()?; + + let count = self.last_ticks.len(); + + let time_for_20_tps = first_tick + Duration::from_secs_f64(count as f64 / 20.0); + + // aim for 20 ticks per second + let now = Instant::now(); + + if time_for_20_tps < now { + return None; + } + + let duration = time_for_20_tps - now; + + // this is a bit of a hack to be conservative when sleeping + Some(duration.mul_f64(0.8)) + } + + pub fn game_loop(&mut self) { + while !SHUTDOWN.load(std::sync::atomic::Ordering::Relaxed) { + self.tick(); + + if let Some(wait_duration) = self.wait_duration() { + std::thread::sleep(wait_duration); + } + } + } + + pub fn tick(&mut self) { + const HISTORY_SIZE: usize = 100; + + let now = Instant::now(); + self.last_ticks.push_back(now); + + // let mut tps = None; + if self.last_ticks.len() > HISTORY_SIZE { + self.last_ticks.pop_front(); + // let ticks_per_second = 100.0 / (now - front).as_secs_f64(); + // tps = Some(ticks_per_second); + } + + while let Ok(connection) = self.incoming.try_recv() { + let ClientConnection { packets, name } = connection; + + let player = self.world.spawn(); + + let event = InitPlayer { + entity: player, + io: packets, + name, + pos: FullEntityPose { + position: DVec3::new(0.0, 2.0, 0.0), + bounding: BoundingBox::create(DVec3::new(0.0, 2.0, 0.0), 0.6, 1.8), + yaw: 0.0, + pitch: 0.0, + }, + }; + + self.world.send(event); + } + + self.world.send(Gametick); + + let ms = now.elapsed().as_nanos() as f64 / 1_000_000.0; + self.last_ms_per_tick.push_back(ms); + + if self.last_ms_per_tick.len() > HISTORY_SIZE { + self.last_ms_per_tick.pop_front(); + + let ms_per_tick = + self.last_ms_per_tick.iter().sum::() / self.last_ms_per_tick.len() as f64; + + self.world.send(TpsEvent { ms_per_tick }); + } + + // info!("Tick took: {:02.8}ms", ms); + } +} + +// The `Receiver` parameter tells our handler to listen for the `Tick` event. +fn process_packets( + _: Receiver, + mut fetcher: Fetcher<(EntityId, &mut Player, &mut FullEntityPose)>, + mut sender: Sender<(KickPlayer, InitEntity, KillAllEntities)>, +) { + // todo: flume the best things to use here? also this really ust needs to be mpsc not mpmc + // let (tx, rx) = flume::unbounded(); + + fetcher.iter_mut().for_each(|(_id, player, position)| { + // info!("Processing packets for player: {:?}", id); + while let Ok(packet) = player.packets.reader.try_recv() { + // info!("Received packet: {:?}", packet); + if let Err(e) = packets::switch(packet, player, position, &mut sender) { + let reason = format!("error: {e}"); + + // todo: handle error + let _ = player.packets.writer.send_chat_message(&reason); + + warn!("invalid packet: {reason}"); + // let _ = tx.send(KickPlayer { target: id, reason }); + } + } + }); +} + +static SHUTDOWN: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + +#[derive(Component, Copy, Clone, Debug)] +pub struct FullEntityPose { + pub position: DVec3, + pub yaw: f32, + pub pitch: f32, + pub bounding: BoundingBox, +} + +impl FullEntityPose { + fn move_by(&mut self, vec: DVec3) { + self.position += vec; + self.bounding = self.bounding.move_by(vec); + } +} + +#[derive(Debug, Default)] +pub struct EntityReactionInner { + velocity: DVec3, +} + +#[derive(Component, Debug, Default)] +pub struct EntityReaction(UnsafeCell); + +impl EntityReaction { + #[allow(dead_code)] + fn get_mut(&mut self) -> &mut EntityReactionInner { + self.0.get_mut() + } +} + +#[allow(clippy::undocumented_unsafe_blocks)] +unsafe impl Send for EntityReaction {} + +#[allow(clippy::undocumented_unsafe_blocks)] +unsafe impl Sync for EntityReaction {} diff --git a/server/src/main.rs b/server/src/main.rs index 6022b216..ce8346f8 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,305 +1,8 @@ -#![allow(clippy::many_single_char_names)] +use server::Game; -extern crate core; -mod chunk; +fn main() -> anyhow::Result<()> { + let mut game = Game::init()?; + game.game_loop(); -#[global_allocator] -static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; - -use std::{ - cell::UnsafeCell, - collections::VecDeque, - sync::atomic::AtomicU32, - time::{Duration, Instant}, -}; - -use evenio::prelude::*; -use signal_hook::iterator::Signals; -use tracing::{info, warn}; -use valence_protocol::math::DVec3; - -use crate::{ - bounding_box::BoundingBox, - handshake::{server, ClientConnection, Packets}, -}; - -mod global; -mod handshake; - -mod packets; -mod system; - -mod bits; - -mod quad_tree; - -pub mod bounding_box; - -// A zero-sized component, often called a "marker" or "tag". -#[derive(Component)] -struct Player { - packets: Packets, - name: Box, - last_keep_alive_sent: Instant, - locale: Option, -} - -#[derive(Event)] -struct InitPlayer { - entity: EntityId, - io: Packets, - name: Box, - pos: FullEntityPose, -} - -#[derive(Component, Copy, Clone)] -struct Uuid(uuid::Uuid); - -#[derive(Event)] -struct InitEntity { - pose: FullEntityPose, -} - -#[derive(Event)] -struct PlayerJoinWorld { - #[event(target)] - target: EntityId, -} - -#[derive(Component, Debug)] -pub struct MinecraftEntity; - -#[derive(Component, Debug, Copy, Clone)] -pub struct RunningSpeed(f64); - -impl Default for RunningSpeed { - fn default() -> Self { - Self(0.1) - } -} - -#[derive(Event)] -struct KickPlayer { - #[event(target)] // Works on tuple struct fields as well. - target: EntityId, - reason: String, -} - -#[derive(Event)] -struct KillAllEntities; - -#[derive(Event)] -struct TpsEvent { - ms_per_tick: f64, -} - -#[derive(Event)] -struct Gametick; - -static GLOBAL: global::Global = global::Global { - player_count: AtomicU32::new(0), -}; - -struct Game { - world: World, - last_ticks: VecDeque, - last_ms_per_tick: VecDeque, - - incoming: flume::Receiver, -} - -impl Game { - fn wait_duration(&self) -> Option { - let &first_tick = self.last_ticks.front()?; - - let count = self.last_ticks.len(); - - let time_for_20_tps = first_tick + Duration::from_secs_f64(count as f64 / 20.0); - - // aim for 20 ticks per second - let now = Instant::now(); - - if time_for_20_tps < now { - return None; - } - - let duration = time_for_20_tps - now; - - // this is a bit of a hack to be conservative when sleeping - Some(duration.mul_f64(0.8)) - } - - fn tick(&mut self) { - const HISTORY_SIZE: usize = 100; - - let now = Instant::now(); - self.last_ticks.push_back(now); - - // let mut tps = None; - if self.last_ticks.len() > HISTORY_SIZE { - let _front = self.last_ticks.pop_front().unwrap(); - // let ticks_per_second = 100.0 / (now - front).as_secs_f64(); - // tps = Some(ticks_per_second); - } - - while let Ok(connection) = self.incoming.try_recv() { - let ClientConnection { packets, name } = connection; - - let player = self.world.spawn(); - - let event = InitPlayer { - entity: player, - io: packets, - name, - pos: FullEntityPose { - position: DVec3::new(0.0, 2.0, 0.0), - bounding: BoundingBox::create(DVec3::new(0.0, 2.0, 0.0), 0.6, 1.8), - yaw: 0.0, - pitch: 0.0, - }, - }; - - self.world.send(event); - } - - self.world.send(Gametick); - - let ms = now.elapsed().as_nanos() as f64 / 1_000_000.0; - self.last_ms_per_tick.push_back(ms); - - if self.last_ms_per_tick.len() > HISTORY_SIZE { - self.last_ms_per_tick.pop_front().unwrap(); - - let ms_per_tick = - self.last_ms_per_tick.iter().sum::() / self.last_ms_per_tick.len() as f64; - - self.world.send(TpsEvent { ms_per_tick }); - } - - // info!("Tick took: {:02.8}ms", ms); - } -} - -// The `Receiver` parameter tells our handler to listen for the `Tick` event. -fn process_packets( - _: Receiver, - mut fetcher: Fetcher<(EntityId, &mut Player, &mut FullEntityPose)>, - mut sender: Sender<(KickPlayer, InitEntity, KillAllEntities)>, -) { - // todo: flume the best things to use here? also this really ust needs to be mpsc not mpmc - // let (tx, rx) = flume::unbounded(); - - fetcher.iter_mut().for_each(|(_id, player, position)| { - // info!("Processing packets for player: {:?}", id); - while let Ok(packet) = player.packets.reader.try_recv() { - // info!("Received packet: {:?}", packet); - if let Err(e) = packets::switch(packet, player, position, &mut sender) { - let reason = format!("error: {e}"); - - player.packets.writer.send_chat_message(&reason).unwrap(); - - warn!("invalid packet: {reason}"); - // let _ = tx.send(KickPlayer { target: id, reason }); - } - } - }); -} - -static SHUTDOWN: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); - -fn main() { - tracing_subscriber::fmt::init(); - - info!("Starting mc-server"); - - let current_threads = evenio::rayon::current_num_threads(); - let max_threads = evenio::rayon::max_num_threads(); - - info!("rayon\tcurrent threads: {current_threads}, max threads: {max_threads}"); - - let mut signals = - Signals::new([signal_hook::consts::SIGINT, signal_hook::consts::SIGTERM]).unwrap(); - - let (shutdown_tx, shutdown_rx) = flume::bounded(1); - - std::thread::spawn(move || { - for _ in signals.forever() { - warn!("Shutting down..."); - SHUTDOWN.store(true, std::sync::atomic::Ordering::Relaxed); - let _ = shutdown_tx.send(()); - } - }); - - let server = server(shutdown_rx); - - let mut world = World::new(); - - world.add_handler(system::init_player); - world.add_handler(system::player_join_world); - world.add_handler(system::player_kick); - world.add_handler(system::entity_spawn); - world.add_handler(system::entity_move_logic); - world.add_handler(system::entity_detect_collisions); - world.add_handler(system::reset_bounding_boxes); - - world.add_handler(system::keep_alive); - world.add_handler(process_packets); - world.add_handler(system::tps_message); - world.add_handler(system::kill_all); - - let bounding_boxes = world.spawn(); - world.insert(bounding_boxes, bounding_box::EntityBoundingBoxes::default()); - - let mut game = Game { - world, - last_ticks: VecDeque::default(), - last_ms_per_tick: VecDeque::default(), - incoming: server, - }; - - game.last_ticks.push_back(Instant::now()); - - while !SHUTDOWN.load(std::sync::atomic::Ordering::Relaxed) { - game.tick(); - - if let Some(wait_duration) = game.wait_duration() { - std::thread::sleep(wait_duration); - } - } + Ok(()) } - -#[derive(Component, Copy, Clone, Debug)] -pub struct FullEntityPose { - pub position: DVec3, - pub yaw: f32, - pub pitch: f32, - pub bounding: BoundingBox, -} - -impl FullEntityPose { - fn move_by(&mut self, vec: DVec3) { - self.position += vec; - self.bounding = self.bounding.move_by(vec); - } -} - -#[derive(Debug, Default)] -pub struct EntityReactionInner { - velocity: DVec3, -} - -#[derive(Component, Debug, Default)] -pub struct EntityReaction(UnsafeCell); - -impl EntityReaction { - #[allow(dead_code)] - fn get_mut(&mut self) -> &mut EntityReactionInner { - self.0.get_mut() - } -} - -#[allow(clippy::undocumented_unsafe_blocks)] -unsafe impl Send for EntityReaction {} - -#[allow(clippy::undocumented_unsafe_blocks)] -unsafe impl Sync for EntityReaction {} diff --git a/server/src/system/entity_detect_collisions.rs b/server/src/system/entity_detect_collisions.rs index d1ab7978..a84f8477 100644 --- a/server/src/system/entity_detect_collisions.rs +++ b/server/src/system/entity_detect_collisions.rs @@ -10,6 +10,7 @@ use evenio::{ query::{Not, Query}, rayon::prelude::*, }; +use sha2::digest::generic_array::arr; use tracing::info; use valence_protocol::{math::DVec2, ByteAngle, VarInt}; @@ -36,7 +37,6 @@ pub fn call( let entity_bounding_boxes = entity_bounding_boxes.0; - // todo: make par iterator poses_fetcher.par_iter().for_each(|(id, pose, reaction)| { let context = CollisionContext { bounding: pose.bounding, diff --git a/server/src/system/entity_move_logic.rs b/server/src/system/entity_move_logic.rs index 47a7eada..45bf406c 100644 --- a/server/src/system/entity_move_logic.rs +++ b/server/src/system/entity_move_logic.rs @@ -1,3 +1,5 @@ +#![allow(unused)] + use evenio::{ entity::EntityId, event::Receiver, @@ -94,8 +96,6 @@ pub fn call( }; player.iter_mut().for_each(|(player, ..)| { - // todo: this is inefficient we want to serialize once - // todo: remove _ let _ = player.packets.writer.send_packet(&pos); let _ = player.packets.writer.send_packet(&look); }); diff --git a/server/src/system/init_entity.rs b/server/src/system/init_entity.rs index a518be30..4801a042 100644 --- a/server/src/system/init_entity.rs +++ b/server/src/system/init_entity.rs @@ -1,3 +1,4 @@ +#![allow(unused)] use evenio::{ event::{Insert, Receiver, Sender, Spawn}, fetch::Fetcher, diff --git a/server/src/system/kill_all.rs b/server/src/system/kill_all.rs index d5e66ce2..06d6745c 100644 --- a/server/src/system/kill_all.rs +++ b/server/src/system/kill_all.rs @@ -15,13 +15,13 @@ pub fn kill_all( let entity_ids = ids.iter().map(|id| VarInt(id.index().0 as i32)).collect(); let despawn_packet = valence_protocol::packets::play::EntitiesDestroyS2c { entity_ids }; - + players.par_iter_mut().for_each(|player| { // todo: handle error let _ = player.packets.writer.send_packet(&despawn_packet); }); - for &id in &ids { + for id in ids { s.send(Despawn(id)); } } diff --git a/server/src/system/tps_message.rs b/server/src/system/tps_message.rs index b45daae9..37fffacf 100644 --- a/server/src/system/tps_message.rs +++ b/server/src/system/tps_message.rs @@ -1,5 +1,4 @@ use evenio::prelude::*; -use evenio::rayon::prelude::*; use crate::{Player, TpsEvent}; @@ -8,12 +7,12 @@ pub fn call( mut players: Fetcher<&mut Player>, ) { let ms_per_tick = r.event.ms_per_tick; - + // with 4 zeroes // lead 2 zeroes let message = format!("MSPT: {ms_per_tick:07.4}"); - - players.par_iter_mut().for_each(|player| { + + players.iter_mut().for_each(|player| { // todo: handle error let _ = player.packets.writer.send_chat_message(&message); });