Skip to main content

stacktrack_snapshot/
snapshot.rs

1// Copyright 2026 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::Error;
6use flex_fuchsia_memory_stacktrack_client as fstacktrack_client;
7use futures::stream::StreamExt;
8
9/// Contains all the data received over a `SnapshotReceiver` channel.
10#[derive(Debug, Default)]
11pub struct Snapshot {
12    /// The page size of the system, if reported.
13    pub page_size: u64,
14
15    /// All the stack traces collected, one per thread.
16    pub stack_traces: Vec<StackTrace>,
17
18    /// All the executable memory regions in the analyzed process.
19    pub executable_regions: Vec<ExecutableRegion>,
20}
21
22/// A memory region containing code loaded from an ELF file.
23#[derive(Debug)]
24pub struct ExecutableRegion {
25    /// Region name for human consumption (usually either the ELF soname or the VMO name), if known.
26    pub name: String,
27
28    /// The start address of this region.
29    pub address: u64,
30
31    /// Region size, in bytes.
32    pub size: u64,
33
34    /// The address of the memory region relative to the file's load address.
35    pub vaddr: u64,
36
37    /// The Build ID of the ELF file.
38    pub build_id: Vec<u8>,
39}
40
41/// A stack trace.
42#[derive(Debug)]
43pub struct StackTrace {
44    /// The koid of the thread with this stack trace.
45    pub thread_koid: u64,
46
47    /// The stack frames, listed bottom-to-top.
48    pub frames: Vec<CallFrame>,
49}
50
51/// A frame in a stack trace.
52#[derive(Debug, Clone)]
53pub struct CallFrame {
54    /// The program counter (PC) or return address.
55    pub program_address: u64,
56
57    /// The frame pointer (FP).
58    pub frame_pointer: u64,
59}
60
61/// Gets the value of a field in a FIDL table as a `Result<T, Error>`.
62///
63/// An `Err(Error::MissingField { .. })` is returned if the field's value is `None`.
64///
65/// Usage: `read_field!(container_expression => ContainerType, field_name)`
66///
67/// # Example
68///
69/// ```
70/// struct MyFidlTable { field: Option<u32>, .. }
71/// let table = MyFidlTable { field: Some(44), .. };
72///
73/// let val = read_field!(table => MyFidlTable, field)?;
74/// ```
75macro_rules! read_field {
76    ($e:expr => $c:ident, $f:ident) => {
77        $e.$f.ok_or(Error::MissingField {
78            container: std::stringify!($c),
79            field: std::stringify!($f),
80        })
81    };
82}
83
84impl Snapshot {
85    /// Receives a snapshot over a `SnapshotReceiver` channel and reassembles it.
86    pub async fn receive_from(
87        mut stream: fstacktrack_client::SnapshotReceiverRequestStream,
88    ) -> Result<Snapshot, Error> {
89        let mut page_size = None;
90        let mut stack_traces = Vec::new();
91        let mut executable_regions = Vec::new();
92
93        loop {
94            // Wait for the next batch of elements.
95            let batch = match stream.next().await.transpose()? {
96                Some(fstacktrack_client::SnapshotReceiverRequest::Batch { batch, responder }) => {
97                    // Send acknowledgment as quickly as possible, then keep processing the received
98                    // batch.
99                    responder.send()?;
100                    batch
101                }
102                Some(fstacktrack_client::SnapshotReceiverRequest::ReportError {
103                    error,
104                    responder,
105                }) => {
106                    let _ = responder.send(); // Ignore the result of the acknowledgment.
107                    return Err(Error::CollectorError(error));
108                }
109                None => return Err(Error::UnexpectedEndOfStream),
110            };
111
112            if batch.is_empty() {
113                let page_size = page_size.ok_or(Error::PageSizeMissing)?;
114                return Ok(Snapshot { page_size, stack_traces, executable_regions });
115            }
116
117            for element in batch {
118                match element {
119                    fstacktrack_client::SnapshotElement::PageSize(size) => {
120                        page_size = Some(size);
121                    }
122                    fstacktrack_client::SnapshotElement::StackTrace(trace) => {
123                        let thread_koid = read_field!(trace => StackTrace, thread_koid)?;
124                        let frames = read_field!(trace => StackTrace, frames)?
125                            .into_iter()
126                            .map(|f| CallFrame {
127                                program_address: f.program_address,
128                                frame_pointer: f.frame_pointer,
129                            })
130                            .collect();
131
132                        stack_traces.push(StackTrace { thread_koid, frames });
133                    }
134                    fstacktrack_client::SnapshotElement::ExecutableRegion(region) => {
135                        let address = read_field!(region => ExecutableRegion, address)?;
136                        let size = read_field!(region => ExecutableRegion, size)?;
137                        let name = region.name.unwrap_or_default();
138                        let vaddr = read_field!(region => ExecutableRegion, vaddr)?;
139                        let build_id = read_field!(region => ExecutableRegion, build_id)?.value;
140
141                        executable_regions.push(ExecutableRegion {
142                            name,
143                            address,
144                            size,
145                            vaddr,
146                            build_id,
147                        });
148                    }
149                    _ => return Err(Error::UnexpectedElementType),
150                }
151            }
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use crate::test_helpers::create_client;
160    use fuchsia_async as fasync;
161    use test_case::test_case;
162
163    #[fasync::run_singlethreaded(test)]
164    async fn test_receive_snapshot() {
165        let client = create_client();
166        let (receiver_proxy, receiver_stream) =
167            client.create_proxy_and_stream::<fstacktrack_client::SnapshotReceiverMarker>();
168
169        let receive_task = fasync::Task::local(Snapshot::receive_from(receiver_stream));
170
171        let elements = vec![
172            fstacktrack_client::SnapshotElement::PageSize(4096),
173            fstacktrack_client::SnapshotElement::ExecutableRegion(
174                fstacktrack_client::ExecutableRegion {
175                    address: Some(0x10000),
176                    size: Some(0x2000),
177                    name: Some("test".to_string()),
178                    vaddr: Some(0x5000),
179                    build_id: Some(fstacktrack_client::BuildId { value: vec![0xAA, 0xBB] }),
180                    ..Default::default()
181                },
182            ),
183            fstacktrack_client::SnapshotElement::StackTrace(fstacktrack_client::StackTrace {
184                thread_koid: Some(8888),
185                frames: Some(vec![fstacktrack_client::CallFrame {
186                    program_address: 0x1234,
187                    frame_pointer: 0x5678,
188                }]),
189                ..Default::default()
190            }),
191        ];
192
193        receiver_proxy.batch(&elements).await.expect("failed to send batch");
194        receiver_proxy.batch(&[]).await.expect("failed to send end marker");
195
196        let snapshot = receive_task.await.expect("failed to receive snapshot");
197
198        assert_eq!(snapshot.page_size, 4096);
199        assert_eq!(snapshot.executable_regions.len(), 1);
200        assert_eq!(snapshot.stack_traces.len(), 1);
201
202        let region = &snapshot.executable_regions[0];
203        assert_eq!(region.address, 0x10000);
204        assert_eq!(region.size, 0x2000);
205        assert_eq!(region.name, "test");
206        assert_eq!(region.vaddr, 0x5000);
207        assert_eq!(region.build_id, vec![0xAA, 0xBB]);
208
209        let trace = &snapshot.stack_traces[0];
210        assert_eq!(trace.thread_koid, 8888);
211        assert_eq!(trace.frames.len(), 1);
212        assert_eq!(trace.frames[0].program_address, 0x1234);
213        assert_eq!(trace.frames[0].frame_pointer, 0x5678);
214    }
215
216    #[test_case(|trace| trace.thread_koid = None => matches
217        Err(Error::MissingField { container: "StackTrace", field: "thread_koid" }) ; "thread_koid")]
218    #[test_case(|trace| trace.frames = None => matches
219        Err(Error::MissingField { container: "StackTrace", field: "frames" }) ; "frames")]
220    #[test_case(|_| () /* if we do not set any field to None, the result should be Ok */ => matches
221        Ok(_) ; "success")]
222    #[fasync::run_singlethreaded(test)]
223    async fn test_stack_trace_required_fields(
224        set_one_field_to_none: fn(&mut fstacktrack_client::StackTrace),
225    ) -> Result<Snapshot, Error> {
226        let client = create_client();
227        let (receiver_proxy, receiver_stream) =
228            client.create_proxy_and_stream::<fstacktrack_client::SnapshotReceiverMarker>();
229        let receive_worker = fasync::Task::local(Snapshot::receive_from(receiver_stream));
230
231        let mut stack_trace = fstacktrack_client::StackTrace {
232            thread_koid: Some(123),
233            frames: Some(vec![fstacktrack_client::CallFrame {
234                program_address: 0x100,
235                frame_pointer: 0x200,
236            }]),
237            ..Default::default()
238        };
239        set_one_field_to_none(&mut stack_trace);
240
241        // Ignore result, as the peer may detect the error and close the channel.
242        let _ = receiver_proxy
243            .batch(&[
244                fstacktrack_client::SnapshotElement::PageSize(4096),
245                fstacktrack_client::SnapshotElement::StackTrace(stack_trace),
246            ])
247            .await;
248        let _ = receiver_proxy.batch(&[]).await;
249
250        receive_worker.await
251    }
252
253    #[test_case(|region| region.address = None => matches
254        Err(Error::MissingField { container: "ExecutableRegion", field: "address" }) ; "address")]
255    #[test_case(|region| region.size = None => matches
256        Err(Error::MissingField { container: "ExecutableRegion", field: "size" }) ; "size")]
257    #[test_case(|region| region.vaddr = None => matches
258        Err(Error::MissingField { container: "ExecutableRegion", field: "vaddr" }) ; "vaddr")]
259    #[test_case(|region| region.build_id = None => matches
260        Err(Error::MissingField { container: "ExecutableRegion", field: "build_id" }) ; "build_id")]
261    #[test_case(|_| () /* if we do not set any field to None, the result should be Ok */ => matches
262        Ok(_) ; "success")]
263    #[fasync::run_singlethreaded(test)]
264    async fn test_executable_region_required_fields(
265        set_one_field_to_none: fn(&mut fstacktrack_client::ExecutableRegion),
266    ) -> Result<Snapshot, Error> {
267        let client = create_client();
268        let (receiver_proxy, receiver_stream) =
269            client.create_proxy_and_stream::<fstacktrack_client::SnapshotReceiverMarker>();
270        let receive_worker = fasync::Task::local(Snapshot::receive_from(receiver_stream));
271
272        let mut region = fstacktrack_client::ExecutableRegion {
273            address: Some(0x10000),
274            size: Some(0x2000),
275            name: Some("test".to_string()),
276            vaddr: Some(0x5000),
277            build_id: Some(fstacktrack_client::BuildId { value: vec![0xAA, 0xBB] }),
278            ..Default::default()
279        };
280        set_one_field_to_none(&mut region);
281
282        // Ignore result, as the peer may detect the error and close the channel.
283        let _ = receiver_proxy
284            .batch(&[
285                fstacktrack_client::SnapshotElement::PageSize(4096),
286                fstacktrack_client::SnapshotElement::ExecutableRegion(region),
287            ])
288            .await;
289        let _ = receiver_proxy.batch(&[]).await;
290
291        receive_worker.await
292    }
293}