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::collections::BTreeMap;
16use std::pin::Pin;
17use std::sync::Arc;
18use std::time::Duration;
19
20/// Ten minutes, an absurdly long time to try and read Inspect
21///
22/// Note: do not replace with Duration::MAX. It will overflow in the
23/// implementation of `DurationExt::after_now` used for timeouts.
24const MAX_READ_TIME: std::time::Duration = std::time::Duration::from_secs(60 * 10);
25
26/// Contains the snapshot of the hierarchy and snapshots of all the lazy nodes in the hierarchy.
27#[derive(Debug)]
28pub struct SnapshotTree {
29    snapshot: Snapshot,
30    children: SnapshotTreeMap,
31}
32
33impl SnapshotTree {
34    /// Loads a snapshot tree from the given inspect tree.
35    #[cfg(target_os = "fuchsia")]
36    pub async fn try_from_proxy(
37        tree: &fidl_fuchsia_inspect::TreeProxy,
38    ) -> Result<SnapshotTree, ReaderError> {
39        load_snapshot_tree(tree, MAX_READ_TIME, &Default::default()).await
40    }
41
42    pub async fn try_from_with_timeout<T: ReadableTree + Send + Sync + Clone>(
43        tree: &T,
44        lazy_child_timeout: Duration,
45        timeout_counter: &UintProperty,
46    ) -> Result<SnapshotTree, ReaderError> {
47        load_snapshot_tree(tree, lazy_child_timeout, timeout_counter).await
48    }
49}
50
51impl<T> TryFrom<Trees<T>> for SnapshotTree
52where
53    T: ReadableTree + Send + Sync + Clone,
54{
55    type Error = ReaderError;
56
57    fn try_from(trees: Trees<T>) -> Result<Self, Self::Error> {
58        let snapshot =
59            trees.resolved_node.into_inner().expect("lazy initialization must be performed")?;
60
61        let mut children_map: SnapshotTreeMap = BTreeMap::new();
62
63        // Iterate through successfully discovered children Arc<Trees<T>>.
64        for child_arc in trees.children.into_inner().into_iter() {
65            // Get the child's name to use as the key in the map.
66            if child_arc.name.is_some() {
67                let Some(mut child) = Arc::into_inner(child_arc) else {
68                    return Err(ReaderError::Internal);
69                };
70                let name = child.name.take().unwrap_or_default();
71                let child_result = SnapshotTree::try_from(child);
72
73                children_map.insert(name, child_result);
74            }
75            // If a child has no name, it cannot be represented in the map; skip it.
76        }
77
78        for (name, error) in trees.child_errors.into_inner().into_iter() {
79            children_map.insert(name, Err(error));
80        }
81
82        Ok(SnapshotTree { snapshot, children: children_map })
83    }
84}
85
86type SnapshotTreeMap = BTreeMap<String, Result<SnapshotTree, ReaderError>>;
87
88impl TryInto<DiagnosticsHierarchy> for SnapshotTree {
89    type Error = ReaderError;
90
91    fn try_into(mut self) -> Result<DiagnosticsHierarchy, Self::Error> {
92        let partial = PartialNodeHierarchy::try_from(self.snapshot)?;
93        Ok(expand(partial, &mut self.children))
94    }
95}
96
97const MAX_EXPAND_DEPTH: usize = 128;
98
99fn expand(
100    partial: PartialNodeHierarchy,
101    snapshot_children: &mut SnapshotTreeMap,
102) -> DiagnosticsHierarchy {
103    expand_inner(partial, snapshot_children, 0)
104}
105
106fn expand_inner(
107    partial: PartialNodeHierarchy,
108    snapshot_children: &mut SnapshotTreeMap,
109    depth: usize,
110) -> DiagnosticsHierarchy {
111    let children = if depth >= MAX_EXPAND_DEPTH {
112        vec![]
113    } else {
114        partial
115            .children
116            .into_iter()
117            .map(|child| expand_inner(child, snapshot_children, depth + 1))
118            .collect()
119    };
120    let mut hierarchy = DiagnosticsHierarchy::new(partial.name, partial.properties, children);
121    for link_value in partial.links {
122        let Some(result) = snapshot_children.remove(&link_value.content) else {
123            hierarchy.add_missing(MissingValueReason::LinkNotFound, link_value.name);
124            continue;
125        };
126
127        if depth >= MAX_EXPAND_DEPTH {
128            hierarchy.add_missing(MissingValueReason::MaxDepthExceeded, link_value.name);
129            continue;
130        }
131
132        let result: Result<DiagnosticsHierarchy, ReaderError> =
133            result.and_then(|snapshot_tree| snapshot_tree.try_into());
134        match result {
135            Err(ReaderError::TreeTimedOut) => {
136                hierarchy.add_missing(MissingValueReason::Timeout, link_value.name);
137            }
138            Err(_) => {
139                hierarchy.add_missing(MissingValueReason::LinkParseFailure, link_value.name);
140            }
141            Ok(mut child_hierarchy) => match link_value.disposition {
142                LinkNodeDisposition::Child => {
143                    child_hierarchy.name = link_value.name;
144                    hierarchy.children.push(child_hierarchy);
145                }
146                LinkNodeDisposition::Inline => {
147                    hierarchy.children.extend(child_hierarchy.children);
148                    hierarchy.properties.extend(child_hierarchy.properties);
149                    hierarchy.missing.extend(child_hierarchy.missing);
150                }
151                // TODO(https://fxbug.dev/573703359): Expand LinkNodeDisposition::Blob links into
152                // Property::Bytes by reading the external blob VMO via Tree.OpenBlobChild.
153                LinkNodeDisposition::Blob => {
154                    todo!(
155                        "https://fxbug.dev/573703359: Implement LinkNodeDisposition::Blob expansion"
156                    )
157                }
158            },
159        }
160    }
161    hierarchy
162}
163
164/// Reads the given `ReadableTree` into a DiagnosticsHierarchy.
165/// This reads versions 1 and 2 of the Inspect Format.
166pub async fn read<T>(tree: &T) -> Result<DiagnosticsHierarchy, ReaderError>
167where
168    T: ReadableTree + Send + Sync + Clone,
169{
170    load_snapshot_tree(tree, MAX_READ_TIME, &Default::default()).await?.try_into()
171}
172
173/// Reads the given `ReadableTree` into a DiagnosticsHierarchy with Lazy Node timeout.
174/// This reads versions 1 and 2 of the Inspect Format.
175pub async fn read_with_timeout<T>(
176    tree: &T,
177    lazy_node_timeout: Duration,
178    timeout_counter: &UintProperty,
179) -> Result<DiagnosticsHierarchy, ReaderError>
180where
181    T: ReadableTree + Send + Sync + Clone,
182{
183    load_snapshot_tree(tree, lazy_node_timeout, timeout_counter).await?.try_into()
184}
185
186/// Intermediate type for representing the tree structure of a proxy/Inspector while
187/// inflating nodes in an online-manner. A `Trees` only exists for nodes whose lazy child names
188/// were read successfully; failures are recorded in the parent's `child_errors`.
189struct Trees<T> {
190    node: T,
191    name: Option<String>,
192    resolved_node: Mutex<Option<Result<Snapshot, ReaderError>>>,
193    // TODO: b/431224266 - Explore using bumpalo rather than Arc for these allocations
194    children: Mutex<Vec<Arc<Trees<T>>>>,
195    child_errors: Mutex<BTreeMap<String, ReaderError>>,
196}
197
198impl<T> Trees<T> {
199    fn new(node: T, name: Option<String>) -> Self {
200        Self {
201            node,
202            name,
203            resolved_node: Mutex::new(None),
204            children: Mutex::new(vec![]),
205            child_errors: Mutex::new(BTreeMap::new()),
206        }
207    }
208}
209
210async fn load_snapshot_tree<T>(
211    tree: &T,
212    timeout: Duration,
213    timeout_counter: &UintProperty,
214) -> Result<SnapshotTree, ReaderError>
215where
216    T: ReadableTree + Send + Sync + Clone,
217{
218    let names = match tree
219        .tree_names()
220        .on_timeout(timeout.after_now(), || {
221            timeout_counter.add(1);
222            Err(ReaderError::TreeTimedOut)
223        })
224        .await
225    {
226        Err(ReaderError::TreeTimedOut) => return Err(ReaderError::TreeTimedOut),
227        // If the root's child names can't be read, we still return the root snapshot.
228        names => names.unwrap_or_default(),
229    };
230
231    let root = Arc::new(Trees::new(tree.clone(), None));
232    let mut discovered = vec![Arc::clone(&root)];
233
234    // Every lazy child is read as soon as its parent's child names are known, so that
235    // hanging children time out concurrently rather than one after the other.
236    let mut pending = FuturesUnordered::new();
237    enqueue_children(&mut pending, &root, names, timeout, timeout_counter);
238
239    while let Some(resolved) = pending.next().await {
240        let Some((node, child_names)) = resolved else {
241            continue;
242        };
243        enqueue_children(&mut pending, &node, child_names, timeout, timeout_counter);
244        discovered.push(node);
245    }
246
247    // Snapshots are only read once every lazy node has been discovered, so that lazy callbacks
248    // which modify their parent's data are reflected in the parent's snapshot.
249    future::join_all(
250        discovered.iter().map(|node| resolve_snapshot(node, timeout, timeout_counter)),
251    )
252    .await;
253    drop(discovered);
254
255    Arc::into_inner(root).map(SnapshotTree::try_from).unwrap_or(Err(ReaderError::Internal))
256}
257
258/// Reads the VMO of `node` and records the resulting snapshot (or error) on it.
259async fn resolve_snapshot<T: ReadableTree + Send + Sync>(
260    node: &Trees<T>,
261    timeout: Duration,
262    timeout_counter: &UintProperty,
263) {
264    let resolution = node
265        .node
266        .vmo()
267        .on_timeout(timeout.after_now(), || {
268            timeout_counter.add(1);
269            Err(ReaderError::TreeTimedOut)
270        })
271        .await
272        .and_then(|data| Snapshot::try_from(&data));
273    let _ = node.resolved_node.lock().insert(resolution);
274}
275
276type PendingChildren<'f, T> = FuturesUnordered<
277    Pin<Box<dyn Future<Output = Option<(Arc<Trees<T>>, Vec<String>)>> + Send + 'f>>,
278>;
279
280/// Pushes a future onto `pending` for each lazy child in `names` of `parent`. Each future opens
281/// the child and reads its lazy child names under a single timeout, and registers the child (or
282/// the error) with `parent`.
283fn enqueue_children<'f, T>(
284    pending: &mut PendingChildren<'f, T>,
285    parent: &Arc<Trees<T>>,
286    names: Vec<String>,
287    timeout: Duration,
288    timeout_counter: &'f UintProperty,
289) where
290    T: ReadableTree + Send + Sync + Clone + 'f,
291{
292    for name in names {
293        let parent = Arc::clone(parent);
294        pending.push(
295            async move {
296                let result = async {
297                    let node = parent.node.read_tree(&name).await?;
298                    let child_names = node.tree_names().await?;
299                    Ok::<_, ReaderError>((node, child_names))
300                }
301                .on_timeout(timeout.after_now(), || {
302                    timeout_counter.add(1);
303                    Err(ReaderError::TreeTimedOut)
304                })
305                .await;
306                match result {
307                    Ok((node, child_names)) => {
308                        let child = Arc::new(Trees::new(node, Some(name)));
309                        parent.children.lock().push(Arc::clone(&child));
310                        Some((child, child_names))
311                    }
312                    Err(e) => {
313                        parent.child_errors.lock().insert(name, e);
314                        None
315                    }
316                }
317            }
318            .boxed(),
319        );
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use crate::reader::readable_tree::SnapshotSource;
327    use crate::{Inspector, InspectorConfig, reader};
328    use assert_matches::assert_matches;
329    use diagnostics_assertions::{assert_data_tree, assert_json_diff};
330    use inspect_format::constants;
331
332    #[fuchsia::test]
333    async fn test_read() -> Result<(), anyhow::Error> {
334        let inspector = test_inspector();
335        let hierarchy = read(&inspector).await?;
336        assert_data_tree!(hierarchy, root: {
337            int: 3i64,
338            "lazy-node": {
339                a: "test",
340                child: {
341                    double: 3.25,
342                },
343            }
344        });
345        Ok(())
346    }
347
348    #[fuchsia::test]
349    async fn test_load_snapshot_tree() -> Result<(), anyhow::Error> {
350        let instrumentation = Inspector::default();
351        let counter = instrumentation.root().create_uint("counter", 0);
352
353        let inspector = test_inspector();
354        let mut snapshot_tree = load_snapshot_tree(&inspector, MAX_READ_TIME, &counter).await?;
355
356        assert_data_tree!(instrumentation, root: { counter: 0u64 });
357
358        let root_hierarchy: DiagnosticsHierarchy =
359            PartialNodeHierarchy::try_from(snapshot_tree.snapshot)?.into();
360        assert_eq!(snapshot_tree.children.keys().collect::<Vec<&String>>(), vec!["lazy-node-0"]);
361        assert_data_tree!(root_hierarchy, root: {
362            int: 3i64,
363        });
364
365        let mut lazy_node = snapshot_tree.children.remove("lazy-node-0").unwrap().unwrap();
366        let lazy_node_hierarchy: DiagnosticsHierarchy =
367            PartialNodeHierarchy::try_from(lazy_node.snapshot)?.into();
368        assert_eq!(lazy_node.children.keys().collect::<Vec<&String>>(), vec!["lazy-values-0"]);
369        assert_data_tree!(lazy_node_hierarchy, root: {
370            a: "test",
371            child: {},
372        });
373
374        let lazy_values = lazy_node.children.remove("lazy-values-0").unwrap().unwrap();
375        let lazy_values_hierarchy = PartialNodeHierarchy::try_from(lazy_values.snapshot)?;
376        assert_eq!(lazy_values.children.keys().len(), 0);
377        assert_data_tree!(lazy_values_hierarchy, root: {
378            double: 3.25,
379        });
380
381        Ok(())
382    }
383
384    #[fuchsia::test]
385    async fn read_with_hanging_lazy_node() -> Result<(), anyhow::Error> {
386        let instrumentation = Inspector::default();
387        let counter = instrumentation.root().create_uint("counter", 0);
388
389        let inspector = Inspector::default();
390        let root = inspector.root();
391        root.record_string("child", "value");
392
393        root.record_lazy_values("lazy-node-always-hangs", || {
394            async move {
395                fuchsia_async::Timer::new(Duration::from_secs(30 * 60).after_now()).await;
396                Ok(Inspector::default())
397            }
398            .boxed()
399        });
400
401        root.record_int("int", 3);
402
403        let hierarchy = read_with_timeout(&inspector, Duration::from_secs(2), &counter).await?;
404
405        assert_data_tree!(instrumentation, root: { counter: 1u64 });
406
407        assert_json_diff!(hierarchy, root: {
408            child: "value",
409            int: 3i64,
410        });
411
412        Ok(())
413    }
414
415    #[fuchsia::test]
416    async fn read_too_big_string() {
417        // magic size is the amount of data that can be written. Basically it is the size of the
418        // VMO minus all of the header and bookkeeping data stored in the VMO
419        let magic_size_found_by_experiment = 259076;
420        let inspector =
421            Inspector::new(InspectorConfig::default().size(constants::DEFAULT_VMO_SIZE_BYTES));
422        let string_head = "X".repeat(magic_size_found_by_experiment);
423        let string_tail =
424            "Y".repeat((constants::DEFAULT_VMO_SIZE_BYTES * 2) - magic_size_found_by_experiment);
425        let full_string = format!("{string_head}{string_tail}");
426
427        inspector.root().record_int(full_string, 5);
428        let hierarchy = reader::read(&inspector).await.unwrap();
429        // somewhat redundant to check both things, but checking the size makes fencepost errors
430        // obvious, while checking the contents make real problems obvious
431        assert_eq!(hierarchy.properties[0].key().len(), string_head.len());
432        assert_eq!(hierarchy.properties[0].key(), &string_head);
433    }
434
435    #[fuchsia::test]
436    async fn missing_value_parse_failure() -> Result<(), anyhow::Error> {
437        let inspector = Inspector::default();
438        let _lazy_child = inspector.root().create_lazy_child("lazy", || {
439            async move {
440                // For the sake of the test, force an invalid vmo.
441                Ok(Inspector::new(InspectorConfig::default().no_op()))
442            }
443            .boxed()
444        });
445        let hierarchy = reader::read(&inspector).await?;
446        assert_eq!(hierarchy.missing.len(), 1);
447        assert_eq!(hierarchy.missing[0].reason, MissingValueReason::LinkParseFailure);
448        assert_data_tree!(hierarchy, root: {});
449        Ok(())
450    }
451
452    #[fuchsia::test]
453    async fn missing_value_not_found() -> Result<(), anyhow::Error> {
454        let inspector = Inspector::default();
455        if let Some(state) = inspector.state() {
456            let mut state = state.try_lock().expect("lock state");
457            state
458                .allocate_link("missing", "missing-404", LinkNodeDisposition::Child, 0.into())
459                .unwrap();
460        }
461        let hierarchy = reader::read(&inspector).await?;
462        assert_eq!(hierarchy.missing.len(), 1);
463        assert_eq!(hierarchy.missing[0].reason, MissingValueReason::LinkNotFound);
464        assert_eq!(hierarchy.missing[0].name, "missing");
465        assert_data_tree!(hierarchy, root: {});
466        Ok(())
467    }
468
469    #[fuchsia::test]
470    async fn missing_value_max_depth_exceeded() {
471        let partial = PartialNodeHierarchy {
472            name: "root".to_string(),
473            properties: vec![],
474            children: vec![],
475            links: vec![crate::reader::LinkValue {
476                name: "link".to_string(),
477                content: "link-content".to_string(),
478                disposition: LinkNodeDisposition::Child,
479            }],
480        };
481        let mut snapshot_children = SnapshotTreeMap::new();
482        snapshot_children.insert(
483            "link-content".to_string(),
484            Ok(SnapshotTree {
485                snapshot: Snapshot::try_from(&Inspector::default().vmo().await.unwrap()).unwrap(),
486                children: BTreeMap::new(),
487            }),
488        );
489        let hierarchy = expand_inner(partial, &mut snapshot_children, MAX_EXPAND_DEPTH);
490        assert_eq!(hierarchy.missing.len(), 1);
491        assert_eq!(hierarchy.missing[0].reason, MissingValueReason::MaxDepthExceeded);
492        assert_eq!(hierarchy.missing[0].name, "link");
493    }
494
495    fn test_inspector() -> Inspector {
496        let inspector = Inspector::default();
497        let root = inspector.root();
498        root.record_int("int", 3);
499        root.record_lazy_child("lazy-node", || {
500            async move {
501                let inspector = Inspector::default();
502                inspector.root().record_string("a", "test");
503                let child = inspector.root().create_child("child");
504                child.record_lazy_values("lazy-values", || {
505                    async move {
506                        let inspector = Inspector::default();
507                        inspector.root().record_double("double", 3.25);
508                        Ok(inspector)
509                    }
510                    .boxed()
511                });
512                inspector.root().record(child);
513                Ok(inspector)
514            }
515            .boxed()
516        });
517        inspector
518    }
519
520    #[fuchsia::test]
521    async fn test_does_not_deadlock() {
522        let inspector = Inspector::default();
523        let parent = inspector.root();
524        let parent_clone = parent.clone_weak();
525        parent.record_lazy_child("test", move || {
526            let child = Inspector::default();
527            parent_clone.record_int("testing", 0);
528            child.root().record_int("test_2", 0);
529            futures::future::ok(child).boxed()
530        });
531        assert_data_tree!(inspector, root: {
532            test:{
533                test_2: 0i64,
534            },
535            testing:0i64,
536        });
537    }
538
539    #[fuchsia::test]
540    async fn try_from_trees_for_snapshot_tree() {
541        // 1. Set up the data
542        let inspector_root = Inspector::default();
543        inspector_root.root().record_int("val", 1);
544        let vmo_root = inspector_root.vmo().await.unwrap();
545        let snapshot_root = Snapshot::try_from(&vmo_root).unwrap();
546
547        let inspector_child1 = Inspector::default();
548        inspector_child1.root().record_int("val", 2);
549        let vmo_child1 = inspector_child1.vmo().await.unwrap();
550        let snapshot_child1 = Snapshot::try_from(&vmo_child1).unwrap();
551
552        let child1 = Arc::new(Trees::<Inspector> {
553            node: inspector_child1,
554            name: Some("child1".to_string()),
555            resolved_node: Mutex::new(Some(Ok(snapshot_child1))),
556            children: Mutex::new(vec![]),
557            child_errors: Mutex::new(BTreeMap::new()),
558        });
559
560        let mut child_errors = BTreeMap::new();
561        child_errors.insert("child2".to_string(), ReaderError::TreeTimedOut);
562
563        let root_trees = Trees::<Inspector> {
564            node: inspector_root,
565            name: Some("root".to_string()),
566            resolved_node: Mutex::new(Some(Ok(snapshot_root))),
567            children: Mutex::new(vec![child1]),
568            child_errors: Mutex::new(child_errors),
569        };
570
571        // 2. Call the function to be tested
572        let mut snapshot_tree = SnapshotTree::try_from(root_trees).unwrap();
573
574        // 3. Assert the results
575        let root_hierarchy: DiagnosticsHierarchy =
576            PartialNodeHierarchy::try_from(snapshot_tree.snapshot).unwrap().into();
577        assert_data_tree!(root_hierarchy, root: {
578            val: 1i64,
579        });
580
581        assert_eq!(snapshot_tree.children.len(), 2);
582
583        // Check the successful child
584        let child1_tree = snapshot_tree.children.remove("child1").unwrap().unwrap();
585        let child1_hierarchy: DiagnosticsHierarchy =
586            PartialNodeHierarchy::try_from(child1_tree.snapshot).unwrap().into();
587        assert_data_tree!(child1_hierarchy, root: {
588            val: 2i64,
589        });
590        assert!(child1_tree.children.is_empty());
591
592        // Check the error child
593        let child2_error = snapshot_tree.children.remove("child2").unwrap().unwrap_err();
594        assert!(matches!(child2_error, ReaderError::TreeTimedOut));
595    }
596
597    #[fuchsia::test]
598    async fn try_from_trees_for_snapshot_tree_child_with_no_name() {
599        // 1. Set up the data
600        let inspector_root = Inspector::default();
601        inspector_root.root().record_int("val", 1);
602        let vmo_root = inspector_root.vmo().await.unwrap();
603        let snapshot_root = Snapshot::try_from(&vmo_root).unwrap();
604
605        let inspector_child1 = Inspector::default();
606        inspector_child1.root().record_int("val", 2);
607        let vmo_child1 = inspector_child1.vmo().await.unwrap();
608        let snapshot_child1 = Snapshot::try_from(&vmo_child1).unwrap();
609
610        let child1 = Arc::new(Trees::<Inspector> {
611            node: inspector_child1,
612            name: None, // No name
613            resolved_node: Mutex::new(Some(Ok(snapshot_child1))),
614            children: Mutex::new(vec![]),
615            child_errors: Mutex::new(BTreeMap::new()),
616        });
617
618        let root_trees = Trees::<Inspector> {
619            node: inspector_root,
620            name: Some("root".to_string()),
621            resolved_node: Mutex::new(Some(Ok(snapshot_root))),
622            children: Mutex::new(vec![child1]),
623            child_errors: Mutex::new(BTreeMap::new()),
624        };
625
626        // 2. Call the function to be tested
627        let snapshot_tree = SnapshotTree::try_from(root_trees).unwrap();
628
629        // 3. Assert the results
630        // The child with no name should be skipped.
631        assert!(snapshot_tree.children.is_empty());
632    }
633
634    /// Behavior of a `FakeTree` node when it is read.
635    #[derive(Clone, Copy, PartialEq)]
636    enum FakeBehavior {
637        /// The node's VMO and child names are returned immediately.
638        Healthy,
639        /// Reading the node's VMO and child names never completes, as is the case for a lazy node
640        /// whose callback never resolves when it is served over `fuchsia.inspect.Tree`.
641        Hangs,
642        /// Reading the node's VMO fails immediately.
643        VmoError,
644    }
645
646    /// A `ReadableTree` whose nodes can be configured to hang or fail.
647    #[derive(Clone)]
648    struct FakeTree {
649        inspector: Inspector,
650        behavior: FakeBehavior,
651        children: Arc<BTreeMap<String, FakeTree>>,
652    }
653
654    impl FakeTree {
655        fn new(behavior: FakeBehavior) -> Self {
656            Self { inspector: Inspector::default(), behavior, children: Arc::new(BTreeMap::new()) }
657        }
658
659        fn with_child(mut self, name: &str, child: FakeTree) -> Self {
660            Arc::make_mut(&mut self.children).insert(name.to_string(), child);
661            self
662        }
663    }
664
665    #[async_trait::async_trait]
666    impl ReadableTree for FakeTree {
667        async fn tree_names(&self) -> Result<Vec<String>, ReaderError> {
668            if self.behavior == FakeBehavior::Hangs {
669                future::pending::<()>().await;
670            }
671            Ok(self.children.keys().cloned().collect())
672        }
673
674        async fn vmo(&self) -> Result<SnapshotSource, ReaderError> {
675            match self.behavior {
676                FakeBehavior::Hangs => future::pending().await,
677                FakeBehavior::VmoError => Err(ReaderError::FetchVmo),
678                FakeBehavior::Healthy => ReadableTree::vmo(&self.inspector).await,
679            }
680        }
681
682        async fn read_tree(&self, name: &str) -> Result<Self, ReaderError> {
683            self.children.get(name).cloned().ok_or(ReaderError::FailedToLoadTree(name.to_string()))
684        }
685    }
686
687    const FAKE_TIMEOUT: Duration = Duration::from_secs(2);
688
689    #[fuchsia::test]
690    async fn pipelined_hanging_children_time_out_concurrently() {
691        let instrumentation = Inspector::default();
692        let counter = instrumentation.root().create_uint("counter", 0);
693
694        let tree = FakeTree::new(FakeBehavior::Healthy)
695            .with_child("hang-0", FakeTree::new(FakeBehavior::Hangs))
696            .with_child("hang-1", FakeTree::new(FakeBehavior::Hangs))
697            .with_child("hang-2", FakeTree::new(FakeBehavior::Hangs))
698            .with_child("healthy", FakeTree::new(FakeBehavior::Healthy));
699
700        let start = std::time::Instant::now();
701        let mut snapshot_tree = load_snapshot_tree(&tree, FAKE_TIMEOUT, &counter).await.unwrap();
702        let elapsed = start.elapsed();
703
704        // Reading the hanging children serially would take at least 3 * FAKE_TIMEOUT.
705        assert!(elapsed < 3 * FAKE_TIMEOUT, "took {elapsed:?}");
706        assert_data_tree!(instrumentation, root: { counter: 3u64 });
707
708        assert_eq!(snapshot_tree.children.len(), 4);
709        for name in ["hang-0", "hang-1", "hang-2"] {
710            assert_matches!(
711                snapshot_tree.children.remove(name),
712                Some(Err(ReaderError::TreeTimedOut))
713            );
714        }
715        assert_matches!(snapshot_tree.children.remove("healthy"), Some(Ok(_)));
716    }
717
718    #[fuchsia::test]
719    async fn hanging_child_times_out_once_per_level() {
720        let instrumentation = Inspector::default();
721        let counter = instrumentation.root().create_uint("counter", 0);
722
723        let tree = FakeTree::new(FakeBehavior::Healthy)
724            .with_child(
725                "healthy",
726                FakeTree::new(FakeBehavior::Healthy)
727                    .with_child("nested-hang", FakeTree::new(FakeBehavior::Hangs)),
728            )
729            .with_child("hang", FakeTree::new(FakeBehavior::Hangs));
730
731        let start = std::time::Instant::now();
732        let mut snapshot_tree = load_snapshot_tree(&tree, FAKE_TIMEOUT, &counter).await.unwrap();
733        let elapsed = start.elapsed();
734
735        // The nested hanging child is read as soon as its parent resolves, so it times out
736        // concurrently with its hanging uncle rather than after it.
737        assert!(elapsed < 2 * FAKE_TIMEOUT, "took {elapsed:?}");
738        // Each hanging node increments the counter exactly once.
739        assert_data_tree!(instrumentation, root: { counter: 2u64 });
740
741        assert_eq!(snapshot_tree.children.len(), 2);
742        assert_matches!(
743            snapshot_tree.children.remove("hang"),
744            Some(Err(ReaderError::TreeTimedOut))
745        );
746        let mut healthy = snapshot_tree.children.remove("healthy").unwrap().unwrap();
747        assert_eq!(healthy.children.len(), 1);
748        assert_matches!(
749            healthy.children.remove("nested-hang"),
750            Some(Err(ReaderError::TreeTimedOut))
751        );
752    }
753
754    #[fuchsia::test]
755    async fn root_timeout_counts_once_and_errors() {
756        let instrumentation = Inspector::default();
757        let counter = instrumentation.root().create_uint("counter", 0);
758
759        let tree = FakeTree::new(FakeBehavior::Hangs)
760            .with_child("healthy", FakeTree::new(FakeBehavior::Healthy));
761
762        let result = load_snapshot_tree(&tree, FAKE_TIMEOUT, &counter).await;
763
764        assert_matches!(result, Err(ReaderError::TreeTimedOut));
765        assert_data_tree!(instrumentation, root: { counter: 1u64 });
766    }
767
768    #[fuchsia::test]
769    async fn child_vmo_error_does_not_increment_counter() {
770        let instrumentation = Inspector::default();
771        let counter = instrumentation.root().create_uint("counter", 0);
772
773        let tree = FakeTree::new(FakeBehavior::Healthy)
774            .with_child("broken", FakeTree::new(FakeBehavior::VmoError))
775            .with_child("healthy", FakeTree::new(FakeBehavior::Healthy));
776
777        let mut snapshot_tree = load_snapshot_tree(&tree, FAKE_TIMEOUT, &counter).await.unwrap();
778
779        assert_data_tree!(instrumentation, root: { counter: 0u64 });
780        assert_eq!(snapshot_tree.children.len(), 2);
781        assert_matches!(snapshot_tree.children.remove("broken"), Some(Err(ReaderError::FetchVmo)));
782        assert_matches!(snapshot_tree.children.remove("healthy"), Some(Ok(_)));
783    }
784}