1use crate::platform::PlatformServices;
6use fidl_fuchsia_virtualization::{
7 BalloonControllerMarker, BalloonControllerProxy, GuestMarker, GuestStatus,
8};
9use guest_cli_args as arguments;
10use prettytable::format::consts::FORMAT_CLEAN;
11use prettytable::{Table, cell, row};
12use std::fmt;
13
14#[derive(Default, serde::Serialize, serde::Deserialize)]
15pub struct BalloonStats {
16 current_pages: Option<u32>,
17 requested_pages: Option<u32>,
18 swap_in: Option<u64>,
19 swap_out: Option<u64>,
20 major_faults: Option<u64>,
21 minor_faults: Option<u64>,
22 hugetlb_allocs: Option<u64>,
23 hugetlb_failures: Option<u64>,
24 free_memory: Option<u64>,
25 total_memory: Option<u64>,
26 available_memory: Option<u64>,
27 disk_caches: Option<u64>,
28}
29
30#[derive(serde::Serialize, serde::Deserialize)]
31pub enum BalloonResult {
32 Stats(BalloonStats),
33 SetComplete(u32),
34 NotRunning,
35 NoBalloonDevice,
36 Internal(String),
37}
38
39impl fmt::Display for BalloonResult {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 BalloonResult::Stats(stats) => {
43 let mut table = Table::new();
44 table.set_format(*FORMAT_CLEAN);
45
46 table.add_row(row![
47 "current-pages:",
48 stats.current_pages.map_or_else(|| "UNKNOWN".to_string(), |i| i.to_string())
49 ]);
50 table.add_row(row![
51 "requested-pages:",
52 stats.requested_pages.map_or_else(|| "UNKNOWN".to_string(), |i| i.to_string())
53 ]);
54 table.add_row(row![
55 "swap-in:",
56 stats.swap_in.map_or_else(|| "UNKNOWN".to_string(), |i| i.to_string())
57 ]);
58 table.add_row(row![
59 "swap-out:",
60 stats.swap_out.map_or_else(|| "UNKNOWN".to_string(), |i| i.to_string())
61 ]);
62 table.add_row(row![
63 "major-faults:",
64 stats.major_faults.map_or_else(|| "UNKNOWN".to_string(), |i| i.to_string())
65 ]);
66 table.add_row(row![
67 "minor-faults:",
68 stats.minor_faults.map_or_else(|| "UNKNOWN".to_string(), |i| i.to_string())
69 ]);
70 table.add_row(row![
71 "hugetlb-allocations:",
72 stats.hugetlb_allocs.map_or_else(|| "UNKNOWN".to_string(), |i| i.to_string())
73 ]);
74 table.add_row(row![
75 "hugetlb-failures:",
76 stats.hugetlb_failures.map_or_else(|| "UNKNOWN".to_string(), |i| i.to_string())
77 ]);
78 table.add_row(row![
79 "free-memory:",
80 stats.free_memory.map_or_else(|| "UNKNOWN".to_string(), |i| i.to_string())
81 ]);
82 table.add_row(row![
83 "total-memory:",
84 stats.total_memory.map_or_else(|| "UNKNOWN".to_string(), |i| i.to_string())
85 ]);
86 table.add_row(row![
87 "available-memory:",
88 stats.available_memory.map_or_else(|| "UNKNOWN".to_string(), |i| i.to_string())
89 ]);
90 table.add_row(row![
91 "disk-caches:",
92 stats.disk_caches.map_or_else(|| "UNKNOWN".to_string(), |i| i.to_string())
93 ]);
94
95 write!(f, "{}", table)
96 }
97 BalloonResult::SetComplete(pages) => {
98 write!(f, "Resizing memory balloon to {} pages!", pages)
99 }
100 BalloonResult::NotRunning => write!(f, "The guest is not running"),
101 BalloonResult::NoBalloonDevice => write!(f, "The guest has no balloon device"),
102 BalloonResult::Internal(err) => write!(f, "Internal failure: {}", err),
103 }
104 }
105}
106
107const VIRTIO_BALLOON_S_SWAP_IN: u16 = 0;
109const VIRTIO_BALLOON_S_SWAP_OUT: u16 = 1;
110const VIRTIO_BALLOON_S_MAJFLT: u16 = 2;
111const VIRTIO_BALLOON_S_MINFLT: u16 = 3;
112const VIRTIO_BALLOON_S_MEMFREE: u16 = 4;
113const VIRTIO_BALLOON_S_MEMTOT: u16 = 5;
114const VIRTIO_BALLOON_S_AVAIL: u16 = 6; const VIRTIO_BALLOON_S_CACHES: u16 = 7; const VIRTIO_BALLOON_S_HTLB_PGALLOC: u16 = 8; const VIRTIO_BALLOON_S_HTLB_PGFAIL: u16 = 9; #[allow(clippy::result_large_err)] pub async fn connect_to_balloon_controller<P: PlatformServices>(
121 services: &P,
122 guest_type: arguments::GuestType,
123) -> Result<BalloonControllerProxy, BalloonResult> {
124 let guest_manager = services
125 .connect_to_manager(guest_type)
126 .await
127 .map_err(|err| BalloonResult::Internal(format!("failed to connect to manager: {}", err)))?;
128
129 let guest_info = guest_manager
130 .get_info()
131 .await
132 .map_err(|err| BalloonResult::Internal(format!("failed to get guest info: {}", err)))?;
133 let status = guest_info.guest_status.expect("guest status should always be set");
134 if status != GuestStatus::Starting && status != GuestStatus::Running {
135 return Err(BalloonResult::NotRunning);
136 }
137
138 let (guest_endpoint, guest_server_end) = fidl::endpoints::create_proxy::<GuestMarker>();
139 guest_manager
140 .connect(guest_server_end)
141 .await
142 .map_err(|err| BalloonResult::Internal(format!("failed to send msg: {:?}", err)))?
143 .map_err(|err| BalloonResult::Internal(format!("failed to connect: {:?}", err)))?;
144
145 let (balloon_controller, balloon_server_end) =
146 fidl::endpoints::create_proxy::<BalloonControllerMarker>();
147 guest_endpoint
148 .get_balloon_controller(balloon_server_end)
149 .await
150 .map_err(|err| BalloonResult::Internal(format!("failed to send msg: {:?}", err)))?
151 .map_err(|_| BalloonResult::NoBalloonDevice)?;
152
153 Ok(balloon_controller)
154}
155
156fn handle_balloon_set(balloon_controller: BalloonControllerProxy, num_pages: u32) -> BalloonResult {
157 if let Err(err) = balloon_controller.request_num_pages(num_pages) {
158 BalloonResult::Internal(format!("failed to request pages: {:?}", err))
159 } else {
160 BalloonResult::SetComplete(num_pages)
161 }
162}
163
164async fn handle_balloon_stats(balloon_controller: BalloonControllerProxy) -> BalloonResult {
165 let result = balloon_controller.get_balloon_size().await;
166 let Ok((current_num_pages, requested_num_pages)) = result else {
167 return BalloonResult::Internal(format!("failed to send msg: {:?}", result.unwrap_err()));
168 };
169
170 let result = balloon_controller.get_mem_stats().await;
171 let Ok((status, mem_stats)) = result else {
172 return BalloonResult::Internal(format!("failed to send msg: {:?}", result.unwrap_err()));
173 };
174
175 if mem_stats.is_none() {
177 return BalloonResult::Internal(format!("failed to query stats: {}", status));
178 }
179
180 let mut stats = BalloonStats {
181 current_pages: Some(current_num_pages),
182 requested_pages: Some(requested_num_pages),
183 ..BalloonStats::default()
184 };
185
186 for stat in mem_stats.unwrap() {
187 match stat.tag {
188 VIRTIO_BALLOON_S_SWAP_IN => stats.swap_in = Some(stat.val),
189 VIRTIO_BALLOON_S_SWAP_OUT => stats.swap_out = Some(stat.val),
190 VIRTIO_BALLOON_S_MAJFLT => stats.major_faults = Some(stat.val),
191 VIRTIO_BALLOON_S_MINFLT => stats.minor_faults = Some(stat.val),
192 VIRTIO_BALLOON_S_MEMFREE => stats.free_memory = Some(stat.val),
193 VIRTIO_BALLOON_S_MEMTOT => stats.total_memory = Some(stat.val),
194 VIRTIO_BALLOON_S_AVAIL => stats.available_memory = Some(stat.val),
195 VIRTIO_BALLOON_S_CACHES => stats.disk_caches = Some(stat.val),
196 VIRTIO_BALLOON_S_HTLB_PGALLOC => stats.hugetlb_allocs = Some(stat.val),
197 VIRTIO_BALLOON_S_HTLB_PGFAIL => stats.hugetlb_failures = Some(stat.val),
198 tag => println!("unrecognized tag: {}", tag),
199 }
200 }
201
202 BalloonResult::Stats(stats)
203}
204
205pub async fn handle_balloon<P: PlatformServices>(
206 services: &P,
207 args: &arguments::balloon_args::BalloonArgs,
208) -> BalloonResult {
209 match &args.balloon_cmd {
210 arguments::balloon_args::BalloonCommands::Set(args) => {
211 let controller = match connect_to_balloon_controller(services, args.guest_type).await {
212 Ok(controller) => controller,
213 Err(result) => {
214 return result;
215 }
216 };
217
218 handle_balloon_set(controller, args.num_pages)
219 }
220 arguments::balloon_args::BalloonCommands::Stats(args) => {
221 let controller = match connect_to_balloon_controller(services, args.guest_type).await {
222 Ok(controller) => controller,
223 Err(result) => {
224 return result;
225 }
226 };
227
228 handle_balloon_stats(controller).await
229 }
230 }
231}
232
233#[cfg(test)]
234mod test {
235 use super::*;
236 use fidl::endpoints::{ControlHandle, RequestStream, create_proxy_and_stream};
237 use fidl_fuchsia_virtualization::MemStat;
238 use fuchsia_async as fasync;
239 use futures::StreamExt;
240 use zx_status;
241
242 #[fasync::run_until_stalled(test)]
243 async fn balloon_valid_page_num_returns_ok() {
244 let (proxy, mut stream) = create_proxy_and_stream::<BalloonControllerMarker>();
245 let expected_string = "Resizing memory balloon to 0 pages!";
246
247 let result = handle_balloon_set(proxy, 0);
248 let _ = stream
249 .next()
250 .await
251 .expect("Failed to read from stream")
252 .expect("Failed to parse request")
253 .into_request_num_pages()
254 .expect("Unexpected call to Balloon Controller");
255
256 assert_eq!(result.to_string(), expected_string);
257 }
258
259 #[fasync::run_until_stalled(test)]
260 async fn balloon_stats_server_shut_down_returns_err() {
261 let (proxy, mut stream) = create_proxy_and_stream::<BalloonControllerMarker>();
262 let _task = fasync::Task::spawn(async move {
263 let _ = stream
264 .next()
265 .await
266 .expect("Failed to read from stream")
267 .expect("Failed to parse request")
268 .into_get_balloon_size()
269 .expect("Unexpected call to Balloon Controller");
270 stream.control_handle().shutdown();
271 });
272
273 let result = handle_balloon_stats(proxy).await;
274 assert_eq!(
275 std::mem::discriminant(&result),
276 std::mem::discriminant(&BalloonResult::Internal(String::new()))
277 );
278 }
279
280 #[fasync::run_until_stalled(test)]
281 async fn balloon_stats_empty_input_returns_err() {
282 let (proxy, mut stream) = create_proxy_and_stream::<BalloonControllerMarker>();
283
284 let _task = fasync::Task::spawn(async move {
285 let get_balloon_size_responder = stream
286 .next()
287 .await
288 .expect("Failed to read from stream")
289 .expect("Failed to parse request")
290 .into_get_balloon_size()
291 .expect("Unexpected call to Balloon Controller");
292 get_balloon_size_responder.send(0, 0).expect("Failed to send request to proxy");
293
294 let get_mem_stats_responder = stream
295 .next()
296 .await
297 .expect("Failed to read from stream")
298 .expect("Failed to parse request")
299 .into_get_mem_stats()
300 .expect("Unexpected call to Balloon Controller");
301 get_mem_stats_responder
302 .send(zx_status::Status::INTERNAL.into_raw(), None)
303 .expect("Failed to send request to proxy");
304 });
305
306 let result = handle_balloon_stats(proxy).await;
307 assert_eq!(
308 std::mem::discriminant(&result),
309 std::mem::discriminant(&BalloonResult::Internal(String::new()))
310 );
311 }
312
313 #[fasync::run_until_stalled(test)]
314 async fn balloon_stats_valid_input_returns_valid_string() {
315 let test_stats = [
316 MemStat { tag: VIRTIO_BALLOON_S_SWAP_IN, val: 2 },
317 MemStat { tag: VIRTIO_BALLOON_S_SWAP_OUT, val: 3 },
318 MemStat { tag: VIRTIO_BALLOON_S_MAJFLT, val: 4 },
319 MemStat { tag: VIRTIO_BALLOON_S_MINFLT, val: 5 },
320 MemStat { tag: VIRTIO_BALLOON_S_MEMFREE, val: 6 },
321 MemStat { tag: VIRTIO_BALLOON_S_MEMTOT, val: 7 },
322 MemStat { tag: VIRTIO_BALLOON_S_AVAIL, val: 8 },
323 MemStat { tag: VIRTIO_BALLOON_S_CACHES, val: 9 },
324 MemStat { tag: VIRTIO_BALLOON_S_HTLB_PGALLOC, val: 10 },
325 MemStat { tag: VIRTIO_BALLOON_S_HTLB_PGFAIL, val: 11 },
326 ];
327
328 let current_num_pages = 6;
329 let requested_num_pages = 8;
330 let (proxy, mut stream) = create_proxy_and_stream::<BalloonControllerMarker>();
331 let _task = fasync::Task::spawn(async move {
332 let get_balloon_size_responder = stream
333 .next()
334 .await
335 .expect("Failed to read from stream")
336 .expect("Failed to parse request")
337 .into_get_balloon_size()
338 .expect("Unexpected call to Balloon Controller");
339 get_balloon_size_responder
340 .send(current_num_pages, requested_num_pages)
341 .expect("Failed to send request to proxy");
342
343 let get_mem_stats_responder = stream
344 .next()
345 .await
346 .expect("Failed to read from stream")
347 .expect("Failed to parse request")
348 .into_get_mem_stats()
349 .expect("Unexpected call to Balloon Controller");
350 get_mem_stats_responder
351 .send(0, Some(&test_stats))
352 .expect("Failed to send request to proxy");
353 });
354
355 let result = handle_balloon_stats(proxy).await;
356 assert_eq!(
357 result.to_string(),
358 concat!(
359 " current-pages: 6 \n",
360 " requested-pages: 8 \n",
361 " swap-in: 2 \n",
362 " swap-out: 3 \n",
363 " major-faults: 4 \n",
364 " minor-faults: 5 \n",
365 " hugetlb-allocations: 10 \n",
366 " hugetlb-failures: 11 \n",
367 " free-memory: 6 \n",
368 " total-memory: 7 \n",
369 " available-memory: 8 \n",
370 " disk-caches: 9 \n",
371 )
372 );
373 }
374}