use std::time::Duration;
use crate::gate::Rate;
use crate::manager::{
InboundOverflow, Limits, OutboundOverflow, Overflow, PresenceOverflow, Queues, DEFAULT_CONDITIONER_CAPACITY,
};
pub const DEFAULT_SOCKET_BUFFER_BYTES: usize = 540 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Priority {
LossFree,
LatencyFirst,
}
pub const MIN_OUTBOUND_CAPACITY: usize = 4;
pub const MIN_CONTROLLER_CAPACITY: usize = 8;
impl Priority {
fn headroom(self) -> usize {
match self {
Priority::LossFree => 2,
Priority::LatencyFirst => 1,
}
}
}
#[derive(Debug, Clone)]
pub struct Workload {
pub tick_rate: u32,
pub peak_players: usize,
pub ops_per_player_per_tick: u32,
pub stall_tolerance: Duration,
pub join_burst: usize,
pub tick_budget: Option<Duration>,
pub max_payload: usize,
pub memory_budget: Option<usize>,
pub priority: Priority,
pub socket_buffer_bytes: usize,
}
impl Default for Workload {
fn default() -> Self {
Self {
tick_rate: 20,
peak_players: 32,
ops_per_player_per_tick: 1,
stall_tolerance: Duration::from_millis(500),
join_burst: 32,
tick_budget: None,
max_payload: 4 * 1024,
memory_budget: None,
priority: Priority::LatencyFirst,
socket_buffer_bytes: DEFAULT_SOCKET_BUFFER_BYTES,
}
}
}
fn events(rate: u32, over: Duration) -> usize {
(rate as f64 * over.as_secs_f64()).ceil() as usize
}
impl Workload {
pub fn action() -> Self {
Self {
tick_rate: 60,
peak_players: 16,
ops_per_player_per_tick: 2,
stall_tolerance: Duration::from_millis(200),
join_burst: 16,
max_payload: 512,
priority: Priority::LatencyFirst,
..Self::default()
}
}
pub fn horde() -> Self {
Self {
tick_rate: 60,
peak_players: 64,
ops_per_player_per_tick: 2,
stall_tolerance: Duration::from_secs(1),
join_burst: 64,
max_payload: 40 * 1024,
memory_budget: Some(256 * 1024 * 1024),
priority: Priority::LatencyFirst,
..Self::default()
}
}
pub fn turn_based() -> Self {
Self {
tick_rate: 4,
peak_players: 8,
ops_per_player_per_tick: 1,
stall_tolerance: Duration::from_secs(5),
join_burst: 8,
max_payload: 8 * 1024,
priority: Priority::LossFree,
..Self::default()
}
}
pub fn social_relay() -> Self {
Self {
tick_rate: 10,
peak_players: 4096,
ops_per_player_per_tick: 1,
stall_tolerance: Duration::from_secs(2),
join_burst: 512,
max_payload: 512,
memory_budget: Some(512 * 1024 * 1024),
priority: Priority::LatencyFirst,
..Self::default()
}
}
pub fn spectator() -> Self {
Self {
tick_rate: 30,
peak_players: 4096,
ops_per_player_per_tick: 0,
stall_tolerance: Duration::from_secs(2),
join_burst: 512,
max_payload: 8 * 1024,
memory_budget: Some(512 * 1024 * 1024),
priority: Priority::LatencyFirst,
..Self::default()
}
}
pub fn lobby() -> Self {
Self {
tick_rate: 1,
peak_players: 2048,
ops_per_player_per_tick: 0,
stall_tolerance: Duration::from_secs(10),
join_burst: 2048,
max_payload: 4 * 1024,
priority: Priority::LossFree,
..Self::default()
}
}
pub fn local() -> Self {
Self::default()
}
fn backlog_ticks(&self) -> usize {
match self.tick_budget {
None => 1,
Some(budget) => events(self.tick_rate, budget),
}
}
fn socket_frames(&self) -> usize {
self.socket_buffer_bytes / self.max_payload.max(1)
}
}
impl Queues {
pub fn for_workload(workload: &Workload) -> Self {
let headroom = workload.priority.headroom();
let stall = events(workload.tick_rate, workload.stall_tolerance);
let mut outbound = (stall.saturating_sub(workload.socket_frames()) * headroom).max(MIN_OUTBOUND_CAPACITY);
if let Some(budget) = workload.memory_budget {
let per_connection = budget / workload.peak_players.max(1) / workload.max_payload.max(1);
outbound = outbound.min(per_connection);
}
let arrivals_per_tick = workload.peak_players * workload.ops_per_player_per_tick as usize;
let backlog = workload.backlog_ticks() * arrivals_per_tick;
let pipe = (arrivals_per_tick + backlog) * headroom;
Self {
inbound: pipe.div_ceil(2).max(MIN_CONTROLLER_CAPACITY),
decoded: pipe.div_ceil(2).max(MIN_CONTROLLER_CAPACITY),
presence: (workload.join_burst * headroom).max(MIN_CONTROLLER_CAPACITY),
outbound: outbound.max(1),
conditioner: DEFAULT_CONDITIONER_CAPACITY,
}
}
}
impl Overflow {
pub fn for_workload(workload: &Workload) -> Self {
match workload.priority {
Priority::LossFree => Self {
outbound: OutboundOverflow::Disconnect,
inbound: InboundOverflow::Backpressure,
presence: PresenceOverflow::Backpressure,
},
Priority::LatencyFirst => Self::drop_everywhere(),
}
}
}
pub const RATE_HEADROOM: f64 = 4.0;
pub const MIN_INBOUND_RATE: f64 = 5.0;
impl Rate {
pub fn for_workload(workload: &Workload) -> Self {
let claimed = f64::from(workload.tick_rate) * f64::from(workload.ops_per_player_per_tick);
Self::per_second((claimed * RATE_HEADROOM).max(MIN_INBOUND_RATE))
}
}
impl Limits {
pub fn for_workload(workload: &Workload) -> Self {
let cap = workload.max_payload.saturating_mul(2).max(64 * 1024);
Self {
max_frame_bytes: cap,
max_message_bytes: cap,
inbound_rate: None,
outbound_budget: None,
..Limits::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn presets() -> Vec<(&'static str, Workload)> {
vec![
("action", Workload::action()),
("horde", Workload::horde()),
("turn_based", Workload::turn_based()),
("social_relay", Workload::social_relay()),
("spectator", Workload::spectator()),
("lobby", Workload::lobby()),
("local", Workload::local()),
]
}
#[test]
fn every_preset_derives_something_a_channel_can_be_built_from() {
for (name, workload) in presets() {
let queues = Queues::for_workload(&workload);
for (field, depth) in [
("inbound", queues.inbound),
("decoded", queues.decoded),
("presence", queues.presence),
("outbound", queues.outbound),
("conditioner", queues.conditioner),
] {
assert!(depth > 0, "{name}.{field} is zero; session_channel panics on that");
}
}
}
#[test]
fn a_preset_that_derives_what_another_does_is_a_second_name_not_a_second_preset() {
let derived: Vec<_> = presets()
.into_iter()
.map(|(name, workload)| {
let q = Queues::for_workload(&workload);
let l = Limits::for_workload(&workload);
(
name,
(q.inbound, q.decoded, q.presence, q.outbound, l.max_frame_bytes),
)
})
.collect();
let mut collisions = Vec::new();
for (position, (name, shape)) in derived.iter().enumerate() {
for (other, other_shape) in &derived[..position] {
if shape == other_shape {
collisions.push(format!("{name} derives what {other} does: {shape:?}"));
}
}
}
assert!(collisions.is_empty(), "{}", collisions.join("; "));
}
#[test]
fn a_stall_the_socket_already_covers_needs_no_queue() {
let small = Workload {
tick_rate: 60,
stall_tolerance: Duration::from_millis(200),
max_payload: 512,
..Workload::default()
};
assert!(
small.socket_frames() > events(small.tick_rate, small.stall_tolerance),
"1049 frames of socket against 12 of stall"
);
assert_eq!(Queues::for_workload(&small).outbound, MIN_OUTBOUND_CAPACITY);
}
#[test]
fn a_large_frame_leaves_the_queue_carrying_the_stall() {
let large = Workload {
tick_rate: 60,
stall_tolerance: Duration::from_secs(1),
max_payload: 40 * 1024,
memory_budget: None,
priority: Priority::LatencyFirst,
..Workload::default()
};
assert_eq!(large.socket_frames(), 13);
assert_eq!(Queues::for_workload(&large).outbound, 47);
}
#[test]
fn the_socket_estimate_tracks_what_was_measured() {
for (payload, measured) in [(512usize, 1049usize), (4096, 135), (40960, 14)] {
let workload = Workload {
max_payload: payload,
..Workload::default()
};
let estimated = workload.socket_frames();
let apart = estimated.abs_diff(measured);
assert!(
apart <= 1 || apart * 20 <= measured,
"at {payload} B the estimate is {estimated} frames against {measured} measured"
);
}
}
#[test]
fn a_memory_budget_caps_what_a_stall_asks_for() {
let mut hungry = Workload::horde();
hungry.memory_budget = None;
let uncapped = Queues::for_workload(&hungry).outbound;
hungry.memory_budget = Some(16 * 1024 * 1024);
let capped = Queues::for_workload(&hungry).outbound;
assert!(capped < uncapped, "{capped} should be under {uncapped}");
}
#[test]
fn an_unset_tick_budget_is_one_tick_and_not_two() {
for tick_rate in [1u32, 4, 10, 20, 30, 60, 128] {
let workload = Workload {
tick_rate,
tick_budget: None,
..Workload::default()
};
assert_eq!(workload.backlog_ticks(), 1, "at {tick_rate} Hz");
}
}
#[test]
fn an_explicit_tick_budget_is_read_in_ticks() {
let workload = Workload {
tick_rate: 60,
tick_budget: Some(Duration::from_millis(50)),
..Workload::default()
};
assert_eq!(workload.backlog_ticks(), 3, "50ms of a 60Hz tick is three of them");
}
#[test]
fn no_preset_spends_more_than_the_budget_it_names() {
for (name, workload) in presets() {
let Some(budget) = workload.memory_budget else { continue };
let spent = Queues::for_workload(&workload).outbound * workload.peak_players * workload.max_payload;
assert!(spent <= budget, "{name} derives {spent} B against a budget of {budget} B");
}
}
#[test]
fn priority_is_what_decides_whether_a_queue_waits() {
let waiting = Overflow::for_workload(&Workload::turn_based());
assert_eq!(waiting.inbound, InboundOverflow::Backpressure);
assert_eq!(waiting.outbound, OutboundOverflow::Disconnect);
let dropping = Overflow::for_workload(&Workload::action());
assert_eq!(dropping, Overflow::drop_everywhere());
}
#[test]
fn an_audience_that_never_sends_still_gets_a_usable_inbound_queue() {
let queues = Queues::for_workload(&Workload::spectator());
assert_eq!(queues.inbound, MIN_CONTROLLER_CAPACITY);
assert_eq!(queues.decoded, MIN_CONTROLLER_CAPACITY);
}
}