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