1use serde::{Deserialize, Serialize};
2
3use crate::discovery::{CapacitySnapshot, MemoryProbe, SystemMemoryProbe};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum CapacitySource {
9 OperatingSystem,
11 Cgroup,
13 Nvml,
15 Drm,
17 Dxgi,
19 Metal,
21 User,
23 Adaptive,
25}
26
27#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum MemoryResourceKind {
31 Host,
33 Device,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
39pub struct DeviceIdentity {
40 pub adapter_index: usize,
42 pub vendor_id: u32,
44 pub device_id: u32,
46 pub pci_bus_id: String,
48}
49
50#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52pub struct MemoryResource {
53 pub id: String,
55 pub name: String,
57 pub kind: MemoryResourceKind,
59 pub total_bytes: Option<u64>,
61 pub available_bytes: Option<u64>,
63 pub capacity_source: CapacitySource,
65 pub device_identity: Option<DeviceIdentity>,
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub(crate) enum ResourceValidationError {
71 EmptyId,
72 AvailableExceedsTotal,
73 HostHasDeviceIdentity,
74 HostHasDeviceTelemetry,
75 DeviceHasHostTelemetry,
76 TelemetryHasNoCapacity,
77}
78
79impl MemoryResource {
80 fn adaptive_device(id: impl Into<String>, name: impl Into<String>) -> Self {
81 Self {
82 id: id.into(),
83 name: name.into(),
84 kind: MemoryResourceKind::Device,
85 total_bytes: None,
86 available_bytes: None,
87 capacity_source: CapacitySource::Adaptive,
88 device_identity: None,
89 }
90 }
91
92 pub(crate) fn discover_device(
93 id: impl Into<String>,
94 name: impl Into<String>,
95 identity: DeviceIdentity,
96 fallback_bytes: u64,
97 ) -> Self {
98 let mut resource = Self::adaptive_device(id, name);
99 resource.device_identity = Some(identity);
100 if let Ok(snapshot) = SystemMemoryProbe.probe_device(&resource) {
101 resource.apply_capacity_snapshot(snapshot);
102 return resource;
103 }
104 resource.total_bytes = Some(fallback_bytes);
105 resource.available_bytes = Some(fallback_bytes);
106 resource
107 }
108
109 pub(crate) fn user_device(
110 id: impl Into<String>,
111 name: impl Into<String>,
112 total_bytes: u64,
113 available_bytes: Option<u64>,
114 ) -> Self {
115 Self {
116 id: id.into(),
117 name: name.into(),
118 kind: MemoryResourceKind::Device,
119 total_bytes: Some(total_bytes),
120 available_bytes: Some(available_bytes.unwrap_or(total_bytes).min(total_bytes)),
121 capacity_source: CapacitySource::User,
122 device_identity: None,
123 }
124 }
125
126 pub(crate) fn apply_capacity_snapshot(&mut self, snapshot: CapacitySnapshot) {
127 self.total_bytes = Some(snapshot.total_bytes);
128 self.available_bytes = Some(snapshot.available_bytes);
129 self.capacity_source = snapshot.source;
130 }
131
132 pub(crate) fn validate(&self) -> Result<(), ResourceValidationError> {
133 if self.id.is_empty() {
134 return Err(ResourceValidationError::EmptyId);
135 }
136 if let (Some(total), Some(available)) = (self.total_bytes, self.available_bytes)
137 && available > total
138 {
139 return Err(ResourceValidationError::AvailableExceedsTotal);
140 }
141 match self.kind {
142 MemoryResourceKind::Host => {
143 if self.device_identity.is_some() {
144 return Err(ResourceValidationError::HostHasDeviceIdentity);
145 }
146 if matches!(
147 self.capacity_source,
148 CapacitySource::Nvml
149 | CapacitySource::Drm
150 | CapacitySource::Dxgi
151 | CapacitySource::Metal
152 | CapacitySource::Adaptive
153 ) {
154 return Err(ResourceValidationError::HostHasDeviceTelemetry);
155 }
156 }
157 MemoryResourceKind::Device => {
158 if matches!(
159 self.capacity_source,
160 CapacitySource::OperatingSystem | CapacitySource::Cgroup
161 ) {
162 return Err(ResourceValidationError::DeviceHasHostTelemetry);
163 }
164 }
165 }
166 if self.capacity_source != CapacitySource::Adaptive
167 && (self.total_bytes.is_none() || self.available_bytes.is_none())
168 {
169 return Err(ResourceValidationError::TelemetryHasNoCapacity);
170 }
171 Ok(())
172 }
173
174 pub(crate) fn normalize_capacity(&mut self) {
175 if let Some(total) = self.total_bytes
176 && let Some(available) = self.available_bytes.as_mut()
177 {
178 *available = (*available).min(total);
179 }
180 }
181
182 pub(crate) fn effective_available(&self) -> u64 {
183 self.available_bytes
184 .or(self.total_bytes)
185 .unwrap_or(u64::MAX)
186 }
187
188 pub(crate) fn is_refreshable(&self) -> bool {
189 self.kind == MemoryResourceKind::Device
190 && self.capacity_source != CapacitySource::User
191 && self.device_identity.is_some()
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::discovery::discover_host;
199
200 fn device() -> MemoryResource {
201 MemoryResource {
202 id: "test".into(),
203 name: "Test".into(),
204 kind: MemoryResourceKind::Device,
205 total_bytes: Some(1_000),
206 available_bytes: Some(500),
207 capacity_source: CapacitySource::User,
208 device_identity: None,
209 }
210 }
211
212 #[test]
213 fn validation_names_invalid_field_combinations() {
214 let mut invalid = device();
215 invalid.id.clear();
216 assert_eq!(invalid.validate(), Err(ResourceValidationError::EmptyId));
217 let mut invalid = device();
218 invalid.available_bytes = Some(1_001);
219 assert_eq!(
220 invalid.validate(),
221 Err(ResourceValidationError::AvailableExceedsTotal)
222 );
223 let mut invalid = discover_host();
224 invalid.device_identity = Some(DeviceIdentity {
225 adapter_index: 0,
226 vendor_id: 1,
227 device_id: 2,
228 pci_bus_id: "bus-0".into(),
229 });
230 assert_eq!(
231 invalid.validate(),
232 Err(ResourceValidationError::HostHasDeviceIdentity)
233 );
234 let mut invalid = device();
235 invalid.capacity_source = CapacitySource::OperatingSystem;
236 assert_eq!(
237 invalid.validate(),
238 Err(ResourceValidationError::DeviceHasHostTelemetry)
239 );
240
241 let mut invalid = device();
242 invalid.capacity_source = CapacitySource::Nvml;
243 invalid.total_bytes = None;
244 assert_eq!(
245 invalid.validate(),
246 Err(ResourceValidationError::TelemetryHasNoCapacity)
247 );
248
249 let mut invalid = discover_host();
250 invalid.capacity_source = CapacitySource::Metal;
251 assert_eq!(
252 invalid.validate(),
253 Err(ResourceValidationError::HostHasDeviceTelemetry)
254 );
255 }
256}