Skip to main content

hyperlight_host/metrics/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4// Counter metric that counter number of times a guest error occurred
5pub(crate) static METRIC_GUEST_ERROR: &str = "guest_errors_total";
6pub(crate) static METRIC_GUEST_ERROR_LABEL_CODE: &str = "code";
7
8// Counter metric that counts the number of times a guest function was called due to timing out
9pub(crate) static METRIC_GUEST_CANCELLATION: &str = "guest_cancellations_total";
10
11// Counter metric that counts the number of times a vCPU was erroneously kicked by a stale cancellation
12// This can happen in two scenarios:
13// 1. Linux: A signal from a previous guest call arrives late and interrupts a new call
14// 2. Windows: WHvCancelRunVirtualProcessor is called right after vCPU exits but RUNNING_BIT is still true
15pub(crate) static METRIC_ERRONEOUS_VCPU_KICKS: &str = "erroneous_vcpu_kicks_total";
16
17// Histogram metric that measures the duration of guest function calls
18#[cfg(feature = "function_call_metrics")]
19pub(crate) static METRIC_GUEST_FUNC_DURATION: &str = "guest_call_duration_seconds";
20
21// Histogram metric that measures the duration of host function calls
22#[cfg(feature = "function_call_metrics")]
23pub(crate) static METRIC_HOST_FUNC_DURATION: &str = "host_call_duration_seconds";
24
25/// If the the `function_call_metrics` feature is enabled, this function measures
26/// the time it takes to execute the given closure, and will then emit a guest call metric
27/// with the given function name.
28///
29/// If the feature is not enabled, the given closure is executed without any additional metrics being emitted,
30/// and the result of the closure is returned directly.
31pub(crate) fn maybe_time_and_emit_guest_call<T, F: FnOnce() -> T>(
32    #[allow(unused_variables)] name: &str,
33    f: F,
34) -> T {
35    cfg_if::cfg_if! {
36        if #[cfg(feature = "function_call_metrics")] {
37            use std::time::Instant;
38
39            let start = Instant::now();
40            let result = f();
41            let duration = start.elapsed();
42
43            static LABEL_GUEST_FUNC_NAME: &str = "function_name";
44            metrics::histogram!(METRIC_GUEST_FUNC_DURATION, LABEL_GUEST_FUNC_NAME => name.to_string()).record(duration);
45            result
46        } else {
47            f()
48        }
49    }
50}
51
52/// If the the `function_call_metrics` feature is enabled, this function measures
53/// the time it takes to execute the given closure, and will then emit a host call metric
54/// with the given function name.
55///
56/// If the feature is not enabled, the given closure is executed without any additional metrics being emitted,
57/// and the result of the closure is returned directly.
58pub(crate) fn maybe_time_and_emit_host_call<T, F: FnOnce() -> T>(
59    #[allow(unused_variables)] name: &str,
60    f: F,
61) -> T {
62    cfg_if::cfg_if! {
63        if #[cfg(feature = "function_call_metrics")] {
64            use std::time::Instant;
65
66            let start = Instant::now();
67            let result = f();
68            let duration = start.elapsed();
69
70            static LABEL_HOST_FUNC_NAME: &str = "function_name";
71            metrics::histogram!(METRIC_HOST_FUNC_DURATION, LABEL_HOST_FUNC_NAME => name.to_string()).record(duration);
72            result
73        } else {
74            f()
75        }
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use std::thread;
82    use std::time::Duration;
83
84    use hyperlight_testing::simple_guest_as_pathbuf;
85    use metrics::{Key, with_local_recorder};
86    use metrics_util::CompositeKey;
87
88    use super::*;
89    use crate::SandboxBuilder;
90
91    #[test]
92    fn test_metrics_are_emitted() {
93        let recorder = metrics_util::debugging::DebuggingRecorder::new();
94        let snapshotter = recorder.snapshotter();
95        let snapshot = with_local_recorder(&recorder, || {
96            let mut multi = SandboxBuilder::from_file(simple_guest_as_pathbuf())
97                .build()
98                .unwrap();
99            let interrupt_handle = multi.interrupt_handle();
100
101            // interrupt the guest function call to "Spin" after 1 second
102            let thread = thread::spawn(move || {
103                thread::sleep(Duration::from_secs(1));
104                assert!(interrupt_handle.kill());
105            });
106
107            multi
108                .call::<i32>("PrintOutput", "Hello".to_string())
109                .unwrap();
110
111            multi.call::<i32>("Spin", ()).unwrap_err();
112            thread.join().unwrap();
113
114            snapshotter.snapshot()
115        });
116
117        // Convert snapshot into a hashmap for easier lookup
118        let snapshot = snapshot.into_hashmap();
119
120        cfg_if::cfg_if! {
121            if #[cfg(feature = "function_call_metrics")] {
122                use metrics::Label;
123
124                let expected_num_metrics = 4;
125
126                // Verify that the histogram metrics are recorded correctly
127                assert_eq!(snapshot.len(), expected_num_metrics);
128
129                // 1. Guest call duration
130                let histogram_key = CompositeKey::new(
131                    metrics_util::MetricKind::Histogram,
132                    Key::from_parts(
133                        METRIC_GUEST_FUNC_DURATION,
134                        vec![Label::new("function_name", "PrintOutput")],
135                    ),
136                );
137                let histogram_value = &snapshot.get(&histogram_key).unwrap().2;
138                assert!(
139                    matches!(
140                        histogram_value,
141                        metrics_util::debugging::DebugValue::Histogram(histogram) if histogram.len() == 1
142                    ),
143                    "Histogram metric does not match expected value"
144                );
145
146                // 2. Guest cancellation
147                let counter_key = CompositeKey::new(
148                    metrics_util::MetricKind::Counter,
149                    Key::from_name(METRIC_GUEST_CANCELLATION),
150                );
151                assert_eq!(
152                    snapshot.get(&counter_key).unwrap().2,
153                    metrics_util::debugging::DebugValue::Counter(1)
154                );
155
156                // 3. Guest call duration
157                let histogram_key = CompositeKey::new(
158                    metrics_util::MetricKind::Histogram,
159                    Key::from_parts(
160                        METRIC_GUEST_FUNC_DURATION,
161                        vec![Label::new("function_name", "Spin")],
162                    ),
163                );
164                let histogram_value = &snapshot.get(&histogram_key).unwrap().2;
165                assert!(
166                    matches!(
167                        histogram_value,
168                        metrics_util::debugging::DebugValue::Histogram(histogram) if histogram.len() == 1
169                    ),
170                    "Histogram metric does not match expected value"
171                );
172            } else {
173                // Verify that the counter metrics are recorded correctly
174                assert_eq!(snapshot.len(), 1);
175
176                let counter_key = CompositeKey::new(
177                    metrics_util::MetricKind::Counter,
178                    Key::from_name(METRIC_GUEST_CANCELLATION),
179                );
180                assert_eq!(
181                    snapshot.get(&counter_key).unwrap().2,
182                    metrics_util::debugging::DebugValue::Counter(1)
183                );
184            }
185        }
186    }
187}