1use 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
20const MAX_READ_TIME: std::time::Duration = std::time::Duration::from_secs(60 * 10);
25
26#[derive(Debug)]
28pub struct SnapshotTree {
29 snapshot: Snapshot,
30 children: SnapshotTreeMap,
31}
32
33impl SnapshotTree {
34 #[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 for child_arc in trees.children.into_inner().into_iter() {
65 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 }
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 LinkNodeDisposition::Blob => {
154 todo!(
155 "https://fxbug.dev/573703359: Implement LinkNodeDisposition::Blob expansion"
156 )
157 }
158 },
159 }
160 }
161 hierarchy
162}
163
164pub 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
173pub 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
186struct Trees<T> {
190 node: T,
191 name: Option<String>,
192 resolved_node: Mutex<Option<Result<Snapshot, ReaderError>>>,
193 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 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 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 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
258async 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
280fn 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 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 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 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 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 let mut snapshot_tree = SnapshotTree::try_from(root_trees).unwrap();
573
574 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 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 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 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, 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 let snapshot_tree = SnapshotTree::try_from(root_trees).unwrap();
628
629 assert!(snapshot_tree.children.is_empty());
632 }
633
634 #[derive(Clone, Copy, PartialEq)]
636 enum FakeBehavior {
637 Healthy,
639 Hangs,
642 VmoError,
644 }
645
646 #[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 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 assert!(elapsed < 2 * FAKE_TIMEOUT, "took {elapsed:?}");
738 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}