// TODO: Replace RunnerArgs with a Builder struct implementing fluent interface /// A set of arguments that define the behavior of Runner #[derive(Serialize, Deserialize, Debug)] pubstruct RunnerArgs { /// How long should Runner run the test suite before timing out pub timeout: Duration, /// Whether memory will be locked before testing and whether the requested memory size of /// testing can be reduced to accomodate memory locking /// If memory locking failed but is required, Runner returns with error pub mem_lock_mode: MemLockMode, /// Whether the process working set can be resized to accomodate memory locking /// This argument is only meaningful for Windows pub allow_working_set_resize: bool, /// Whether mulithreading is enabled pub allow_multithread: bool, /// Whether Runner returns immediately if a test fails or continues until all tests are run pub allow_early_termination: bool,
}
/// The minimum memory length (in usize) for Runner to run tests on /// On a 64-bit machine, this is the size of a page pubconst MIN_MEMORY_LENGTH: usize = 512;
/// A struct to ensure the test timeouts in a given duration #[derive(Debug)] struct TimeoutChecker {
deadline: Instant,
state: Option<TimeoutCheckerState>,
}
impl Runner { /// Create a Runner containing all test kinds in random order pubfn all_tests_random_order(args: &RunnerArgs) -> Runner { letmut test_kinds = TestKind::ALL.to_vec();
test_kinds.shuffle(&mut thread_rng());
#[cfg(windows)] // TODO: When it is MemLockMode::Resizable and working set resize failed, consider shrinking // the memory region and try again let _working_set_resize_guard = ifself.allow_working_set_resize {
Some(
replace_set_size(std::mem::size_of_val(memory))
.context("Failed to replace process working set size")?,
)
} else {
None
};
for test_kind in &self.test_kinds { let test_result = if timed_out {
Err(memtest::Error::Observer(RuntimeError::Timeout))
} elseifself.allow_multithread {
std::thread::scope(|scope| { let num_threads = num_cpus::get(); let chunk_size = memory.len() / num_threads;
letmut handles = vec![]; for chunk in memory.chunks_exact_mut(chunk_size) {
handles.push(scope.spawn(|| self.run_test(*test_kind, chunk, deadline)));
}
/// Returns true if all tests were run successfully and all tests passed pubfn all_pass(&self) -> bool { self.reports
.iter()
.all(|report| matches!(report.outcome, Ok(Outcome::Pass)))
}
}
impl memtest::TestObserver for TimeoutChecker { type Error = RuntimeError;
/// Initialize TimeoutCheckerState /// This function should be called in the beginning of a memtest. fn init(&mutself, expected_iter: u64) { const FIRST_CHECKPOINT: u64 = 8;
assert!( self.state.is_none(), "init() should only be called once per test"
);
// The first checkpoint is set to 8 to have a more accurate sample of duration per // iteration for determining new checkpoint self.state = Some(TimeoutCheckerState {
test_start_time: Instant::now(),
expected_iter,
completed_iter: 0,
checkpoint: FIRST_CHECKPOINT,
});
}
/// Check if the current iteration is a checkpoint. If so, check if timeout occurred /// /// This function should be called in every iteration of a memtest. /// /// To reduce overhead, the function only checks for timeout at specific checkpoints, and /// early returns otherwise. /// // It is important to ensure that the "early return" hot path is inlined. This results in a // 100% improvement in performance. #[inline(always)] fn check(&mutself) -> Result<(), Self::Error> { let state = self
.state
.as_mut()
.expect("init() should be called before check()");
// TODO: Consider separating this functionality to a `ProgressTracer` // Note: Because `trace_progress()` is only called in `on_checkpoints()`, not every percent // of the test progress is traced. If memtests are running way ahead of the given deadline, the // progress may only be traced once or twice. Although this makes the logs less comprehensive, // it avoids signficiant perforamnce overhead. fn trace_progress(&mutself) { if tracing::enabled!(tracing::Level::TRACE) {
trace!( "Progress on checkpoint: {:.2}%", self.completed_iter as f64 / self.expected_iter as f64 * 100.0
);
}
}
/// Calculate the remaining time before the deadline and schedule the next check at 75% of that /// interval, then estimate the number of iterations to get there and set as next checkpoint fn set_next_checkpoint(&mutself, deadline: Instant, current_time: Instant) { const DEADLINE_CHECK_RATIO: f64 = 0.75;
let duration_until_next_checkpoint = { let duration_until_deadline = deadline - current_time;
duration_until_deadline.mul_f64(DEADLINE_CHECK_RATIO)
};
let avg_iter_duration = { let test_elapsed = current_time - self.test_start_time;
test_elapsed.div_f64(self.completed_iter as f64)
};
let iter_until_next_checkpoint = { let x = Self::div_duration_f64(duration_until_next_checkpoint, avg_iter_duration) as u64;
u64::max(x, 1)
};
self.checkpoint += iter_until_next_checkpoint;
}
// This is equivalent to `Duration::div_duration_f64`, but that is not stable on Rust 1.76 fn div_duration_f64(lhs: Duration, rhs: Duration) -> f64 { const NANOS_PER_SEC: u32 = 1_000_000_000; let lhs_nanos =
(lhs.as_secs() as f64) * (NANOS_PER_SEC as f64) + (lhs.subsec_nanos() as f64); let rhs_nanos =
(rhs.as_secs() as f64) * (NANOS_PER_SEC as f64) + (rhs.subsec_nanos() as f64);
lhs_nanos / rhs_nanos
}
}
#[cfg(windows)] mod windows { use { crate::{prelude::*, MemLockGuard},
std::mem::{size_of, size_of_val},
windows::Win32::{
Foundation::ERROR_WORKING_SET_QUOTA,
System::{
Memory::{VirtualLock, VirtualUnlock},
SystemInformation::{
GetNativeSystemInfo, GlobalMemoryStatusEx, MEMORYSTATUSEX, SYSTEM_INFO,
},
Threading::{
GetCurrentProcess, GetProcessWorkingSetSize, SetProcessWorkingSetSize,
},
},
},
};
// TODO: Consider verifying that the process memory is properly sized by using // `GetProcessMemoryInfo` during memtests to retrieve the number of page faults this process is // causing. If it's suddenly a very high number, it indicates the set size might be too small pub(super) fn replace_set_size(memsize: usize) -> anyhow::Result<WorkingSetResizeGuard> { const ESTIMATED_TEST_MEM_USAGE: usize = 1024 * 1024; // 1MiB let (min_set_size, max_set_size) = get_set_size()?; let new_min_set_size = memsize + ESTIMATED_TEST_MEM_USAGE; let new_max_set_size: usize = get_physical_memory_size()
.context("Failed to get physical memory size")?
.try_into()
.unwrap_or(usize::MAX); unsafe {
SetProcessWorkingSetSize(GetCurrentProcess(), new_min_set_size, new_max_set_size)
.context("Failed to set process working set size")?;
}
Ok(WorkingSetResizeGuard {
min_set_size,
max_set_size,
})
}
impl Drop for WorkingSetResizeGuard { fn drop(&mutself) { unsafe { iflet Err(e) = SetProcessWorkingSetSize(
GetCurrentProcess(), self.min_set_size, self.max_set_size,
) {
warn!("Failed to restore process working set: {e}");
}
}
}
}
pub(super) fn memory_lock(
memory: &mut [usize],
) -> anyhow::Result<(&mut [usize], MemLockGuard)> { let base_ptr = memory.as_mut_ptr(); let mem_size = size_of_val(memory);
pub(super) fn memory_resize_and_lock( mut memory: &mut [usize],
) -> anyhow::Result<(&mut [usize], MemLockGuard)> { // Resizing to system limit first is more efficient than only decrementing by page // size and retry locking. let min_set_size_usize = get_set_size()?.0 / size_of::<usize>(); if memory.len() > min_set_size_usize {
memory = &mut memory[0..min_set_size_usize];
warn!( "Resized memory to system limit ({} bytes)",
size_of_val(memory)
);
}
let usize_per_page = get_page_size()? / std::mem::size_of::<usize>(); loop { let base_ptr = memory.as_mut_ptr(); let mem_size = size_of_val(memory);
let res = unsafe { VirtualLock(base_ptr.cast(), mem_size) }; let Err(e) = res else {
info!("Successfully locked {} bytes", mem_size); return Ok((memory, MemLockGuard { base_ptr, mem_size }));
};
ensure!(
e == ERROR_WORKING_SET_QUOTA.into(),
anyhow!(e).context("VirtualLock failed")
);
// Locking with the system limit can still fail as the memory to be locked may not be // page aligned. In that case retry locking after decrement memory size by a page. let new_len = memory
.len()
.checked_sub(usize_per_page)
.context("Failed to lock any memory, memory size has been decremented to 0")?;
pub(super) fn memory_resize_and_lock( mut memory: &mut [usize],
) -> anyhow::Result<(&mut [usize], MemLockGuard)> { // Note: Resizing to system limit first is more efficient than only decrementing by page // size and retry locking, but this may not work as intended when running as a priviledged // process, since priviledged processes do not need to respect the limit. let max_mem_lock_usize = get_max_mem_lock()? / size_of::<usize>(); if memory.len() > max_mem_lock_usize {
memory = &mut memory[0..max_mem_lock_usize];
warn!( "Resized memory to system limit ({} bytes)",
size_of_val(memory)
);
}
let e = io::Error::last_os_error();
ensure!(
e.kind() == ErrorKind::OutOfMemory,
anyhow!(e).context("mlock failed")
);
// Locking with the system limit can still fail as the memory to be locked may not be // page aligned. In that case retry locking after decrement memory size by a page. let new_len = memory
.len()
.checked_sub(usize_per_page)
.context("Failed to lock any memory, memory size has been decremented to 0")?;
memory = &mut memory[0..new_len];
warn!( "Decremented memory size to {} bytes, retry memory locking",
size_of_val(memory)
);
}
}
impl Drop for MemLockGuard { fn drop(&mutself) { unsafe { if munlock(self.base_ptr.cast(), self.mem_size) != 0 {
warn!("Failed to unlock memory: {}", io::Error::last_os_error())
}
}
}
}
impl TestObserver for RuntimeChecker { type Error = RuntimeError;
/// This function should be called in the beginning of a memtest. fn init(&mutself, expected_iter: u64) { self.timeout_checker.init(expected_iter); self.page_fault_checker.init(expected_iter);
}
/// Calculate the remaining time before the deadline and schedule the next check at 75% of that /// interval, then estimate the number of iterations to get there and set as next checkpoint fn set_next_checkpoint(&mutself) { self.checkpoint += self.checkpoint_step;
}
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.