mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
This is helpful in the following to properly aggregate statistics for local migrations. On-behalf-of: SAP philipp.schuster@sap.com Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
694 lines
26 KiB
Rust
694 lines
26 KiB
Rust
// Copyright © 2026 Cyberus Technology GmbH
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
|
|
//! Module for context and metrics of migrations.
|
|
//!
|
|
//! Main exports:
|
|
//! - [`OngoingMigrationContext`]
|
|
//! - [`CompletedMigrationContext`]
|
|
//! - [`MemoryMigrationContext`]
|
|
|
|
use std::fmt;
|
|
use std::fmt::{Display, Formatter};
|
|
use std::time::{Duration, Instant};
|
|
|
|
use thiserror::Error;
|
|
|
|
use crate::protocol::MemoryRangeTable;
|
|
|
|
/// Metrics of the VM downtime during a migration.
|
|
///
|
|
/// By downtime, we mean the time between the VM pause() and the corresponding
|
|
/// resume() on the destination. This downtime covers the time when the vCPUs
|
|
/// didn't execute a single instruction. The network downtime might be longer
|
|
/// and is not covered by this type.
|
|
///
|
|
/// This metric is only relevant for the migration of running VMs.
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct DowntimeContext {
|
|
/// The effective downtime Cloud Hypervisor observed (from the migration sender).
|
|
///
|
|
/// This is roughly the sum of all the other durations.
|
|
pub effective_downtime: Duration,
|
|
/// The time of the final memory iteration.
|
|
pub final_memory_iteration_dur: Duration,
|
|
/// The time needed to aggregate the final VM state (i.e., snapshotting it).
|
|
pub state_dur: Duration,
|
|
/// The time needed to send the final VM state including deserializing it on
|
|
/// the destination
|
|
pub send_state_dur: Duration,
|
|
/// The time of the completion request. This includes resuming the VM (if it
|
|
/// was running before the migration).
|
|
pub complete_dur: Duration,
|
|
}
|
|
|
|
impl Display for DowntimeContext {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
|
write!(
|
|
f,
|
|
// Caution: This format is specifically crafted for the VMM log
|
|
"{}ms (final_iter:{}ms state:{}ms send_state:{}ms complete:{}ms)",
|
|
self.effective_downtime.as_millis(),
|
|
self.final_memory_iteration_dur.as_millis(),
|
|
self.state_dur.as_millis(),
|
|
self.send_state_dur.as_millis(),
|
|
self.complete_dur.as_millis()
|
|
)
|
|
}
|
|
}
|
|
|
|
/// The internal metrics of a completed migration.
|
|
///
|
|
/// The properties of this type help to investigate timings of the migration,
|
|
/// with specific focus on the VM downtime.
|
|
///
|
|
/// This type is static once it was created and should not change.
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct CompletedMigrationContext {
|
|
/// Total duration of the migration.
|
|
pub migration_dur: Duration,
|
|
pub downtime_ctx: DowntimeContext,
|
|
/// The finalized context of the memory migration.
|
|
pub memory_ctx: MemoryMigrationContext,
|
|
}
|
|
|
|
impl CompletedMigrationContext {
|
|
fn new(
|
|
migration_dur: Duration,
|
|
effective_downtime: Duration,
|
|
state_dur: Duration,
|
|
send_state_dur: Duration,
|
|
complete_dur: Duration,
|
|
memory_ctx: MemoryMigrationContext,
|
|
) -> Self {
|
|
Self {
|
|
migration_dur,
|
|
downtime_ctx: DowntimeContext {
|
|
effective_downtime,
|
|
final_memory_iteration_dur: memory_ctx.iteration_duration.unwrap_or_default(),
|
|
state_dur,
|
|
send_state_dur,
|
|
complete_dur,
|
|
},
|
|
memory_ctx,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Error returned when the migration context is advanced in an invalid order.
|
|
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
|
|
pub enum MigrationContextError {
|
|
/// The memory migration context was not finalized before transition.
|
|
#[error("memory migration context should be finalized before pausing the VM")]
|
|
MemoryContextNotFinalized,
|
|
/// The transition to `VmPaused` was attempted from an invalid state.
|
|
#[error("memory migration should only advance from the Begin state")]
|
|
InvalidVmPausedTransition,
|
|
/// Finalization was attempted before memory migration completed.
|
|
#[error("migration should only finalize after memory migration completed")]
|
|
InvalidFinalizeTransition,
|
|
}
|
|
|
|
/// Holds context and metrics about the current ongoing migration.
|
|
///
|
|
/// This is a state-machine to properly reflect the intermediate states and
|
|
/// their properties. This machine does not have a `Completed` variant in favor
|
|
/// of [`CompletedMigrationContext`], which is easier to work with.
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum OngoingMigrationContext {
|
|
/// Migration started.
|
|
Begin {
|
|
/// Begin of the migration.
|
|
migration_begin: Instant,
|
|
},
|
|
/// VM memory fully transferred to the destination and the VM is paused.
|
|
VmPaused {
|
|
/// Begin of the migration.
|
|
migration_begin: Instant,
|
|
/// Downtime begin of the migration.
|
|
downtime_begin: Instant,
|
|
/// The finalized context of the memory migration.
|
|
finalized_memory_ctx: MemoryMigrationContext,
|
|
},
|
|
}
|
|
|
|
impl OngoingMigrationContext {
|
|
/// Creates a new context.
|
|
pub fn new() -> Self {
|
|
Self::Begin {
|
|
migration_begin: Instant::now(),
|
|
}
|
|
}
|
|
|
|
/// Marks the memory migration as completed and records when downtime
|
|
/// started. The VM is now in paused state.
|
|
pub fn set_vm_paused(
|
|
&mut self,
|
|
downtime_begin: Instant,
|
|
finalized_memory_ctx: MemoryMigrationContext,
|
|
) -> Result<(), MigrationContextError> {
|
|
if finalized_memory_ctx.migration_duration.is_none() {
|
|
return Err(MigrationContextError::MemoryContextNotFinalized);
|
|
}
|
|
let migration_begin = match self {
|
|
Self::Begin { migration_begin } => *migration_begin,
|
|
_ => return Err(MigrationContextError::InvalidVmPausedTransition),
|
|
};
|
|
*self = Self::VmPaused {
|
|
migration_begin,
|
|
downtime_begin,
|
|
finalized_memory_ctx,
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
/// Finalizes the metrics and returns a [`CompletedMigrationContext`].
|
|
///
|
|
/// This should be called right after the completed migration was
|
|
/// acknowledged by the receiver. From now on, the metrics are considered
|
|
/// finalized and should not be modified. They can be stored for further
|
|
/// analysis.
|
|
///
|
|
/// # Arguments
|
|
/// - `state_dur`: The time needed to aggregate the final VM state (i.e.,
|
|
/// snapshotting it).
|
|
/// - `send_state_dur`: The time needed to send the final VM state
|
|
/// including deserializing it on the destination.
|
|
/// - `complete_dur`: The time of the completion request. This includes
|
|
/// resuming the VM (if it was running before the migration).
|
|
pub fn finalize(
|
|
self,
|
|
state_dur: Duration,
|
|
send_state_dur: Duration,
|
|
complete_dur: Duration,
|
|
) -> Result<CompletedMigrationContext, MigrationContextError> {
|
|
let (migration_begin, downtime_begin, finalized_memory_ctx) = match self {
|
|
Self::VmPaused {
|
|
migration_begin,
|
|
downtime_begin,
|
|
finalized_memory_ctx,
|
|
} => (migration_begin, downtime_begin, finalized_memory_ctx),
|
|
_ => return Err(MigrationContextError::InvalidFinalizeTransition),
|
|
};
|
|
|
|
Ok(CompletedMigrationContext::new(
|
|
migration_begin.elapsed(),
|
|
downtime_begin.elapsed(),
|
|
state_dur,
|
|
send_state_dur,
|
|
complete_dur,
|
|
finalized_memory_ctx,
|
|
))
|
|
}
|
|
}
|
|
|
|
impl Default for OngoingMigrationContext {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Internal metrics for the precopy migration phase.
|
|
///
|
|
/// The context aggregates runtime statistics such as iteration count,
|
|
/// transferred bytes, durations, bandwidth, and estimated downtime.
|
|
/// These metrics allow the migration logic to make decisions based on
|
|
/// observed runtime behavior, for example terminating further iterations
|
|
/// once the expected downtime falls below a configured threshold.
|
|
///
|
|
/// The structure is updated both between iterations and during an
|
|
/// iteration so that it always reflects the most recent state.
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct MemoryMigrationContext {
|
|
/// Current iteration: 0 initial total transmission, >0 delta transmission.
|
|
pub iteration: usize,
|
|
/// Total bytes sent across all iterations.
|
|
total_sent_bytes: u64,
|
|
/// Total bytes to send in the current iteration.
|
|
pub current_iteration_total_bytes: u64,
|
|
/// The currently measured bandwidth.
|
|
///
|
|
/// This is updated (at least) after each completed iteration.
|
|
bandwidth_bytes_per_second: f64,
|
|
/// Calculated downtime in milliseconds regarding the current bandwidth and
|
|
/// the remaining memory.
|
|
///
|
|
/// This is only `None` for iteration 0.
|
|
///
|
|
/// Please note that this ignores any additional migration overhead and
|
|
/// only looks at the memory transfer itself.
|
|
pub estimated_downtime: Option<Duration>,
|
|
/// Begin of the memory migration.
|
|
pub migration_begin: Instant,
|
|
/// Duration of the memory migration.
|
|
///
|
|
/// This is only `None` until the last iteration is finished.
|
|
migration_duration: Option<Duration>,
|
|
/// Begin of the current iteration.
|
|
iteration_begin: Instant,
|
|
/// Duration of the current iteration.
|
|
///
|
|
/// This includes the transmission, all logging, and update of any metrics.
|
|
///
|
|
/// This is only `None` for iteration 0.
|
|
pub iteration_duration: Option<Duration>,
|
|
/// Begin of the current transfer.
|
|
transfer_begin: Instant,
|
|
/// Duration of the current transfer.
|
|
///
|
|
/// This is only `None` for iteration 0.
|
|
pub transfer_duration: Option<Duration>,
|
|
}
|
|
|
|
impl MemoryMigrationContext {
|
|
/// Creates a new context.
|
|
///
|
|
/// Please note that you should create this struct right before the precopy
|
|
/// memory migration starts, as the field `migration_begin` is set to
|
|
/// [`Instant::now`].
|
|
pub fn new() -> Self {
|
|
Self {
|
|
iteration: 0,
|
|
total_sent_bytes: 0,
|
|
current_iteration_total_bytes: 0,
|
|
bandwidth_bytes_per_second: 0.0,
|
|
estimated_downtime: None,
|
|
migration_begin: Instant::now(),
|
|
migration_duration: None,
|
|
// Will be updated soon -> so this value is never read
|
|
iteration_begin: Instant::now(),
|
|
iteration_duration: None,
|
|
// Will be updated soon -> so this value is never read
|
|
transfer_begin: Instant::now(),
|
|
transfer_duration: None,
|
|
}
|
|
}
|
|
|
|
/// Returns an empty finalized block.
|
|
///
|
|
/// This can be used if no memory was transferred (e.g., local migration).
|
|
pub fn empty_finalized() -> Self {
|
|
let mut this = Self::new();
|
|
this.finalize();
|
|
this
|
|
}
|
|
|
|
/// Updates the metrics right before the transfer over the wire.
|
|
///
|
|
/// Supposed to be called once per precopy memory iteration.
|
|
///
|
|
/// This helps to feed the "is converged?" with fresh metrics to
|
|
/// potentially stop the precopy phase.
|
|
pub fn update_metrics_before_transfer(
|
|
&mut self,
|
|
iteration_begin: Instant,
|
|
iteration_table: &MemoryRangeTable,
|
|
) {
|
|
self.iteration_begin = iteration_begin;
|
|
self.current_iteration_total_bytes = iteration_table.effective_size();
|
|
self.estimated_downtime = if self.current_iteration_total_bytes == 0 {
|
|
Some(Duration::ZERO)
|
|
} else if self.bandwidth_bytes_per_second == 0.0 {
|
|
// Only happens on the very first iteration
|
|
None
|
|
} else {
|
|
let calculated_downtime_s =
|
|
self.current_iteration_total_bytes as f64 / (self.bandwidth_bytes_per_second);
|
|
Some(Duration::from_secs_f64(calculated_downtime_s))
|
|
}
|
|
}
|
|
|
|
/// Updates the metrics right after the transfer over the wire.
|
|
///
|
|
/// Supposed to be called once per precopy memory iteration.
|
|
///
|
|
/// This updates the bandwidth and ensures that
|
|
/// [`Self::update_metrics_before_transfer`] operates on fresh metrics on
|
|
/// the new iteration.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// If the transfer duration is longer than the iteration duration, this
|
|
/// function panics. This can never happen with real-world data but in
|
|
/// artificial unit test scenarios.
|
|
pub fn update_metrics_after_transfer(
|
|
&mut self,
|
|
transfer_begin: Instant,
|
|
transfer_duration: Duration,
|
|
) {
|
|
self.transfer_begin = transfer_begin;
|
|
self.transfer_duration = Some(transfer_duration);
|
|
self.total_sent_bytes += self.current_iteration_total_bytes;
|
|
self.bandwidth_bytes_per_second =
|
|
Self::calculate_bandwidth(self.current_iteration_total_bytes, transfer_duration);
|
|
|
|
// We might have a few operations after that before the loop starts
|
|
// (e.g., logging) again, but practically, this is negligible for this
|
|
// metric.
|
|
self.iteration_duration = Some(self.iteration_begin.elapsed());
|
|
|
|
// Catch programming errors:
|
|
// unwrap is fine as both values are set by now
|
|
assert!(
|
|
self.iteration_duration.unwrap() >= self.transfer_duration.unwrap(),
|
|
"iteration_duration must be larger than transfer_duration: {}ms < {}ms",
|
|
self.iteration_duration.unwrap().as_millis(),
|
|
self.transfer_duration.unwrap().as_millis(),
|
|
);
|
|
}
|
|
|
|
/// Finalizes the metrics.
|
|
///
|
|
/// From now on, the metrics are considered finalized and should not be
|
|
/// modified. They can be stored for further analysis.
|
|
#[inline]
|
|
pub fn finalize(&mut self) {
|
|
// Any overhead from the function call is negligible.
|
|
self.migration_duration = Some(self.migration_begin.elapsed());
|
|
}
|
|
|
|
/// Returns the average bandwidth over the whole duration of the migration.
|
|
#[inline]
|
|
pub fn average_bandwidth(&self) -> f64 {
|
|
Self::calculate_bandwidth(self.total_sent_bytes, self.migration_begin.elapsed())
|
|
}
|
|
|
|
/// Calculates the bandwidth in bytes per second.
|
|
///
|
|
/// Returns `0.0` if the duration is zero to avoid division by zero.
|
|
#[inline]
|
|
fn calculate_bandwidth(bytes: u64, duration: Duration) -> f64 {
|
|
if duration == Duration::ZERO {
|
|
0.0
|
|
} else {
|
|
bytes as f64 / duration.as_secs_f64()
|
|
}
|
|
}
|
|
|
|
/// Calculates the overhead of an iteration.
|
|
///
|
|
/// This is the additional time next to the transfer time and includes
|
|
/// fetching and parsing the dirty log, for example.
|
|
fn iteration_overhead(&self) -> Duration {
|
|
self.iteration_duration
|
|
.and_then(|iter| {
|
|
self.transfer_duration.map(|tr| {
|
|
// This is guaranteed by update_metrics_after_transfer()
|
|
assert!(iter >= tr);
|
|
iter - tr
|
|
})
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
}
|
|
|
|
impl Default for MemoryMigrationContext {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
// The display format must be a compact one-liner to enable concise log messages per iteration.
|
|
impl Display for MemoryMigrationContext {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let curr_mib = self.current_iteration_total_bytes.div_ceil(1024 * 1024);
|
|
let total_mib = self.total_sent_bytes.div_ceil(1024 * 1024);
|
|
|
|
// Current bandwidth in MiB/s
|
|
let curr_bw_mib_s = self.bandwidth_bytes_per_second / 1024.0 / 1024.0;
|
|
|
|
// Time elapsed since memory migration start.
|
|
let elapsed = self
|
|
.migration_duration
|
|
.unwrap_or_else(|| Instant::now() - self.migration_begin)
|
|
.as_secs_f64();
|
|
|
|
// Internally, this again evaluates `self.migration_begin.elapsed()`
|
|
// but this is negligible.
|
|
let avg_bw_mib_s = self.average_bandwidth() / 1024.0 / 1024.0;
|
|
|
|
// Transfer duration and iteration overhead
|
|
let transfer_s = self.transfer_duration.map_or(0.0, |d| d.as_secs_f64());
|
|
let iteration_overhead_ms = self.iteration_overhead().as_millis();
|
|
|
|
let est_downtime_ms = self.estimated_downtime.map_or(0, |d| d.as_millis());
|
|
|
|
write!(
|
|
f,
|
|
"iter={} \
|
|
curr={curr_mib}MiB \
|
|
total={total_mib}MiB \
|
|
bw={curr_bw_mib_s:.2}MiB/s \
|
|
transfer={transfer_s:.2}s \
|
|
overhead={iteration_overhead_ms}ms \
|
|
est_downtime={est_downtime_ms}ms \
|
|
elapsed={elapsed:.2}s \
|
|
avg_bw={avg_bw_mib_s:.2}MiB/s",
|
|
self.iteration,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod unit_tests {
|
|
use super::*;
|
|
|
|
/// Tests for [`CompletedMigrationContext`] and [`OngoingMigrationContext`].
|
|
mod migration_ctx_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn memory_migrated_and_vm_paused_records_transition() {
|
|
let mut ctx = OngoingMigrationContext::new();
|
|
let downtime_begin = Instant::now();
|
|
|
|
let mut memory_ctx = MemoryMigrationContext::new();
|
|
memory_ctx.finalize();
|
|
|
|
ctx.set_vm_paused(downtime_begin, memory_ctx)
|
|
.expect("migration context should transition to VmPaused after memory migration");
|
|
|
|
assert!(matches!(
|
|
ctx,
|
|
OngoingMigrationContext::VmPaused {
|
|
downtime_begin: recorded_downtime_begin,
|
|
..
|
|
} if recorded_downtime_begin == downtime_begin
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn finalize_returns_completed_context() {
|
|
let mut ctx = OngoingMigrationContext::new();
|
|
let downtime_begin = Instant::now() - Duration::from_millis(10);
|
|
|
|
let mut memory_ctx = MemoryMigrationContext::new();
|
|
memory_ctx.finalize();
|
|
|
|
ctx.set_vm_paused(downtime_begin, memory_ctx)
|
|
.expect("migration context should transition to VmPaused after memory migration");
|
|
|
|
let completed = ctx
|
|
.finalize(
|
|
Duration::from_millis(1),
|
|
Duration::from_millis(2),
|
|
Duration::from_millis(3),
|
|
)
|
|
.expect("migration context should finalize after memory migration completed");
|
|
|
|
assert_eq!(completed.downtime_ctx.state_dur, Duration::from_millis(1));
|
|
assert_eq!(
|
|
completed.downtime_ctx.send_state_dur,
|
|
Duration::from_millis(2)
|
|
);
|
|
assert_eq!(
|
|
completed.downtime_ctx.complete_dur,
|
|
Duration::from_millis(3)
|
|
);
|
|
assert!(completed.downtime_ctx.effective_downtime >= Duration::from_millis(10));
|
|
assert!(completed.migration_dur > Duration::ZERO);
|
|
assert!(completed.memory_ctx.migration_duration.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn finalize_errors_before_memory_migration_completed() {
|
|
let err = OngoingMigrationContext::new()
|
|
.finalize(Duration::ZERO, Duration::ZERO, Duration::ZERO)
|
|
.unwrap_err();
|
|
|
|
assert_eq!(err, MigrationContextError::InvalidFinalizeTransition);
|
|
}
|
|
}
|
|
|
|
/// Tests for [`MemoryMigrationContext`].
|
|
mod memory_migration_ctx_tests {
|
|
use std::time::{Duration, Instant};
|
|
|
|
use super::*;
|
|
use crate::protocol::MemoryRange;
|
|
|
|
fn make_table(bytes: u64) -> MemoryRangeTable {
|
|
let mut table = MemoryRangeTable::default();
|
|
if bytes > 0 {
|
|
table.push(MemoryRange {
|
|
gpa: 0,
|
|
length: bytes,
|
|
});
|
|
}
|
|
table
|
|
}
|
|
|
|
/// A controlled migration scenario with fixed timing offsets.
|
|
///
|
|
/// ```text
|
|
/// migration_begin
|
|
/// + 1.0s -> iteration_begin
|
|
/// + 1.1s -> transfer_begin
|
|
/// + 2.0s -> transfer ends (transfer_duration = 0.9s)
|
|
/// + 2.1s -> iteration ends (iteration_duration = 1.1s, overhead = 0.2s)
|
|
/// ```
|
|
struct Scenario {
|
|
migration_begin: Instant,
|
|
iteration_begin: Instant,
|
|
transfer_begin: Instant,
|
|
transfer_duration: Duration,
|
|
}
|
|
|
|
impl Scenario {
|
|
/// We use a fixed point in the past so all offsets are in the past too,
|
|
/// meaning elapsed() calls in the code under test will be >= our durations.
|
|
const FIXPOINT_PAST: Duration = Duration::from_secs(10);
|
|
|
|
fn new() -> Self {
|
|
// Use a fixed point in the past so all offsets are in the past too,
|
|
// meaning elapsed() calls in the code under test will be >= our durations.
|
|
let migration_begin = Instant::now() - Self::FIXPOINT_PAST;
|
|
Self {
|
|
migration_begin,
|
|
iteration_begin: migration_begin + Duration::from_millis(1000),
|
|
transfer_begin: migration_begin + Duration::from_millis(1100),
|
|
transfer_duration: Duration::from_millis(900),
|
|
}
|
|
}
|
|
|
|
fn make_ctx(&self) -> MemoryMigrationContext {
|
|
let mut ctx = MemoryMigrationContext::new();
|
|
// Override migration_begin with our controlled value.
|
|
ctx.migration_begin = self.migration_begin;
|
|
ctx
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn before_transfer_updates_begin_and_bytes() {
|
|
let s = Scenario::new();
|
|
let mut ctx = s.make_ctx();
|
|
|
|
ctx.update_metrics_before_transfer(s.iteration_begin, &make_table(4096));
|
|
|
|
assert_eq!(ctx.iteration_begin, s.iteration_begin);
|
|
assert_eq!(ctx.current_iteration_total_bytes, 4096);
|
|
}
|
|
|
|
#[test]
|
|
fn before_transfer_estimated_downtime() {
|
|
let s = Scenario::new();
|
|
let mut ctx = s.make_ctx();
|
|
|
|
// Empty table -> zero downtime regardless of bandwidth
|
|
ctx.update_metrics_before_transfer(s.iteration_begin, &make_table(0));
|
|
assert_eq!(ctx.estimated_downtime, Some(Duration::ZERO));
|
|
|
|
// No bandwidth yet -> None
|
|
ctx.update_metrics_before_transfer(s.iteration_begin, &make_table(1024));
|
|
assert_eq!(ctx.estimated_downtime, None);
|
|
|
|
// 1024 B/s, 1024 bytes -> 1s
|
|
ctx.bandwidth_bytes_per_second = 1024.0;
|
|
ctx.update_metrics_before_transfer(s.iteration_begin, &make_table(1024));
|
|
assert_eq!(ctx.estimated_downtime, Some(Duration::from_secs(1)));
|
|
}
|
|
|
|
#[test]
|
|
fn after_transfer_updates_timing_and_bandwidth() {
|
|
let s = Scenario::new();
|
|
let mut ctx = s.make_ctx();
|
|
|
|
ctx.update_metrics_before_transfer(s.iteration_begin, &make_table(1024));
|
|
ctx.update_metrics_after_transfer(s.transfer_begin, s.transfer_duration);
|
|
|
|
assert_eq!(ctx.transfer_begin, s.transfer_begin);
|
|
assert_eq!(ctx.transfer_duration, Some(s.transfer_duration));
|
|
// 1024 bytes / 0.9s
|
|
assert_eq!(ctx.bandwidth_bytes_per_second, 1024.0 / 0.9);
|
|
// iteration_duration = time from iteration_begin until now (>= transfer_duration)
|
|
assert!(ctx.iteration_duration.unwrap() >= s.transfer_duration);
|
|
// Zero transfer_duration -> bandwidth is 0.0, no division by zero
|
|
ctx.update_metrics_before_transfer(s.iteration_begin, &make_table(1024));
|
|
ctx.update_metrics_after_transfer(s.transfer_begin, Duration::ZERO);
|
|
assert_eq!(ctx.bandwidth_bytes_per_second, 0.0);
|
|
|
|
// Check finalize() sets migration duration
|
|
assert_eq!(ctx.migration_duration, None);
|
|
ctx.finalize();
|
|
assert!(matches!(ctx.migration_duration, Some(d) if d >= Scenario::FIXPOINT_PAST));
|
|
}
|
|
|
|
#[test]
|
|
fn two_iterations_accumulate_bytes_and_feed_downtime_estimate() {
|
|
let s = Scenario::new();
|
|
let mut ctx = s.make_ctx();
|
|
|
|
// Iteration 0: no bandwidth yet -> downtime is None
|
|
ctx.update_metrics_before_transfer(s.iteration_begin, &make_table(1024));
|
|
assert_eq!(ctx.estimated_downtime, None);
|
|
ctx.update_metrics_after_transfer(s.transfer_begin, s.transfer_duration);
|
|
assert_eq!(ctx.total_sent_bytes, 1024);
|
|
|
|
// Iteration 1: bandwidth now known -> downtime is Some
|
|
ctx.update_metrics_before_transfer(s.iteration_begin, &make_table(2048));
|
|
assert!(ctx.estimated_downtime.is_some());
|
|
ctx.update_metrics_after_transfer(s.transfer_begin, s.transfer_duration);
|
|
assert_eq!(ctx.total_sent_bytes, 1024 + 2048);
|
|
|
|
// Check finalize() sets migration duration
|
|
assert_eq!(ctx.migration_duration, None);
|
|
ctx.finalize();
|
|
assert!(matches!(ctx.migration_duration, Some(d) if d >= Scenario::FIXPOINT_PAST));
|
|
}
|
|
|
|
#[test]
|
|
/// The display format is specifically crafted to be very insightful in logs.
|
|
/// Therefore, we have a dedicated test for that format.
|
|
fn display_format() {
|
|
let s = Scenario::new();
|
|
let mut ctx = s.make_ctx();
|
|
|
|
// Iteration 0: 1 MiB in 1s
|
|
ctx.update_metrics_before_transfer(s.iteration_begin, &make_table(1024 * 1024));
|
|
ctx.update_metrics_after_transfer(s.transfer_begin, Duration::from_secs(1));
|
|
ctx.iteration += 1;
|
|
|
|
// Iteration 1: 512 KiB in 1s; fix migration_duration for deterministic elapsed/avg_bw
|
|
ctx.update_metrics_before_transfer(s.iteration_begin, &make_table(512 * 1024));
|
|
ctx.update_metrics_after_transfer(s.transfer_begin, Duration::from_secs(1));
|
|
|
|
ctx.migration_duration = Some(Duration::from_secs(2));
|
|
let out = ctx.to_string();
|
|
|
|
assert_eq!(
|
|
out,
|
|
"iter=1 curr=1MiB total=2MiB bw=0.50MiB/s transfer=1.00s overhead=8000ms est_downtime=500ms elapsed=2.00s avg_bw=0.15MiB/s"
|
|
);
|
|
|
|
// Should change elapsed() time!
|
|
// Since this is at least 10s, we never face timing issues in CI!
|
|
ctx.finalize();
|
|
let out2 = ctx.to_string();
|
|
assert_ne!(out2, out, "elapsed time should have changed! is={out2}");
|
|
}
|
|
}
|
|
}
|