1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! Host RAM detection and guest-memory recommendations.
use std::num::NonZero;
use sysinfo::System;
use super::{ByteSize, Mib};
use crate::{Error, Result};
/// A nonzero total host RAM capacity.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct HostMemory(NonZero<u64>);
impl HostMemory {
/// Minimum default guest size, even when the host's quarter-share is smaller.
const MIN_GUEST_MEMORY: ByteSize =
ByteSize::checked_from_mib(512).expect("512 MiB fits in a u64 byte count");
/// Maximum default guest size, regardless of host capacity.
const MAX_GUEST_MEMORY: ByteSize =
ByteSize::checked_from_gib(8).expect("8 GiB fits in a u64 byte count");
/// Use one quarter of total host RAM as the initial guest budget.
const GUEST_MEMORY_DIVISOR: u64 = 4;
/// Detects total host RAM, failing if sysinfo does not support the host or
/// the host reports no memory.
pub(super) fn detect() -> Result<Self> {
// sysinfo fails here only on platforms it does not support; its error
// says no more than that, so it maps to the same variant as zero memory.
let mut system = System::new().map_err(|_| Error::HostMemory)?;
system.refresh_memory();
Self::from_size(ByteSize::from_bytes(system.total_memory()))
}
/// Wraps a reported capacity, rejecting zero as unavailable host memory.
pub(super) fn from_size(size: ByteSize) -> Result<Self> {
NonZero::new(size.as_bytes())
.map(Self)
.ok_or(Error::HostMemory)
}
/// Returns total host capacity, without applying guest sizing policy.
pub(super) fn total(self) -> ByteSize {
ByteSize::from_bytes(self.0.get())
}
/// Recommends a default guest RAM size from total host capacity.
///
/// # Policy
///
/// 1. Start with one quarter of total host RAM, leaving capacity for the host
/// and other workloads.
/// 2. Clamp that budget to a minimum of 512 MiB and a maximum of 8 GiB
/// (8192 MiB). These are default-sizing choices, not limits on explicitly
/// configured guest memory.
/// 3. Round down to whole MiB for the configuration value.
///
/// For example, 1 GiB of host RAM suggests 512 MiB, 16 GiB suggests 4096 MiB,
/// and 64 GiB suggests 8192 MiB.
///
/// # Limitations
///
/// This uses total RAM, not currently free or available RAM. It neither
/// allocates memory nor accounts for other running guests, so the result
/// does not guarantee that sufficient memory is available.
///
/// The minimum overrides the quarter-share on hosts below 2 GiB and can
/// even exceed total host RAM on hosts below 512 MiB.
///
/// # Errors
///
/// Returns [`Error::GuestMemorySize`] if the recommendation cannot be
/// represented as a `u32` MiB count. The current 8 GiB cap prevents this,
/// but the conversion remains checked if the sizing policy changes.
pub(super) fn suggested(self) -> Result<Mib> {
let memory = ByteSize::from_bytes(self.total().as_bytes() / Self::GUEST_MEMORY_DIVISOR)
.clamp(Self::MIN_GUEST_MEMORY, Self::MAX_GUEST_MEMORY);
Mib::try_from(memory).map_err(Error::GuestMemorySize)
}
}
#[cfg(test)]
mod tests {
use super::{HostMemory, Mib};
use crate::{ByteSize, Error};
#[test]
fn zero_reported_memory_is_unavailable() {
assert!(matches!(
HostMemory::from_size(ByteSize::from_bytes(0)),
Err(Error::HostMemory)
));
}
#[test]
fn reported_memory_preserves_positive_byte_counts() {
for bytes in [1, 1024, 17_179_869_184, u64::MAX] {
let memory = HostMemory::from_size(ByteSize::from_bytes(bytes)).unwrap();
assert_eq!(memory.total().as_bytes(), bytes);
}
}
#[test]
fn reserves_capacity_and_caps_large_hosts() {
for (host_mib, expected_mib) in [
(256, 512),
(1024, 512),
(2048, 512),
(4096, 1024),
(16384, 4096),
(32768, 8192),
(262_144, 8192),
] {
let memory =
HostMemory::from_size(ByteSize::checked_from_mib(host_mib).unwrap()).unwrap();
assert_eq!(memory.suggested().unwrap(), Mib::new(expected_mib));
}
}
#[test]
fn rounds_down_at_byte_boundaries_and_keeps_limits() {
let smallest = HostMemory::from_size(ByteSize::from_bytes(1)).unwrap();
assert_eq!(smallest.suggested().unwrap(), Mib::new(512));
for (host_mib, before, at) in [
(2048, 512, 512),
(2052, 512, 513),
(32768, 8191, 8192),
(32772, 8192, 8192),
] {
let bytes = ByteSize::checked_from_mib(host_mib).unwrap().as_bytes();
for (bytes, expected) in [(bytes - 1, before), (bytes, at), (bytes + 1, at)] {
let memory = HostMemory::from_size(ByteSize::from_bytes(bytes)).unwrap();
assert_eq!(memory.suggested().unwrap(), Mib::new(expected));
}
}
}
#[test]
fn does_not_overflow_on_extreme_capacity() {
let memory = HostMemory::from_size(ByteSize::from_bytes(u64::MAX)).unwrap();
assert_eq!(memory.suggested().unwrap(), Mib::new(8192));
}
}