Skip to main content

guest_cli/
balloon.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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
107// Constants from zircon/system/ulib/virtio/include/virtio/balloon.h
108const 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; // Available memory as in /proc
115const VIRTIO_BALLOON_S_CACHES: u16 = 7; // Disk caches
116const VIRTIO_BALLOON_S_HTLB_PGALLOC: u16 = 8; // HugeTLB page allocations
117const VIRTIO_BALLOON_S_HTLB_PGFAIL: u16 = 9; // HugeTLB page allocation failures
118
119#[allow(clippy::result_large_err)] // TODO(https://fxbug.dev/401253790)
120pub 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    // The device isn't in a good state to query stats. Trying again may succeed.
176    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}