Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
10 kB · 256 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257//! How big a task a match needs, from what is in it.//!//! A match task runs Suramadu, the host JVM with every Princess inside it,//! the watcher, and one MegaMek client JVM per connected human. The task//! definition carries the one-human bot-match shape; anything bigger asks//! for more at launch through RunTask's per-task override — the same//! mechanism the manifest URL rides in on, so nothing new touches the task//! definition.//!//! The demand model, in thousandths of a vCPU://!//! demand = BASE the host, Suramadu, the watcher//! + HUMAN · humans one client JVM each; boots are//! simultaneous, so boot is the//! sizing case, not steady state//! + BOT_FIRST + BOT_EXTRA·(n−1) Princess thinks mostly one at a//! time; presence costs more than//! count//! + UNIT_BUMP if units > UNIT_THRESHOLD//!//! The coefficients are anchored to what production has shown, not to a//! fitted curve: one human plus one bot runs clean on the 2048/8192 task//! definition (many matches, no client stalls in their logs); two humans//! on the same shape stalled both boots for ten seconds in the first PvP//! match, so two clients take the next tier; and a host running two//! thinking Princesses saturates about one vCPU on its own. The unit bump//! is the one unanchored term, deliberately conservative. Each launch logs//! the size it chose, and the per-match stats artifacts are where these//! numbers get refined - from real matches, not a laptop stand-in.
/// What RunTask is asked for. Strings because the ECS API takes strings.////// `memory` is owned rather than `&'static str`: the legal values are a/// range per tier, not a short list, and spelling them all out was already/// wrong once — 24576 MiB, which eight clients want, fell through a match/// arm to 20480.#[derive(Debug, PartialEq)]pub struct TaskSize { pub cpu: &'static str, pub memory: String,}
/// One Fargate CPU tier: its size in Fargate units, the string RunTask/// wants, and the memory range and step it accepts. Fargate rejects any/// combination outside these, so the pick snaps into them.////// The step is per tier and is not decoration: 4 vCPU takes memory in 1 GiB/// increments, 8 vCPU in 4 GiB, and 16 vCPU in 8 GiB. Asking 8 vCPU for/// 18432 MiB is as invalid as asking it for 4096.struct Tier { units: u32, cpu: &'static str, memory_min: u32, memory_max: u32, memory_step: u32,}
const TIERS: &[Tier] = &[ Tier { units: 2048, cpu: "2048", memory_min: 4096, memory_max: 16384, memory_step: 1024, }, Tier { units: 4096, cpu: "4096", memory_min: 8192, memory_max: 30720, memory_step: 1024, }, Tier { units: 8192, cpu: "8192", memory_min: 16384, memory_max: 61440, memory_step: 4096, }, // 16 vCPU, and the reason this tier exists here: eight human seats ask // for 8500 milli-vCPU, which is past what the 8192 tier covers. Without // it the pick fell off the end of the table and served an eight-client // match on eight vCPUs, silently. Tier { units: 16384, cpu: "16384", memory_min: 32768, memory_max: 122880, memory_step: 8192, },];
// Production-anchored coefficients, in milli-vCPU. See the module doc for// what each term pays for.const BASE_MILLI: u32 = 500;const HUMAN_MILLI: u32 = 1000;const BOT_FIRST_MILLI: u32 = 500;const BOT_EXTRA_MILLI: u32 = 250;const UNIT_THRESHOLD: u32 = 20;const UNIT_BUMP_MILLI: u32 = 1000;
/// Client JVM heap plus its share of encode buffers, MiB.const HUMAN_MEMORY_MIB: u32 = 2560;/// Host and Suramadu heaps plus the OS and page cache, MiB.const BASE_MEMORY_MIB: u32 = 4096;/// What the task definition already grants; never ask below it.const FLOOR_MEMORY_MIB: u32 = 8192;
/// The size a match of this shape wants. Never below the task definition's/// own two-vCPU shape: the definition is the floor, this only asks up.////// The top tier is a ceiling, not a promise: a shape that wants more than/// 16 vCPU gets 16 and runs slower, the same way the old top tier behaved./// Nothing here refuses to launch — what stops a lobby growing past what/// Fargate can serve belongs in the lobby, not in the sizer.pub fn task_size(humans: u32, bots: u32, units: u32) -> TaskSize { let mut demand = BASE_MILLI + HUMAN_MILLI * humans; if bots > 0 { demand += BOT_FIRST_MILLI + BOT_EXTRA_MILLI * (bots - 1); } if units > UNIT_THRESHOLD { demand += UNIT_BUMP_MILLI; }
// The task definition's own shape is the floor: this override only // ever asks up, so a match the old shape served keeps it byte for byte. let memory_wanted = (BASE_MEMORY_MIB + HUMAN_MEMORY_MIB * humans).max(FLOOR_MEMORY_MIB);
// Smallest tier that covers the CPU demand (1024 Fargate units ≈ 1000 // milli-vCPU), with the memory snapped into what that tier accepts. let tier = TIERS .iter() .find(|tier| demand <= tier.units * 1000 / 1024) .unwrap_or(&TIERS[TIERS.len() - 1]); TaskSize { cpu: tier.cpu, memory: fit_memory(memory_wanted, tier).to_string(), }}
/// The wanted memory as a figure this tier actually accepts: rounded *up*/// to its step, so nothing is quietly served less than it asked for, and/// held inside the tier's range.////// The ceiling is applied before the rounding rather than after. Rounding/// first is what a reader expects and it overflows on a large enough/// figure; capping first cannot, and costs nothing, because every tier's/// maximum is itself a multiple of its step — so a capped value is already/// on the step and the rounding leaves it alone.fn fit_memory(wanted: u32, tier: &Tier) -> u32 { let capped = wanted.min(tier.memory_max); let stepped = capped.div_ceil(tier.memory_step) * tier.memory_step; stepped.max(tier.memory_min)}
#[cfg(test)]mod tests { use super::*;
/// Today's bot match keeps today's shape: the override must not shrink /// what already works. #[test] fn a_bot_match_keeps_the_task_definition_shape() { let size = task_size(1, 1, 3); assert_eq!(size.cpu, "2048"); assert_eq!(size.memory, "8192"); }
/// The case the first PvP match proved undersized: two client JVMs on /// two vCPUs stalled both players' boots for ten seconds. #[test] fn two_humans_get_four_vcpus() { let size = task_size(2, 0, 2); assert_eq!(size.cpu, "4096"); }
/// Citadel Blitz full: one human, four bots, 29 units. #[test] fn a_big_scenario_bumps_the_tier() { let size = task_size(1, 4, 29); assert_eq!(size.cpu, "4096"); }
/// Five humans is beyond anything the UI offers today; the point is the /// formula does not fall off a cliff or exceed Fargate's ceiling. #[test] fn many_humans_stay_within_fargate() { let size = task_size(5, 0, 29); assert_eq!(size.cpu, "8192"); assert!(size.memory.parse::<u32>().unwrap() >= 16384); }
/// Eight human seats: 8500 milli-vCPU, past what the 8192 tier covers. /// Before the 16384 tier existed this fell off the end of the table and /// served eight client JVMs on eight vCPUs without saying so. #[test] fn eight_humans_take_the_sixteen_vcpu_tier() { let size = task_size(8, 0, 16); assert_eq!(size.cpu, "16384"); assert_eq!( size.memory, "32768", "eight client heaps want 24576 MiB, and the tier's floor is what \ it can actually be given" ); }
/// The arithmetic bug the eight-seat case turned up: 24576 MiB is a /// legal 8192-tier value (that tier steps by 4096) and the old spelled- /// out table had no arm for it, so it fell through to 20480 — less than /// was asked for, which is the one direction rounding must never go. #[test] fn memory_rounds_up_into_the_tier_step_never_down() { let eight = &TIERS[2]; assert_eq!(eight.cpu, "8192"); assert_eq!(fit_memory(24576, eight), 24576, "an exact step is kept"); assert_eq!(fit_memory(20481, eight), 24576, "a part step rounds up"); assert_eq!(fit_memory(1, eight), eight.memory_min); assert_eq!(fit_memory(u32::MAX, eight), eight.memory_max); }
/// Memory follows the client heaps and stays inside the chosen tier's /// legal range — and on its step — for every shape a lobby can now /// produce, eight seats included. #[test] fn memory_lands_in_the_tier_range() { for humans in 0..=8 { for bots in 0..=8 { let size = task_size(humans, bots, 29); let cpu: u32 = size.cpu.parse().unwrap(); let memory: u32 = size.memory.parse().unwrap(); let tier = TIERS.iter().find(|tier| tier.cpu == size.cpu).unwrap(); assert!( memory >= tier.memory_min && memory <= tier.memory_max, "humans={humans} bots={bots}: {cpu} cpu with {memory} MiB" ); assert_eq!( memory % tier.memory_step, 0, "humans={humans} bots={bots}: {memory} MiB is not on the \ {} MiB step {cpu} cpu takes", tier.memory_step ); } } }
/// Every tier's own range has to be expressible on its own step, or /// `fit_memory`'s clamp could hand back a figure Fargate rejects. #[test] fn every_tier_range_sits_on_its_step() { for tier in TIERS { assert_eq!(tier.memory_min % tier.memory_step, 0, "{}", tier.cpu); assert_eq!(tier.memory_max % tier.memory_step, 0, "{}", tier.cpu); } }}