Skip to main content

hcl/
stats.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Code for getting kernel stats from the `mshv_vtl` driver.
5
6use thiserror::Error;
7
8/// Per-CPU VTL transition counts, exposed by `mshv_vtl` as a sysfs attribute on
9/// the `mshv_vtl_low` misc device.
10const VTL_TRANSITIONS_PATH: &str = "/sys/class/misc/mshv_vtl_low/mshv_vtl_transitions";
11
12/// Error returned by [`vp_stats`].
13#[derive(Debug, Error)]
14#[expect(missing_docs)]
15pub enum VpStatsError {
16    #[error("failed to read {VTL_TRANSITIONS_PATH}")]
17    Read(#[source] std::io::Error),
18    #[error("stats are not utf-8")]
19    NotUtf8(#[source] std::str::Utf8Error),
20    #[error("stats are missing the expected header line")]
21    MissingHeader,
22    #[error("failed to parse stats line")]
23    ParseLine,
24}
25
26/// The per-VP stats from the kernel.
27#[derive(Debug, Clone)]
28pub struct HclVpStats {
29    /// The number of VTL transitions.
30    ///
31    /// Note that the kernel only counts transitions that it hands back to
32    /// userspace or handles via the generic intercept path. On TDX every exit
33    /// is either handled in the kernel or returned early, so this never
34    /// increments; on SNP, exits handled entirely in the kernel are not
35    /// counted.
36    pub vtl_transitions: u64,
37}
38
39/// Gets the per-VP stats from the kernel, indexed by Linux CPU number.
40///
41/// The kernel only reports online CPUs, so entries for offline CPUs (e.g. ones
42/// still managed by sidecar) are `None`.
43pub fn vp_stats() -> Result<Vec<Option<HclVpStats>>, VpStatsError> {
44    let data = std::fs::read(VTL_TRANSITIONS_PATH).map_err(VpStatsError::Read)?;
45    let data = std::str::from_utf8(&data).map_err(VpStatsError::NotUtf8)?;
46
47    // The kernel caps sysfs output at one page and silently truncates the last
48    // line, so only consider newline-terminated lines. This means CPUs past the
49    // cutoff are missing entirely, which happens somewhere north of 200 CPUs.
50    let complete = &data[..data.rfind('\n').map_or(0, |i| i + 1)];
51    let mut lines = complete.lines();
52    if !lines.next().is_some_and(|l| l.starts_with("cpu#")) {
53        return Err(VpStatsError::MissingHeader);
54    }
55    let mut stats = Vec::new();
56    for line in lines {
57        let (cpu, rest) = line.split_once(' ').ok_or(VpStatsError::ParseLine)?;
58        let n: usize = cpu
59            .strip_prefix("cpu")
60            .ok_or(VpStatsError::ParseLine)?
61            .parse()
62            .map_err(|_| VpStatsError::ParseLine)?;
63
64        let vtl_transitions = rest
65            .split(' ')
66            .next()
67            .ok_or(VpStatsError::ParseLine)?
68            .parse()
69            .map_err(|_| VpStatsError::ParseLine)?;
70
71        if stats.len() <= n {
72            stats.resize_with(n + 1, || None);
73        }
74        stats[n] = Some(HclVpStats { vtl_transitions });
75    }
76    Ok(stats)
77}