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::borrow::Cow;
16use std::collections::BTreeMap;
17use std::pin::Pin;
18use std::sync::{Arc, Weak};
19use std::time::Duration;
20
21const MAX_READ_TIME: std::time::Duration = std::time::Duration::from_secs(60 * 10);
26
27#[derive(Debug)]
29pub struct SnapshotTree {
30 snapshot: Snapshot,
31 children: SnapshotTreeMap,
32}
33
34impl SnapshotTree {
35 #[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 for child_arc in trees.children.into_inner().into_iter() {
66 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 }
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
158pub 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
167pub 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
180struct Trees<'a, T: Clone> {
183 node: Cow<'a, T>,
184 name: Option<String>,
185 resolved_node: Mutex<Option<Result<Snapshot, ReaderError>>>,
186 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(¤t);
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 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 continue;
251 };
252 if let Some(name) = ¤t.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(¤t);
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(¤t))),
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 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 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 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 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 let mut snapshot_tree = SnapshotTree::try_from(root_trees).unwrap();
552
553 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 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 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 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, 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 let snapshot_tree = SnapshotTree::try_from(root_trees).unwrap();
609
610 assert!(snapshot_tree.children.is_empty());
613 }
614}