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