feat(shared)!: add server stats message and kind
This commit is contained in:
parent
8b031de071
commit
01f566e824
|
|
@ -7,4 +7,5 @@
|
||||||
pub mod generator;
|
pub mod generator;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
pub mod save;
|
pub mod save;
|
||||||
|
pub mod session;
|
||||||
pub mod world;
|
pub mod world;
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
//! Network protocol types and constants.
|
//! Network protocol types and constants.
|
||||||
//!
|
//!
|
||||||
//! The module is split by stream purpose: [`control`]-stream handshake and disconnect messages, and the [`chunk`]-sync request/delivery messages. Control-stream types are re-exported here so callers continue to refer to `shared::protocol::<Type>` regardless of the internal layout, while the chunk types stay namespaced under `shared::protocol::chunk` to keep the two protocols visually distinct.
|
//! The module is split by stream purpose: [`control`]-stream handshake and disconnect messages, and the [`chunk`]-sync request/delivery messages, and the periodic [`authority`]-stream state. Control-stream types are re-exported here so callers continue to refer to `shared::protocol::<Type>` regardless of the internal layout, while the chunk types stay namespaced under `shared::protocol::chunk` to keep the two protocols visually distinct.
|
||||||
|
|
||||||
|
pub mod authority;
|
||||||
pub mod chunk;
|
pub mod chunk;
|
||||||
mod control;
|
mod control;
|
||||||
|
|
||||||
|
|
|
||||||
45
crates/shared/src/protocol/authority.rs
Normal file
45
crates/shared/src/protocol/authority.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
//! Authority-stream messages: periodic state pushed from server to client.
|
||||||
|
//!
|
||||||
|
//! The authority stream ([`StreamLayout::authority`](super::StreamLayout::authority), id 2) carries state the server is the sole authority over and pushes without being asked. Diagnostics are the first such payload; simulation snapshots will join them on the same stream.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Messages carried on the authority stream (stream 2): periodic server-authoritative state.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum AuthorityMessage {
|
||||||
|
/// Periodic simulation diagnostics, emitted on a fixed wall-clock cadence rather than per tick.
|
||||||
|
ServerStats(ServerStats),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A snapshot of the server's simulation health, sent roughly once per second.
|
||||||
|
///
|
||||||
|
/// The measured figures exist because the nominal tick rate advertised in [`HandshakeAck::tick_rate_hint`](super::HandshakeAck::tick_rate_hint) is a constant: it states what the server intends to run at and can never reveal that it is falling behind. Everything here is observed.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
|
||||||
|
pub struct ServerStats {
|
||||||
|
/// Ticks actually completed in the reporting window, expressed per second. Below the nominal rate when the server is overrunning its budget.
|
||||||
|
pub measured_tps: f32,
|
||||||
|
/// Mean wall-clock duration of a tick body in the reporting window, in milliseconds, excluding the sleep that pads the tick out to its period.
|
||||||
|
pub mean_tick_ms: f32,
|
||||||
|
/// Longest tick body observed in the reporting window, in milliseconds. A mean within budget alongside a spiking maximum indicates intermittent stalls rather than sustained overload.
|
||||||
|
pub max_tick_ms: f32,
|
||||||
|
/// Share of the nominal tick period consumed by the mean tick body, in percent. Values at or above 100 mean the server no longer has headroom.
|
||||||
|
pub tick_budget_percent: f32,
|
||||||
|
/// Chunks resident in the server's world cache.
|
||||||
|
pub loaded_chunks: u32,
|
||||||
|
/// Chunk generation jobs outstanding in the server's worker pool.
|
||||||
|
pub chunks_in_flight: u32,
|
||||||
|
/// Clients with an established session.
|
||||||
|
pub connected_clients: u32,
|
||||||
|
/// Entities in the server's ECS world, players included.
|
||||||
|
pub entities: u32,
|
||||||
|
/// Players currently in the world.
|
||||||
|
pub players: u32,
|
||||||
|
/// Wall-clock time since the simulation loop started, in seconds.
|
||||||
|
pub uptime_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../tests/protocol_authority.rs"]
|
||||||
|
mod tests;
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Wire-protocol version. Incremented on any breaking change to the message layout below.
|
/// Wire-protocol version. Incremented on any breaking change to the message layout below.
|
||||||
pub const PROTOCOL_VERSION: u32 = 1;
|
pub const PROTOCOL_VERSION: u32 = 2;
|
||||||
|
|
||||||
/// Messages carried on the control stream (stream 0): handshake and disconnect.
|
/// Messages carried on the control stream (stream 0): handshake and disconnect.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
|
|
||||||
43
crates/shared/src/session.rs
Normal file
43
crates/shared/src/session.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
//! Session-level concepts describing the relationship between a client and the server it is playing against.
|
||||||
|
|
||||||
|
/// Which kind of server a client session is running against.
|
||||||
|
///
|
||||||
|
/// Deliberately not part of the wire protocol. The client already knows the answer without asking: it either spawned a server in-process or dialled a socket. A server-declared field would be redundant at best and spoofable at worst, so the value is constructed client-side from facts the client already holds.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ServerKind {
|
||||||
|
/// The server runs in this process, backing single-player.
|
||||||
|
Integrated,
|
||||||
|
/// The server is a separate process reached over the network.
|
||||||
|
Dedicated {
|
||||||
|
/// Whether the server's address is off this machine. Decided from the `SocketAddr` the client dialled, not from anything the server says.
|
||||||
|
remote: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerKind {
|
||||||
|
/// Classifies a dedicated server from the address the client dialled.
|
||||||
|
///
|
||||||
|
/// A loopback address means the process is on this machine (a locally hosted server), which is distinct from an integrated one: it is still a separate process reached over a socket.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn dedicated(addr: std::net::SocketAddr) -> Self {
|
||||||
|
Self::Dedicated {
|
||||||
|
remote: !addr.ip().is_loopback(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a short human-readable label for the session's server kind.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Integrated => "integrated",
|
||||||
|
Self::Dedicated { remote: false } => "dedicated (local)",
|
||||||
|
Self::Dedicated { remote: true } => "dedicated (remote)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "tests/session.rs"]
|
||||||
|
mod tests;
|
||||||
37
crates/shared/src/tests/protocol_authority.rs
Normal file
37
crates/shared/src/tests/protocol_authority.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
//! Wire round-trip tests for the authority-stream messages.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip_server_stats() -> Result<(), postcard::Error> {
|
||||||
|
let msg = AuthorityMessage::ServerStats(ServerStats {
|
||||||
|
measured_tps: 19.4,
|
||||||
|
mean_tick_ms: 12.5,
|
||||||
|
max_tick_ms: 48.0,
|
||||||
|
tick_budget_percent: 25.0,
|
||||||
|
loaded_chunks: 4096,
|
||||||
|
chunks_in_flight: 12,
|
||||||
|
connected_clients: 1,
|
||||||
|
entities: 37,
|
||||||
|
players: 1,
|
||||||
|
uptime_secs: 3600,
|
||||||
|
});
|
||||||
|
|
||||||
|
let bytes = postcard::to_stdvec(&msg)?;
|
||||||
|
let decoded: AuthorityMessage = postcard::from_bytes(&bytes)?;
|
||||||
|
assert_eq!(decoded, msg);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_stats_survive_a_round_trip() {
|
||||||
|
// The default is what a server reports before its first window has closed; it must decode as cleanly as a populated one.
|
||||||
|
let msg = AuthorityMessage::ServerStats(ServerStats::default());
|
||||||
|
let bytes = postcard::to_stdvec(&msg).unwrap_or_default();
|
||||||
|
assert_eq!(
|
||||||
|
postcard::from_bytes::<AuthorityMessage>(&bytes).ok(),
|
||||||
|
Some(msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
43
crates/shared/src/tests/session.rs
Normal file
43
crates/shared/src/tests/session.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
//! Unit tests for server-kind classification.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loopback_addresses_classify_as_local() {
|
||||||
|
let v4 = SocketAddr::from((Ipv4Addr::LOCALHOST, 25565));
|
||||||
|
let v6 = SocketAddr::from((Ipv6Addr::LOCALHOST, 25565));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ServerKind::dedicated(v4),
|
||||||
|
ServerKind::Dedicated { remote: false }
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ServerKind::dedicated(v6),
|
||||||
|
ServerKind::Dedicated { remote: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_loopback_addresses_classify_as_remote() {
|
||||||
|
let addr = SocketAddr::from((Ipv4Addr::new(10, 0, 0, 4), 25565));
|
||||||
|
assert_eq!(
|
||||||
|
ServerKind::dedicated(addr),
|
||||||
|
ServerKind::Dedicated { remote: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn each_kind_has_a_distinct_label() {
|
||||||
|
assert_eq!(ServerKind::Integrated.label(), "integrated");
|
||||||
|
assert_eq!(
|
||||||
|
ServerKind::Dedicated { remote: false }.label(),
|
||||||
|
"dedicated (local)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ServerKind::Dedicated { remote: true }.label(),
|
||||||
|
"dedicated (remote)"
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue