use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug)]
pub struct Timings {
pub server_create_lo_ms: u64,
pub server_create_hi_ms: u64,
pub server_delete_ms: u64,
pub server_delete_per_volume_ms: u64,
pub storage_create_ms: u64,
pub storage_delete_ms: u64,
pub stop_soft_ms: u64,
pub stop_hard_ms: u64,
pub start_ms: u64,
pub resize_ms: u64,
pub guest_install_ms: u64,
pub guest_install_uart_lo_ms: u64,
pub guest_install_uart_hi_ms: u64,
pub provider_wall_lo_ms: u64,
pub provider_wall_hi_ms: u64,
pub template_first_boot_ms: u64,
pub import_upload_ms_per_mib: u64,
pub storage_sync_lo_ms: u64,
pub storage_sync_hi_ms: u64,
pub clone_online_ms: u64,
pub clone_sync_lo_ms: u64,
pub clone_sync_hi_ms: u64,
pub clone_prepare_ms: u64,
pub install_pass_lo_ms: u64,
pub install_pass_hi_ms: u64,
pub installer_started_ms: u64,
pub installer_started_window_ms: u64,
pub reboot_pass_lo_ms: u64,
pub reboot_pass_hi_ms: u64,
pub poweroff_notice_ms: u64,
pub storage_grow_ms: u64,
pub vnc_settle_ms: u64,
pub sync_tail_maintenance_ms: u64,
}
impl Timings {
pub fn reimage_observers(&self, seed: u64, tag: &str) -> (u64, u64) {
let mut r = crate::rng::SplitMix64::derive(seed, &format!("reimage/{tag}"));
(
r.range(self.guest_install_uart_lo_ms, self.guest_install_uart_hi_ms),
r.range(self.provider_wall_lo_ms, self.provider_wall_hi_ms),
)
}
}
impl Default for Timings {
fn default() -> Self {
Timings {
server_create_lo_ms: 98_000,
server_create_hi_ms: 105_000,
server_delete_ms: 60_000,
server_delete_per_volume_ms: 65_000,
storage_create_ms: 3_000,
storage_delete_ms: 4_000,
stop_soft_ms: 12_000,
stop_hard_ms: 2_000,
start_ms: 10_000,
resize_ms: 25_000,
guest_install_ms: 10_011,
guest_install_uart_lo_ms: 2_001,
guest_install_uart_hi_ms: 4_752,
provider_wall_lo_ms: 110_000,
provider_wall_hi_ms: 253_000,
template_first_boot_ms: 45_000,
import_upload_ms_per_mib: 120,
storage_sync_lo_ms: 100_000,
storage_sync_hi_ms: 130_000,
clone_online_ms: 47_000,
clone_sync_lo_ms: 100_000,
clone_sync_hi_ms: 130_000,
clone_prepare_ms: 3_000,
install_pass_lo_ms: 130_000,
install_pass_hi_ms: 146_000,
installer_started_ms: 3_600,
installer_started_window_ms: 100,
reboot_pass_lo_ms: 900_000,
reboot_pass_hi_ms: 1_100_000,
poweroff_notice_ms: 10_000,
storage_grow_ms: 37_000,
vnc_settle_ms: 2_000,
sync_tail_maintenance_ms: 4_000,
}
}
}
pub struct Clock {
started: Instant,
speed_milli: u64,
pushed: AtomicU64,
}
impl Clock {
pub fn new(speed_milli: u64) -> Clock {
Clock {
started: Instant::now(),
speed_milli,
pushed: AtomicU64::new(0),
}
}
pub fn faithful() -> Clock {
Clock::new(1000)
}
pub fn virtual_only() -> Clock {
Clock::new(0)
}
pub fn speed_milli(&self) -> u64 {
self.speed_milli
}
pub fn now_ms(&self) -> u64 {
let real = self.started.elapsed().as_millis() as u64;
real.saturating_mul(self.speed_milli) / 1000 + self.pushed.load(Ordering::SeqCst)
}
pub fn advance_ms(&self, ms: u64) {
self.pushed.fetch_add(ms, Ordering::SeqCst);
}
pub fn real_wait_for(&self, at_ms: u64) -> Option<Duration> {
if self.speed_milli == 0 {
return None;
}
let now = self.now_ms();
if at_ms <= now {
return None;
}
let virt = at_ms - now;
Some(Duration::from_millis(virt.saturating_mul(1000) / self.speed_milli))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn virtual_clock_only_moves_when_pushed() {
let c = Clock::virtual_only();
assert_eq!(c.now_ms(), 0);
std::thread::sleep(Duration::from_millis(30));
assert_eq!(c.now_ms(), 0, "real time must not leak in at speed 0");
c.advance_ms(98_000);
assert_eq!(c.now_ms(), 98_000);
}
#[test]
fn faithful_clock_tracks_real_time() {
let c = Clock::faithful();
std::thread::sleep(Duration::from_millis(40));
let n = c.now_ms();
assert!((30..400).contains(&n), "{n}");
}
#[test]
fn a_speed_knob_scales_the_wait_not_the_order() {
let c = Clock::new(100_000); let w = c.real_wait_for(100_000).unwrap();
assert!(w <= Duration::from_millis(1100), "{w:?}");
assert_eq!(Clock::virtual_only().real_wait_for(100_000), None);
}
#[test]
fn the_installer_is_not_slow_the_provider_is() {
let t = Timings::default();
for tag in ["a", "b", "c", "d", "e"] {
let (uart, wall) = t.reimage_observers(42, tag);
assert!((2_001..=4_752).contains(&uart), "uart {uart}");
assert!((110_000..=253_000).contains(&wall), "wall {wall}");
assert!(
wall > uart * 20,
"the gap is two orders of magnitude, and it is the PROVIDER's: {wall} vs {uart}"
);
}
assert_eq!(t.reimage_observers(42, "a"), t.reimage_observers(42, "a"));
assert_ne!(t.reimage_observers(42, "a"), t.reimage_observers(43, "a"));
}
#[test]
fn the_measured_iso_uploads_in_five_seconds() {
let t = Timings::default();
let mib = 43_485_184u64 / (1024 * 1024);
let ms = mib * t.import_upload_ms_per_mib;
assert!((4_500..=5_500).contains(&ms), "{ms} ms for {mib} MiB");
}
#[test]
fn the_sync_dwarfs_the_upload_and_is_paid_twice() {
let t = Timings::default();
let upload = (43_485_184u64 / (1024 * 1024)) * t.import_upload_ms_per_mib;
assert!(t.storage_sync_lo_ms > upload * 15, "the wait is the cost, not the transfer");
let one_medium = upload + t.storage_sync_lo_ms;
let a_reimage = 2 * one_medium;
assert!(a_reimage > 200_000, "two media is over three minutes: {a_reimage} ms");
}
#[test]
fn delete_of_a_four_volume_appliance_exceeds_five_minutes() {
let t = Timings::default();
let front = t.server_delete_ms;
let appliance = t.server_delete_ms + 4 * t.server_delete_per_volume_ms;
assert!((55_000..=65_000).contains(&front), "{front}");
assert!(appliance > 300_000, "{appliance}");
}
}