fuchsia_inspect/reader/
tree_reader.rs

1// Copyright 2019 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::reader::error::ReaderError;
6use crate::reader::{
7    DiagnosticsHierarchy, MissingValueReason, PartialNodeHierarchy, ReadableTree, Snapshot,
8};
9use crate::{NumericProperty, UintProperty};
10use fuchsia_async::{DurationExt, TimeoutExt};
11use fuchsia_sync::Mutex;
12use futures::prelude::*;
13use futures::stream::FuturesUnordered;
14use inspect_format::LinkNodeDisposition;
15use std::borrow::Cow;
16use std::collections::BTreeMap;
17use std::pin::Pin;
18use std::sync::{Arc, Weak};
19use std::time::Duration;
20
21/// Ten minutes, an absurdly long time to try and read Inspect
22///
23/// Note: do not replace with Duration::MAX. It will overflow in the
24/// implementation of `DurationExt::after_now` used for timeouts.
25const MAX_READ_TIME: std::time::Duration = std::time::Duration::from_secs(60 * 10);
26
27/// Contains the snapshot of the hierarchy and snapshots of all the lazy nodes in the hierarchy.
28#[derive(Debug)]
29pub struct SnapshotTree {
30    snapshot: Snapshot,
31    children: SnapshotTreeMap,
32}
33
34impl SnapshotTree {
35    /// Loads a snapshot tree from the given inspect tree.
36    #[cfg(target_os = "fuchsia")]
37    pub async fn try_from_proxy(
38        tree: &fidl_fuchsia_inspect::TreeProxy,
39    ) -> Result<SnapshotTree, ReaderError> {
40        load_snapshot_tree(tree, MAX_READ_TIME, &Default::default()).await
41    }
42
43    pub async fn try_from_with_timeout<T: ReadableTree + Send + Sync + Clone>(
44        tree: &T,
45        lazy_child_timeout: Duration,
46        timeout_counter: &UintProperty,
47    ) -> Result<SnapshotTree, ReaderError> {
48        load_snapshot_tree(tree, lazy_child_timeout, timeout_counter).await
49    }
50}
51
52impl<'a, T> TryFrom<Trees<'a, T>> for SnapshotTree
53where
54    T: ReadableTree + Send + Sync + Clone,
55{
56    type Error = ReaderError;
57
58    fn try_from(trees: Trees<'a, T>) -> Result<Self, Self::Error> {
59        let snapshot =
60            trees.resolved_node.into_inner().expect("lazy initialization must be performed")?;
61
62        let mut children_map: SnapshotTreeMap = BTreeMap::new();
63
64        // Iterate through successfully discovered children Arc<Trees<T>>.
65        for child_arc in trees.children.into_inner().into_iter() {
66            // Get the child's name to use as the key in the map.
67            if child_arc.name.is_some() {
68                let Some(mut child) = Arc::into_inner(child_arc) else {
69                    return Err(ReaderError::Internal);
70                };
71                let name = child.name.take().unwrap_or_default();
72                let child_result = SnapshotTree::try_from(child);
73
74                children_map.insert(name, child_result);
75            }
76            // If a child has no name, it cannot be represented in the map; skip it.
77        }
78
79        for (name, error) in trees.child_errors.into_inner().into_iter() {
80            children_map.insert(name, Err(error));
81        }
82
83        Ok(SnapshotTree { snapshot, children: children_map })
84    }
85}
86
87type SnapshotTreeMap = BTreeMap<String, Result<SnapshotTree, ReaderError>>;
88
89impl TryInto<DiagnosticsHierarchy> for SnapshotTree {
90    type Error = ReaderError;
91
92    fn try_into(mut self) -> Result<DiagnosticsHierarchy, Self::Error> {
93        let partial = PartialNodeHierarchy::try_from(self.snapshot)?;
94        Ok(expand(partial, &mut self.children))
95    }
96}
97
98fn expand(
99    partial: PartialNodeHierarchy,
100    snapshot_children: &mut SnapshotTreeMap,
101) -> DiagnosticsHierarchy {
102    // TODO(miguelfrde): remove recursion or limit depth.
103    let children =
104        partial.children.into_iter().map(|child| expand(child, snapshot_children)).collect();
105    let mut hierarchy = DiagnosticsHierarchy::new(partial.name, partial.properties, children);
106    for link_value in partial.links {
107        let Some(result) = snapshot_children.remove(&link_value.content) else {
108            hierarchy.add_missing(MissingValueReason::LinkNotFound, link_value.name);
109            continue;
110        };
111
112        // TODO(miguelfrde): remove recursion or limit depth.
113        let result: Result<DiagnosticsHierarchy, ReaderError> =
114            result.and_then(|snapshot_tree| snapshot_tree.try_into());
115        match result {
116            Err(ReaderError::TreeTimedOut) => {
117                hierarchy.add_missing(MissingValueReason::Timeout, link_value.name);
118            }
119            Err(_) => {
120                hierarchy.add_missing(MissingValueReason::LinkParseFailure, link_value.name);
121            }
122            Ok(mut child_hierarchy) => match link_value.disposition {
123                LinkNodeDisposition::Child => {
124                    child_hierarchy.name = link_value.name;
125                    hierarchy.children.push(child_hierarchy);
126                }
127                LinkNodeDisposition::Inline => {
128                    hierarchy.children.extend(child_hierarchy.children.into_iter());
129                    hierarchy.properties.extend(child_hierarchy.properties.into_iter());
130                    hierarchy.missing.extend(child_hierarchy.missing.into_iter());
131                }
132            },
133        }
134    }
135    hierarchy
136}
137
138/// Reads the given `ReadableTree` into a DiagnosticsHierarchy.
139/// This reads versions 1 and 2 of the Inspect Format.
140pub async fn read<T>(tree: &T) -> Result<DiagnosticsHierarchy, ReaderError>
141where
142    T: ReadableTree + Send + Sync + Clone,
143{
144    load_snapshot_tree(tree, MAX_READ_TIME, &Default::default()).await?.try_into()
145}
146
147/// Reads the given `ReadableTree` into a DiagnosticsHierarchy with Lazy Node timeout.
148/// This reads versions 1 and 2 of the Inspect Format.
149pub async fn read_with_timeout<T>(
150    tree: &T,
151    lazy_node_timeout: Duration,
152    timeout_counter: &UintProperty,
153) -> Result<DiagnosticsHierarchy, ReaderError>
154where
155    T: ReadableTree + Send + Sync + Clone,
156{
157    load_snapshot_tree(tree, lazy_node_timeout, timeout_counter).await?.try_into()
158}
159
160/// Intermediate type for representing the tree structure of a proxy/Inspector while
161/// inflating nodes in an online-manner.
162struct Trees<'a, T: Clone> {
163    node: Cow<'a, T>,
164    name: Option<String>,
165    resolved_node: Mutex<Option<Result<Snapshot, ReaderError>>>,
166    // TODO: b/431224266 - Explore using bumpalo rather than Arc for these allocations
167    children: Mutex<Vec<Arc<Trees<'a, T>>>>,
168    parent: Mutex<Option<Weak<Trees<'a, T>>>>,
169    child_errors: Mutex<BTreeMap<String, ReaderError>>,
170}
171
172async fn load_snapshot_tree<'a, T>(
173    tree: &'a T,
174    timeout: Duration,
175    timeout_counter: &UintProperty,
176) -> Result<SnapshotTree, ReaderError>
177where
178    T: ReadableTree + Send + Sync + Clone,
179{
180    let trees = Arc::new(Trees {
181        node: Cow::Borrowed(tree),
182        name: None,
183        children: Mutex::new(vec![]),
184        parent: Mutex::new(None),
185        resolved_node: Mutex::new(None),
186        child_errors: Mutex::new(BTreeMap::new()),
187    });
188    let vmo_resolver = FuturesUnordered::new();
189    let moveable_for_read_tree_queue = Arc::clone(&trees);
190    let mut read_tree_resolver = FuturesUnordered::<
191        Pin<Box<dyn Future<Output = Option<Arc<Trees<'a, T>>>> + Send>>,
192    >::from_iter([
193        async move { Some(moveable_for_read_tree_queue) }.boxed(),
194    ]);
195
196    while let Some(current) = read_tree_resolver.next().await {
197        let Some(current) = current else {
198            continue;
199        };
200        let moveable = Arc::clone(&current);
201        vmo_resolver.push(async move {
202            let resolution = moveable
203                .node
204                .vmo()
205                .on_timeout(timeout.after_now(), || {
206                    timeout_counter.add(1);
207                    Err(ReaderError::TreeTimedOut)
208                })
209                .await
210                .and_then(|data| Snapshot::try_from(&data));
211            let _ = moveable.resolved_node.lock().insert(resolution);
212        });
213        let tree_names = match current
214            .node
215            .tree_names()
216            .on_timeout(timeout.after_now(), || {
217                // no timeout_counter increment, because a failure here
218                // will also manifest as failing to read the VMO of this node
219                Err(ReaderError::TreeTimedOut)
220            })
221            .await
222        {
223            Ok(tree_names) => tree_names,
224            Err(err) => {
225                let guard = current.parent.lock();
226                let Some(Some(parent)) = guard.as_ref().map(|weak| weak.upgrade()) else {
227                    // If we can't get the parent, then we can't register an error. Oh well.
228                    // We also can't read any children, since the tree_names call didn't work.
229                    // So, just move to the next node and give up.
230                    continue;
231                };
232                if let Some(name) = &current.name {
233                    parent.child_errors.lock().insert(String::clone(name), err);
234                }
235                continue;
236            }
237        };
238
239        for child in tree_names.into_iter() {
240            let current = Arc::clone(&current);
241            let next_node = async move {
242                current
243                    .node
244                    .read_tree(&child)
245                    .map(|node| match node {
246                        Ok(node) => {
247                            let next = Arc::new(Trees {
248                                node: Cow::Owned(node),
249                                name: Some(String::clone(&child)),
250                                resolved_node: Mutex::new(None),
251                                children: Mutex::new(vec![]),
252                                parent: Mutex::new(Some(Arc::downgrade(&current))),
253                                child_errors: Mutex::new(BTreeMap::new()),
254                            });
255                            let mut guard = current.children.lock();
256                            guard.push(next);
257                            guard.last().map(Arc::clone)
258                        }
259                        Err(e) => {
260                            current.child_errors.lock().insert(String::clone(&child), e);
261                            None
262                        }
263                    })
264                    .on_timeout(timeout.after_now(), || {
265                        timeout_counter.add(1);
266                        current
267                            .child_errors
268                            .lock()
269                            .insert(String::clone(&child), ReaderError::TreeTimedOut);
270                        None
271                    })
272                    .await
273            };
274            read_tree_resolver.push(next_node.boxed());
275        }
276    }
277
278    let _ = vmo_resolver.collect::<Vec<()>>().await;
279    Arc::into_inner(trees).map(SnapshotTree::try_from).unwrap_or(Err(ReaderError::Internal))
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use crate::{reader, Inspector, InspectorConfig};
286    use diagnostics_assertions::{assert_data_tree, assert_json_diff};
287    use inspect_format::constants;
288
289    #[fuchsia::test]
290    async fn test_read() -> Result<(), anyhow::Error> {
291        let inspector = test_inspector();
292        let hierarchy = read(&inspector).await?;
293        assert_data_tree!(hierarchy, root: {
294            int: 3i64,
295            "lazy-node": {
296                a: "test",
297                child: {
298                    double: 3.25,
299                },
300            }
301        });
302        Ok(())
303    }
304
305    #[fuchsia::test]
306    async fn test_load_snapshot_tree() -> Result<(), anyhow::Error> {
307        let instrumentation = Inspector::default();
308        let counter = instrumentation.root().create_uint("counter", 0);
309
310        let inspector = test_inspector();
311        let mut snapshot_tree = load_snapshot_tree(&inspector, MAX_READ_TIME, &counter).await?;
312
313        assert_data_tree!(instrumentation, root: { counter: 0u64 });
314
315        let root_hierarchy: DiagnosticsHierarchy =
316            PartialNodeHierarchy::try_from(snapshot_tree.snapshot)?.into();
317        assert_eq!(snapshot_tree.children.keys().collect::<Vec<&String>>(), vec!["lazy-node-0"]);
318        assert_data_tree!(root_hierarchy, root: {
319            int: 3i64,
320        });
321
322        let mut lazy_node = snapshot_tree.children.remove("lazy-node-0").unwrap().unwrap();
323        let lazy_node_hierarchy: DiagnosticsHierarchy =
324            PartialNodeHierarchy::try_from(lazy_node.snapshot)?.into();
325        assert_eq!(lazy_node.children.keys().collect::<Vec<&String>>(), vec!["lazy-values-0"]);
326        assert_data_tree!(lazy_node_hierarchy, root: {
327            a: "test",
328            child: {},
329        });
330
331        let lazy_values = lazy_node.children.remove("lazy-values-0").unwrap().unwrap();
332        let lazy_values_hierarchy = PartialNodeHierarchy::try_from(lazy_values.snapshot)?;
333        assert_eq!(lazy_values.children.keys().len(), 0);
334        assert_data_tree!(lazy_values_hierarchy, root: {
335            double: 3.25,
336        });
337
338        Ok(())
339    }
340
341    #[fuchsia::test]
342    async fn read_with_hanging_lazy_node() -> Result<(), anyhow::Error> {
343        let instrumentation = Inspector::default();
344        let counter = instrumentation.root().create_uint("counter", 0);
345
346        let inspector = Inspector::default();
347        let root = inspector.root();
348        root.record_string("child", "value");
349
350        root.record_lazy_values("lazy-node-always-hangs", || {
351            async move {
352                fuchsia_async::Timer::new(Duration::from_secs(30 * 60).after_now()).await;
353                Ok(Inspector::default())
354            }
355            .boxed()
356        });
357
358        root.record_int("int", 3);
359
360        let hierarchy = read_with_timeout(&inspector, Duration::from_secs(2), &counter).await?;
361
362        assert_data_tree!(instrumentation, root: { counter: 1u64 });
363
364        assert_json_diff!(hierarchy, root: {
365            child: "value",
366            int: 3i64,
367        });
368
369        Ok(())
370    }
371
372    #[fuchsia::test]
373    async fn read_too_big_string() {
374        // magic size is the amount of data that can be written. Basically it is the size of the
375        // VMO minus all of the header and bookkeeping data stored in the VMO
376        let magic_size_found_by_experiment = 259076;
377        let inspector =
378            Inspector::new(InspectorConfig::default().size(constants::DEFAULT_VMO_SIZE_BYTES));
379        let string_head = "X".repeat(magic_size_found_by_experiment);
380        let string_tail =
381            "Y".repeat((constants::DEFAULT_VMO_SIZE_BYTES * 2) - magic_size_found_by_experiment);
382        let full_string = format!("{string_head}{string_tail}");
383
384        inspector.root().record_int(full_string, 5);
385        let hierarchy = reader::read(&inspector).await.unwrap();
386        // somewhat redundant to check both things, but checking the size makes fencepost errors
387        // obvious, while checking the contents make real problems obvious
388        assert_eq!(hierarchy.properties[0].key().len(), string_head.len());
389        assert_eq!(hierarchy.properties[0].key(), &string_head);
390    }
391
392    #[fuchsia::test]
393    async fn missing_value_parse_failure() -> Result<(), anyhow::Error> {
394        let inspector = Inspector::default();
395        let _lazy_child = inspector.root().create_lazy_child("lazy", || {
396            async move {
397                // For the sake of the test, force an invalid vmo.
398                Ok(Inspector::new(InspectorConfig::default().no_op()))
399            }
400            .boxed()
401        });
402        let hierarchy = reader::read(&inspector).await?;
403        assert_eq!(hierarchy.missing.len(), 1);
404        assert_eq!(hierarchy.missing[0].reason, MissingValueReason::LinkParseFailure);
405        assert_data_tree!(hierarchy, root: {});
406        Ok(())
407    }
408
409    #[fuchsia::test]
410    async fn missing_value_not_found() -> Result<(), anyhow::Error> {
411        let inspector = Inspector::default();
412        if let Some(state) = inspector.state() {
413            let mut state = state.try_lock().expect("lock state");
414            state
415                .allocate_link("missing", "missing-404", LinkNodeDisposition::Child, 0.into())
416                .unwrap();
417        }
418        let hierarchy = reader::read(&inspector).await?;
419        assert_eq!(hierarchy.missing.len(), 1);
420        assert_eq!(hierarchy.missing[0].reason, MissingValueReason::LinkNotFound);
421        assert_eq!(hierarchy.missing[0].name, "missing");
422        assert_data_tree!(hierarchy, root: {});
423        Ok(())
424    }
425
426    fn test_inspector() -> Inspector {
427        let inspector = Inspector::default();
428        let root = inspector.root();
429        root.record_int("int", 3);
430        root.record_lazy_child("lazy-node", || {
431            async move {
432                let inspector = Inspector::default();
433                inspector.root().record_string("a", "test");
434                let child = inspector.root().create_child("child");
435                child.record_lazy_values("lazy-values", || {
436                    async move {
437                        let inspector = Inspector::default();
438                        inspector.root().record_double("double", 3.25);
439                        Ok(inspector)
440                    }
441                    .boxed()
442                });
443                inspector.root().record(child);
444                Ok(inspector)
445            }
446            .boxed()
447        });
448        inspector
449    }
450
451    #[fuchsia::test]
452    async fn try_from_trees_for_snapshot_tree() {
453        // 1. Set up the data
454        let inspector_root = Inspector::default();
455        inspector_root.root().record_int("val", 1);
456        let vmo_root = inspector_root.vmo().await.unwrap();
457        let snapshot_root = Snapshot::try_from(&vmo_root).unwrap();
458
459        let inspector_child1 = Inspector::default();
460        inspector_child1.root().record_int("val", 2);
461        let vmo_child1 = inspector_child1.vmo().await.unwrap();
462        let snapshot_child1 = Snapshot::try_from(&vmo_child1).unwrap();
463
464        let child1 = Arc::new(Trees::<Inspector> {
465            node: Cow::Owned(inspector_child1),
466            name: Some("child1".to_string()),
467            resolved_node: Mutex::new(Some(Ok(snapshot_child1))),
468            children: Mutex::new(vec![]),
469            parent: Mutex::new(None),
470            child_errors: Mutex::new(BTreeMap::new()),
471        });
472
473        let mut child_errors = BTreeMap::new();
474        child_errors.insert("child2".to_string(), ReaderError::TreeTimedOut);
475
476        let root_trees = Trees::<Inspector> {
477            node: Cow::Owned(inspector_root),
478            name: Some("root".to_string()),
479            resolved_node: Mutex::new(Some(Ok(snapshot_root))),
480            children: Mutex::new(vec![child1]),
481            parent: Mutex::new(None),
482            child_errors: Mutex::new(child_errors),
483        };
484
485        // 2. Call the function to be tested
486        let mut snapshot_tree = SnapshotTree::try_from(root_trees).unwrap();
487
488        // 3. Assert the results
489        let root_hierarchy: DiagnosticsHierarchy =
490            PartialNodeHierarchy::try_from(snapshot_tree.snapshot).unwrap().into();
491        assert_data_tree!(root_hierarchy, root: {
492            val: 1i64,
493        });
494
495        assert_eq!(snapshot_tree.children.len(), 2);
496
497        // Check the successful child
498        let child1_tree = snapshot_tree.children.remove("child1").unwrap().unwrap();
499        let child1_hierarchy: DiagnosticsHierarchy =
500            PartialNodeHierarchy::try_from(child1_tree.snapshot).unwrap().into();
501        assert_data_tree!(child1_hierarchy, root: {
502            val: 2i64,
503        });
504        assert!(child1_tree.children.is_empty());
505
506        // Check the error child
507        let child2_error = snapshot_tree.children.remove("child2").unwrap().unwrap_err();
508        assert!(matches!(child2_error, ReaderError::TreeTimedOut));
509    }
510
511    #[fuchsia::test]
512    async fn try_from_trees_for_snapshot_tree_child_with_no_name() {
513        // 1. Set up the data
514        let inspector_root = Inspector::default();
515        inspector_root.root().record_int("val", 1);
516        let vmo_root = inspector_root.vmo().await.unwrap();
517        let snapshot_root = Snapshot::try_from(&vmo_root).unwrap();
518
519        let inspector_child1 = Inspector::default();
520        inspector_child1.root().record_int("val", 2);
521        let vmo_child1 = inspector_child1.vmo().await.unwrap();
522        let snapshot_child1 = Snapshot::try_from(&vmo_child1).unwrap();
523
524        let child1 = Arc::new(Trees::<Inspector> {
525            node: Cow::Owned(inspector_child1),
526            name: None, // No name
527            resolved_node: Mutex::new(Some(Ok(snapshot_child1))),
528            children: Mutex::new(vec![]),
529            parent: Mutex::new(None),
530            child_errors: Mutex::new(BTreeMap::new()),
531        });
532
533        let root_trees = Trees::<Inspector> {
534            node: Cow::Owned(inspector_root),
535            name: Some("root".to_string()),
536            resolved_node: Mutex::new(Some(Ok(snapshot_root))),
537            children: Mutex::new(vec![child1]),
538            parent: Mutex::new(None),
539            child_errors: Mutex::new(BTreeMap::new()),
540        };
541
542        // 2. Call the function to be tested
543        let snapshot_tree = SnapshotTree::try_from(root_trees).unwrap();
544
545        // 3. Assert the results
546        // The child with no name should be skipped.
547        assert!(snapshot_tree.children.is_empty());
548    }
549}