hyperlight-host 0.17.0

A lightweight Virtual Machine Manager that can be hosted in an application to safely run untrusted or code within a VM partition with very low latency and overhead.
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2025 The Hyperlight Authors.

// Counter metric that counter number of times a guest error occurred
pub(crate) static METRIC_GUEST_ERROR: &str = "guest_errors_total";
pub(crate) static METRIC_GUEST_ERROR_LABEL_CODE: &str = "code";

// Counter metric that counts the number of times a guest function was called due to timing out
pub(crate) static METRIC_GUEST_CANCELLATION: &str = "guest_cancellations_total";

// Counter metric that counts the number of times a vCPU was erroneously kicked by a stale cancellation
// This can happen in two scenarios:
// 1. Linux: A signal from a previous guest call arrives late and interrupts a new call
// 2. Windows: WHvCancelRunVirtualProcessor is called right after vCPU exits but RUNNING_BIT is still true
pub(crate) static METRIC_ERRONEOUS_VCPU_KICKS: &str = "erroneous_vcpu_kicks_total";

// Histogram metric that measures the duration of guest function calls
#[cfg(feature = "function_call_metrics")]
pub(crate) static METRIC_GUEST_FUNC_DURATION: &str = "guest_call_duration_seconds";

// Histogram metric that measures the duration of host function calls
#[cfg(feature = "function_call_metrics")]
pub(crate) static METRIC_HOST_FUNC_DURATION: &str = "host_call_duration_seconds";

/// If the the `function_call_metrics` feature is enabled, this function measures
/// the time it takes to execute the given closure, and will then emit a guest call metric
/// with the given function name.
///
/// If the feature is not enabled, the given closure is executed without any additional metrics being emitted,
/// and the result of the closure is returned directly.
pub(crate) fn maybe_time_and_emit_guest_call<T, F: FnOnce() -> T>(
    #[allow(unused_variables)] name: &str,
    f: F,
) -> T {
    cfg_if::cfg_if! {
        if #[cfg(feature = "function_call_metrics")] {
            use std::time::Instant;

            let start = Instant::now();
            let result = f();
            let duration = start.elapsed();

            static LABEL_GUEST_FUNC_NAME: &str = "function_name";
            metrics::histogram!(METRIC_GUEST_FUNC_DURATION, LABEL_GUEST_FUNC_NAME => name.to_string()).record(duration);
            result
        } else {
            f()
        }
    }
}

/// If the the `function_call_metrics` feature is enabled, this function measures
/// the time it takes to execute the given closure, and will then emit a host call metric
/// with the given function name.
///
/// If the feature is not enabled, the given closure is executed without any additional metrics being emitted,
/// and the result of the closure is returned directly.
pub(crate) fn maybe_time_and_emit_host_call<T, F: FnOnce() -> T>(
    #[allow(unused_variables)] name: &str,
    f: F,
) -> T {
    cfg_if::cfg_if! {
        if #[cfg(feature = "function_call_metrics")] {
            use std::time::Instant;

            let start = Instant::now();
            let result = f();
            let duration = start.elapsed();

            static LABEL_HOST_FUNC_NAME: &str = "function_name";
            metrics::histogram!(METRIC_HOST_FUNC_DURATION, LABEL_HOST_FUNC_NAME => name.to_string()).record(duration);
            result
        } else {
            f()
        }
    }
}

#[cfg(test)]
mod tests {
    use std::thread;
    use std::time::Duration;

    use hyperlight_testing::simple_guest_as_pathbuf;
    use metrics::{Key, with_local_recorder};
    use metrics_util::CompositeKey;

    use super::*;
    use crate::SandboxBuilder;

    #[test]
    fn test_metrics_are_emitted() {
        let recorder = metrics_util::debugging::DebuggingRecorder::new();
        let snapshotter = recorder.snapshotter();
        let snapshot = with_local_recorder(&recorder, || {
            let mut multi = SandboxBuilder::from_file(simple_guest_as_pathbuf())
                .build()
                .unwrap();
            let interrupt_handle = multi.interrupt_handle();

            // interrupt the guest function call to "Spin" after 1 second
            let thread = thread::spawn(move || {
                thread::sleep(Duration::from_secs(1));
                assert!(interrupt_handle.kill());
            });

            multi
                .call::<i32>("PrintOutput", "Hello".to_string())
                .unwrap();

            multi.call::<i32>("Spin", ()).unwrap_err();
            thread.join().unwrap();

            snapshotter.snapshot()
        });

        // Convert snapshot into a hashmap for easier lookup
        let snapshot = snapshot.into_hashmap();

        cfg_if::cfg_if! {
            if #[cfg(feature = "function_call_metrics")] {
                use metrics::Label;

                let expected_num_metrics = 4;

                // Verify that the histogram metrics are recorded correctly
                assert_eq!(snapshot.len(), expected_num_metrics);

                // 1. Guest call duration
                let histogram_key = CompositeKey::new(
                    metrics_util::MetricKind::Histogram,
                    Key::from_parts(
                        METRIC_GUEST_FUNC_DURATION,
                        vec![Label::new("function_name", "PrintOutput")],
                    ),
                );
                let histogram_value = &snapshot.get(&histogram_key).unwrap().2;
                assert!(
                    matches!(
                        histogram_value,
                        metrics_util::debugging::DebugValue::Histogram(histogram) if histogram.len() == 1
                    ),
                    "Histogram metric does not match expected value"
                );

                // 2. Guest cancellation
                let counter_key = CompositeKey::new(
                    metrics_util::MetricKind::Counter,
                    Key::from_name(METRIC_GUEST_CANCELLATION),
                );
                assert_eq!(
                    snapshot.get(&counter_key).unwrap().2,
                    metrics_util::debugging::DebugValue::Counter(1)
                );

                // 3. Guest call duration
                let histogram_key = CompositeKey::new(
                    metrics_util::MetricKind::Histogram,
                    Key::from_parts(
                        METRIC_GUEST_FUNC_DURATION,
                        vec![Label::new("function_name", "Spin")],
                    ),
                );
                let histogram_value = &snapshot.get(&histogram_key).unwrap().2;
                assert!(
                    matches!(
                        histogram_value,
                        metrics_util::debugging::DebugValue::Histogram(histogram) if histogram.len() == 1
                    ),
                    "Histogram metric does not match expected value"
                );
            } else {
                // Verify that the counter metrics are recorded correctly
                assert_eq!(snapshot.len(), 1);

                let counter_key = CompositeKey::new(
                    metrics_util::MetricKind::Counter,
                    Key::from_name(METRIC_GUEST_CANCELLATION),
                );
                assert_eq!(
                    snapshot.get(&counter_key).unwrap().2,
                    metrics_util::debugging::DebugValue::Counter(1)
                );
            }
        }
    }
}