Skip to main content

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
98const MAX_EXPAND_DEPTH: usize = 128;
99
100fn expand(
101    partial: PartialNodeHierarchy,
102    snapshot_children: &mut SnapshotTreeMap,
103) -> DiagnosticsHierarchy {
104    expand_inner(partial, snapshot_children, 0)
105}
106
107fn expand_inner(
108    partial: PartialNodeHierarchy,
109    snapshot_children: &mut SnapshotTreeMap,
110    depth: usize,
111) -> DiagnosticsHierarchy {
112    let children = if depth >= MAX_EXPAND_DEPTH {
113        vec![]
114    } else {
115        partial
116            .children
117            .into_iter()
118            .map(|child| expand_inner(child, snapshot_children, depth + 1))
119            .collect()
120    };
121    let mut hierarchy = DiagnosticsHierarchy::new(partial.name, partial.properties, children);
122    for link_value in partial.links {
123        let Some(result) = snapshot_children.remove(&link_value.content) else {
124            hierarchy.add_missing(MissingValueReason::LinkNotFound, link_value.name);
125            continue;
126        };
127
128        if depth >= MAX_EXPAND_DEPTH {
129            hierarchy.add_missing(MissingValueReason::MaxDepthExceeded, link_value.name);
130            continue;
131        }
132
133        let result: Result<DiagnosticsHierarchy, ReaderError> =
134            result.and_then(|snapshot_tree| snapshot_tree.try_into());
135        match result {
136            Err(ReaderError::TreeTimedOut) => {
137                hierarchy.add_missing(MissingValueReason::Timeout, link_value.name);
138            }
139            Err(_) => {
140                hierarchy.add_missing(MissingValueReason::LinkParseFailure, link_value.name);
141            }
142            Ok(mut child_hierarchy) => match link_value.disposition {
143                LinkNodeDisposition::Child => {
144                    child_hierarchy.name = link_value.name;
145                    hierarchy.children.push(child_hierarchy);
146                }
147                LinkNodeDisposition::Inline => {
148                    hierarchy.children.extend(child_hierarchy.children);
149                    hierarchy.properties.extend(child_hierarchy.properties);
150                    hierarchy.missing.extend(child_hierarchy.missing);
151                }
152            },
153        }
154    }
155    hierarchy
156}
157
158/// Reads the given `ReadableTree` into a DiagnosticsHierarchy.
159/// This reads versions 1 and 2 of the Inspect Format.
160pub async fn read<T>(tree: &T) -> Result<DiagnosticsHierarchy, ReaderError>
161where
162    T: ReadableTree + Send + Sync + Clone,
163{
164    load_snapshot_tree(tree, MAX_READ_TIME, &Default::default()).await?.try_into()
165}
166
167/// Reads the given `ReadableTree` into a DiagnosticsHierarchy with Lazy Node timeout.
168/// This reads versions 1 and 2 of the Inspect Format.
169pub async fn read_with_timeout<T>(
170    tree: &T,
171    lazy_node_timeout: Duration,
172    timeout_counter: &UintProperty,
173) -> Result<DiagnosticsHierarchy, ReaderError>
174where
175    T: ReadableTree + Send + Sync + Clone,
176{
177    load_snapshot_tree(tree, lazy_node_timeout, timeout_counter).await?.try_into()
178}
179
180/// Intermediate type for representing the tree structure of a proxy/Inspector while
181/// inflating nodes in an online-manner.
182struct Trees<'a, T: Clone> {
183    node: Cow<'a, T>,
184    name: Option<String>,
185    resolved_node: Mutex<Option<Result<Snapshot, ReaderError>>>,
186    // TODO: b/431224266 - Explore using bumpalo rather than Arc for these allocations
187    children: Mutex<Vec<Arc<Trees<'a, T>>>>,
188    parent: Mutex<Option<Weak<Trees<'a, T>>>>,
189    child_errors: Mutex<BTreeMap<String, ReaderError>>,
190}
191
192async fn load_snapshot_tree<'a, T>(
193    tree: &'a T,
194    timeout: Duration,
195    timeout_counter: &UintProperty,
196) -> Result<SnapshotTree, ReaderError>
197where
198    T: ReadableTree + Send + Sync + Clone,
199{
200    let trees = Arc::new(Trees {
201        node: Cow::Borrowed(tree),
202        name: None,
203        children: Mutex::new(vec![]),
204        parent: Mutex::new(None),
205        resolved_node: Mutex::new(None),
206        child_errors: Mutex::new(BTreeMap::new()),
207    });
208    let vmo_resolver = FuturesUnordered::new();
209    let moveable_for_read_tree_queue = Arc::clone(&trees);
210    let mut read_tree_resolver = FuturesUnordered::<
211        Pin<Box<dyn Future<Output = Option<Arc<Trees<'a, T>>>> + Send>>,
212    >::from_iter([
213        async move { Some(moveable_for_read_tree_queue) }.boxed(),
214    ]);
215
216    while let Some(current) = read_tree_resolver.next().await {
217        let Some(current) = current else {
218            continue;
219        };
220        let moveable = Arc::clone(&current);
221        vmo_resolver.push(async move {
222            let resolution = moveable
223                .node
224                .vmo()
225                .on_timeout(timeout.after_now(), || {
226                    timeout_counter.add(1);
227                    Err(ReaderError::TreeTimedOut)
228                })
229                .await
230                .and_then(|data| Snapshot::try_from(&data));
231            let _ = moveable.resolved_node.lock().insert(resolution);
232        });
233        let tree_names = match current
234            .node
235            .tree_names()
236            .on_timeout(timeout.after_now(), || {
237                // no timeout_counter increment, because a failure here
238                // will also manifest as failing to read the VMO of this node
239                Err(ReaderError::TreeTimedOut)
240            })
241            .await
242        {
243            Ok(tree_names) => tree_names,
244            Err(err) => {
245                let guard = current.parent.lock();
246                let Some(Some(parent)) = guard.as_ref().map(|weak| weak.upgrade()) else {
247                    // If we can't get the parent, then we can't register an error. Oh well.
248                    // We also can't read any children, since the tree_names call didn't work.
249                    // So, just move to the next node and give up.
250                    continue;
251                };
252                if let Some(name) = &current.name {
253                    parent.child_errors.lock().insert(String::clone(name), err);
254                }
255                continue;
256            }
257        };
258
259        for child in tree_names.into_iter() {
260            let current = Arc::clone(&current);
261            let next_node = async move {
262                current
263                    .node
264                    .read_tree(&child)
265                    .map(|node| match node {
266                        Ok(node) => {
267                            let next = Arc::new(Trees {
268                                node: Cow::Owned(node),
269                                name: Some(String::clone(&child)),
270                                resolved_node: Mutex::new(None),
271                                children: Mutex::new(vec![]),
272                                parent: Mutex::new(Some(Arc::downgrade(&current))),
273                                child_errors: Mutex::new(BTreeMap::new()),
274                            });
275                            let mut guard = current.children.lock();
276                            guard.push(next);
277                            guard.last().map(Arc::clone)
278                        }
279                        Err(e) => {
280                            current.child_errors.lock().insert(String::clone(&child), e);
281                            None
282                        }
283                    })
284                    .on_timeout(timeout.after_now(), || {
285                        timeout_counter.add(1);
286                        current
287                            .child_errors
288                            .lock()
289                            .insert(String::clone(&child), ReaderError::TreeTimedOut);
290                        None
291                    })
292                    .await
293            };
294            read_tree_resolver.push(next_node.boxed());
295        }
296    }
297
298    let _ = vmo_resolver.collect::<Vec<()>>().await;
299    Arc::into_inner(trees).map(SnapshotTree::try_from).unwrap_or(Err(ReaderError::Internal))
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use crate::{Inspector, InspectorConfig, reader};
306    use diagnostics_assertions::{assert_data_tree, assert_json_diff};
307    use inspect_format::constants;
308
309    #[fuchsia::test]
310    async fn test_read() -> Result<(), anyhow::Error> {
311        let inspector = test_inspector();
312        let hierarchy = read(&inspector).await?;
313        assert_data_tree!(hierarchy, root: {
314            int: 3i64,
315            "lazy-node": {
316                a: "test",
317                child: {
318                    double: 3.25,
319                },
320            }
321        });
322        Ok(())
323    }
324
325    #[fuchsia::test]
326    async fn test_load_snapshot_tree() -> Result<(), anyhow::Error> {
327        let instrumentation = Inspector::default();
328        let counter = instrumentation.root().create_uint("counter", 0);
329
330        let inspector = test_inspector();
331        let mut snapshot_tree = load_snapshot_tree(&inspector, MAX_READ_TIME, &counter).await?;
332
333        assert_data_tree!(instrumentation, root: { counter: 0u64 });
334
335        let root_hierarchy: DiagnosticsHierarchy =
336            PartialNodeHierarchy::try_from(snapshot_tree.snapshot)?.into();
337        assert_eq!(snapshot_tree.children.keys().collect::<Vec<&String>>(), vec!["lazy-node-0"]);
338        assert_data_tree!(root_hierarchy, root: {
339            int: 3i64,
340        });
341
342        let mut lazy_node = snapshot_tree.children.remove("lazy-node-0").unwrap().unwrap();
343        let lazy_node_hierarchy: DiagnosticsHierarchy =
344            PartialNodeHierarchy::try_from(lazy_node.snapshot)?.into();
345        assert_eq!(lazy_node.children.keys().collect::<Vec<&String>>(), vec!["lazy-values-0"]);
346        assert_data_tree!(lazy_node_hierarchy, root: {
347            a: "test",
348            child: {},
349        });
350
351        let lazy_values = lazy_node.children.remove("lazy-values-0").unwrap().unwrap();
352        let lazy_values_hierarchy = PartialNodeHierarchy::try_from(lazy_values.snapshot)?;
353        assert_eq!(lazy_values.children.keys().len(), 0);
354        assert_data_tree!(lazy_values_hierarchy, root: {
355            double: 3.25,
356        });
357
358        Ok(())
359    }
360
361    #[fuchsia::test]
362    async fn read_with_hanging_lazy_node() -> Result<(), anyhow::Error> {
363        let instrumentation = Inspector::default();
364        let counter = instrumentation.root().create_uint("counter", 0);
365
366        let inspector = Inspector::default();
367        let root = inspector.root();
368        root.record_string("child", "value");
369
370        root.record_lazy_values("lazy-node-always-hangs", || {
371            async move {
372                fuchsia_async::Timer::new(Duration::from_secs(30 * 60).after_now()).await;
373                Ok(Inspector::default())
374            }
375            .boxed()
376        });
377
378        root.record_int("int", 3);
379
380        let hierarchy = read_with_timeout(&inspector, Duration::from_secs(2), &counter).await?;
381
382        assert_data_tree!(instrumentation, root: { counter: 1u64 });
383
384        assert_json_diff!(hierarchy, root: {
385            child: "value",
386            int: 3i64,
387        });
388
389        Ok(())
390    }
391
392    #[fuchsia::test]
393    async fn read_too_big_string() {
394        // magic size is the amount of data that can be written. Basically it is the size of the
395        // VMO minus all of the header and bookkeeping data stored in the VMO
396        let magic_size_found_by_experiment = 259076;
397        let inspector =
398            Inspector::new(InspectorConfig::default().size(constants::DEFAULT_VMO_SIZE_BYTES));
399        let string_head = "X".repeat(magic_size_found_by_experiment);
400        let string_tail =
401            "Y".repeat((constants::DEFAULT_VMO_SIZE_BYTES * 2) - magic_size_found_by_experiment);
402        let full_string = format!("{string_head}{string_tail}");
403
404        inspector.root().record_int(full_string, 5);
405        let hierarchy = reader::read(&inspector).await.unwrap();
406        // somewhat redundant to check both things, but checking the size makes fencepost errors
407        // obvious, while checking the contents make real problems obvious
408        assert_eq!(hierarchy.properties[0].key().len(), string_head.len());
409        assert_eq!(hierarchy.properties[0].key(), &string_head);
410    }
411
412    #[fuchsia::test]
413    async fn missing_value_parse_failure() -> Result<(), anyhow::Error> {
414        let inspector = Inspector::default();
415        let _lazy_child = inspector.root().create_lazy_child("lazy", || {
416            async move {
417                // For the sake of the test, force an invalid vmo.
418                Ok(Inspector::new(InspectorConfig::default().no_op()))
419            }
420            .boxed()
421        });
422        let hierarchy = reader::read(&inspector).await?;
423        assert_eq!(hierarchy.missing.len(), 1);
424        assert_eq!(hierarchy.missing[0].reason, MissingValueReason::LinkParseFailure);
425        assert_data_tree!(hierarchy, root: {});
426        Ok(())
427    }
428
429    #[fuchsia::test]
430    async fn missing_value_not_found() -> Result<(), anyhow::Error> {
431        let inspector = Inspector::default();
432        if let Some(state) = inspector.state() {
433            let mut state = state.try_lock().expect("lock state");
434            state
435                .allocate_link("missing", "missing-404", LinkNodeDisposition::Child, 0.into())
436                .unwrap();
437        }
438        let hierarchy = reader::read(&inspector).await?;
439        assert_eq!(hierarchy.missing.len(), 1);
440        assert_eq!(hierarchy.missing[0].reason, MissingValueReason::LinkNotFound);
441        assert_eq!(hierarchy.missing[0].name, "missing");
442        assert_data_tree!(hierarchy, root: {});
443        Ok(())
444    }
445
446    #[fuchsia::test]
447    async fn missing_value_max_depth_exceeded() {
448        let partial = PartialNodeHierarchy {
449            name: "root".to_string(),
450            properties: vec![],
451            children: vec![],
452            links: vec![crate::reader::LinkValue {
453                name: "link".to_string(),
454                content: "link-content".to_string(),
455                disposition: LinkNodeDisposition::Child,
456            }],
457        };
458        let mut snapshot_children = SnapshotTreeMap::new();
459        snapshot_children.insert(
460            "link-content".to_string(),
461            Ok(SnapshotTree {
462                snapshot: Snapshot::try_from(&Inspector::default().vmo().await.unwrap()).unwrap(),
463                children: BTreeMap::new(),
464            }),
465        );
466        let hierarchy = expand_inner(partial, &mut snapshot_children, MAX_EXPAND_DEPTH);
467        assert_eq!(hierarchy.missing.len(), 1);
468        assert_eq!(hierarchy.missing[0].reason, MissingValueReason::MaxDepthExceeded);
469        assert_eq!(hierarchy.missing[0].name, "link");
470    }
471
472    fn test_inspector() -> Inspector {
473        let inspector = Inspector::default();
474        let root = inspector.root();
475        root.record_int("int", 3);
476        root.record_lazy_child("lazy-node", || {
477            async move {
478                let inspector = Inspector::default();
479                inspector.root().record_string("a", "test");
480                let child = inspector.root().create_child("child");
481                child.record_lazy_values("lazy-values", || {
482                    async move {
483                        let inspector = Inspector::default();
484                        inspector.root().record_double("double", 3.25);
485                        Ok(inspector)
486                    }
487                    .boxed()
488                });
489                inspector.root().record(child);
490                Ok(inspector)
491            }
492            .boxed()
493        });
494        inspector
495    }
496
497    #[fuchsia::test]
498    async fn test_does_not_deadlock() {
499        let inspector = Inspector::default();
500        let parent = inspector.root();
501        let parent_clone = parent.clone_weak();
502        parent.record_lazy_child("test", move || {
503            let child = Inspector::default();
504            parent_clone.record_int("testing", 0);
505            child.root().record_int("test_2", 0);
506            futures::future::ok(child).boxed()
507        });
508        assert_data_tree!(inspector, root: {
509            test:{
510                test_2: 0i64,
511            },
512            testing:0i64,
513        });
514    }
515
516    #[fuchsia::test]
517    async fn try_from_trees_for_snapshot_tree() {
518        // 1. Set up the data
519        let inspector_root = Inspector::default();
520        inspector_root.root().record_int("val", 1);
521        let vmo_root = inspector_root.vmo().await.unwrap();
522        let snapshot_root = Snapshot::try_from(&vmo_root).unwrap();
523
524        let inspector_child1 = Inspector::default();
525        inspector_child1.root().record_int("val", 2);
526        let vmo_child1 = inspector_child1.vmo().await.unwrap();
527        let snapshot_child1 = Snapshot::try_from(&vmo_child1).unwrap();
528
529        let child1 = Arc::new(Trees::<Inspector> {
530            node: Cow::Owned(inspector_child1),
531            name: Some("child1".to_string()),
532            resolved_node: Mutex::new(Some(Ok(snapshot_child1))),
533            children: Mutex::new(vec![]),
534            parent: Mutex::new(None),
535            child_errors: Mutex::new(BTreeMap::new()),
536        });
537
538        let mut child_errors = BTreeMap::new();
539        child_errors.insert("child2".to_string(), ReaderError::TreeTimedOut);
540
541        let root_trees = Trees::<Inspector> {
542            node: Cow::Owned(inspector_root),
543            name: Some("root".to_string()),
544            resolved_node: Mutex::new(Some(Ok(snapshot_root))),
545            children: Mutex::new(vec![child1]),
546            parent: Mutex::new(None),
547            child_errors: Mutex::new(child_errors),
548        };
549
550        // 2. Call the function to be tested
551        let mut snapshot_tree = SnapshotTree::try_from(root_trees).unwrap();
552
553        // 3. Assert the results
554        let root_hierarchy: DiagnosticsHierarchy =
555            PartialNodeHierarchy::try_from(snapshot_tree.snapshot).unwrap().into();
556        assert_data_tree!(root_hierarchy, root: {
557            val: 1i64,
558        });
559
560        assert_eq!(snapshot_tree.children.len(), 2);
561
562        // Check the successful child
563        let child1_tree = snapshot_tree.children.remove("child1").unwrap().unwrap();
564        let child1_hierarchy: DiagnosticsHierarchy =
565            PartialNodeHierarchy::try_from(child1_tree.snapshot).unwrap().into();
566        assert_data_tree!(child1_hierarchy, root: {
567            val: 2i64,
568        });
569        assert!(child1_tree.children.is_empty());
570
571        // Check the error child
572        let child2_error = snapshot_tree.children.remove("child2").unwrap().unwrap_err();
573        assert!(matches!(child2_error, ReaderError::TreeTimedOut));
574    }
575
576    #[fuchsia::test]
577    async fn try_from_trees_for_snapshot_tree_child_with_no_name() {
578        // 1. Set up the data
579        let inspector_root = Inspector::default();
580        inspector_root.root().record_int("val", 1);
581        let vmo_root = inspector_root.vmo().await.unwrap();
582        let snapshot_root = Snapshot::try_from(&vmo_root).unwrap();
583
584        let inspector_child1 = Inspector::default();
585        inspector_child1.root().record_int("val", 2);
586        let vmo_child1 = inspector_child1.vmo().await.unwrap();
587        let snapshot_child1 = Snapshot::try_from(&vmo_child1).unwrap();
588
589        let child1 = Arc::new(Trees::<Inspector> {
590            node: Cow::Owned(inspector_child1),
591            name: None, // No name
592            resolved_node: Mutex::new(Some(Ok(snapshot_child1))),
593            children: Mutex::new(vec![]),
594            parent: Mutex::new(None),
595            child_errors: Mutex::new(BTreeMap::new()),
596        });
597
598        let root_trees = Trees::<Inspector> {
599            node: Cow::Owned(inspector_root),
600            name: Some("root".to_string()),
601            resolved_node: Mutex::new(Some(Ok(snapshot_root))),
602            children: Mutex::new(vec![child1]),
603            parent: Mutex::new(None),
604            child_errors: Mutex::new(BTreeMap::new()),
605        };
606
607        // 2. Call the function to be tested
608        let snapshot_tree = SnapshotTree::try_from(root_trees).unwrap();
609
610        // 3. Assert the results
611        // The child with no name should be skipped.
612        assert!(snapshot_tree.children.is_empty());
613    }
614}