test_realm_helpers/
tracing.rs

1// Copyright 2025 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::trace_runner::{TerminationResult, TraceRunner};
6use anyhow::format_err;
7use log::{info, warn};
8use realm_client::InstalledNamespace;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicUsize, Ordering};
11use std::time::Duration;
12
13static NEXT_TRACING_ID: AtomicUsize = AtomicUsize::new(0);
14static DEFAULT_TRACE_TIMEOUT: Duration = Duration::from_mins(2);
15static DEFAULT_TRACE_FILE_MAX_BYTES: usize = 100 * 1024 * 1024;
16
17/// An RAII-style struct that starts tracing in the test realm upon creation via `Tracing::start`
18/// and collects and writes the trace when the struct is dropped.
19pub struct Tracing {
20    output_trace_path: std::ffi::OsString,
21    tracer: TraceRunner,
22    always_record_trace: bool,
23
24    // Hermetic tracing keeps a reference to the test namespace so the realm factory does
25    // not destroy the trace_manager component before the test drops an instance of Tracing.
26    //
27    // Non-hermetic tracing relies on the trace_manager running on the system. There is no
28    // need to keep a reference to the test namespace in that case since trace_manager will
29    // always outlive the test.
30    _test_ns: Option<Arc<InstalledNamespace>>,
31}
32
33pub(crate) struct HermeticityParameters {
34    pub(crate) service_prefix: String,
35}
36
37pub(crate) enum Hermeticity {
38    NonHermetic,
39    Hermetic(HermeticityParameters),
40}
41
42impl Hermeticity {
43    pub fn new_hermetic(service_prefix: &str) -> Self {
44        Self::Hermetic(HermeticityParameters { service_prefix: service_prefix.to_string() })
45    }
46}
47
48impl Tracing {
49    pub async fn start_non_hermetic(trace_file_prefix: &str) -> Result<Self, anyhow::Error> {
50        Self::start_(
51            trace_file_prefix,
52            Hermeticity::NonHermetic,
53            false,
54            DEFAULT_TRACE_TIMEOUT,
55            DEFAULT_TRACE_FILE_MAX_BYTES,
56            None,
57        )
58        .await
59    }
60
61    pub async fn start_at(test_ns: Arc<InstalledNamespace>) -> Result<Self, anyhow::Error> {
62        let service_prefix = test_ns.prefix().to_owned();
63        Self::start_(
64            service_prefix.strip_prefix("/").ok_or_else(|| {
65                format_err!("Provided service prefix does not start with '/': {service_prefix}")
66            })?,
67            Hermeticity::new_hermetic(&service_prefix),
68            false,
69            DEFAULT_TRACE_TIMEOUT,
70            DEFAULT_TRACE_FILE_MAX_BYTES,
71            Some(test_ns),
72        )
73        .await
74    }
75
76    async fn start_<'a>(
77        trace_file_prefix: &'a str,
78        hermeticity: Hermeticity,
79        always_record_trace: bool,
80        trace_timeout: Duration,
81        trace_file_max_bytes: usize,
82        test_ns: Option<Arc<InstalledNamespace>>,
83    ) -> Result<Self, anyhow::Error> {
84        // The test namespace prefix ensures all generated trace files for a test suite
85        // are unique per test realm.  In case multiple trace files are generated in the
86        // same process, the tracing_id ensures their names will still be unique even
87        // when the same test namespace prefix is used.
88        let tracing_id = NEXT_TRACING_ID.fetch_add(1, Ordering::SeqCst);
89        let output_trace_path = std::path::Path::new(
90            format!("/custom_artifacts/{tracing_id:04}-{trace_file_prefix}-trace.fxt").as_str(),
91        )
92        .to_path_buf();
93
94        let tracer = TraceRunner::start(
95            hermeticity,
96            output_trace_path.clone(),
97            trace_timeout,
98            trace_file_max_bytes,
99        )
100        .await?;
101
102        Ok(Tracing {
103            output_trace_path: output_trace_path.into(),
104            tracer,
105            always_record_trace,
106            _test_ns: test_ns,
107        })
108    }
109}
110
111impl Drop for Tracing {
112    fn drop(&mut self) {
113        let always_record_trace = self.always_record_trace;
114        let output_trace_path = self.output_trace_path.clone();
115
116        match self.tracer.terminate_trace(format!("Tracing::drop")) {
117            TerminationResult { termination_signal: None, .. } => {
118                warn!("Terminate signal sent before Tracing::drop. Keeping trace.");
119                return;
120            }
121            TerminationResult { termination_signal: Some(Err(e)), .. } => {
122                warn!("Failed to signal termination of trace-writer: {e:?}");
123                return;
124            }
125            TerminationResult { trace_writer: Some(Err(e)), .. } => {
126                warn!("Failed to terminate trace-writer: {e:?}");
127                return;
128            }
129            _ => (),
130        }
131
132        if !always_record_trace {
133            info!("Discarding trace because Tracing instance dropped before a panic.");
134            let _: Result<(), ()> = std::fs::remove_file(&output_trace_path)
135                .map_err(|e| warn!("Failed to remove {}: {e:?}", output_trace_path.display()));
136        }
137    }
138}