Skip to main content

fuchsia_inspect_contrib/nodes/
list.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 fuchsia_inspect::{Error, Node};
6use std::collections::VecDeque;
7
8/// This struct is intended to represent a list node in Inspect, which doesn't support list
9/// natively. Furthermore, it makes sure that the number of items does not exceed |capacity|
10///
11/// Each item in `BoundedListNode` is represented as a child node with name as index. This
12/// index is always increasing and does not wrap around. For example, if capacity is 3,
13/// then the children names are `[0, 1, 2]` on first three addition. When a new node is
14/// added, `0` is popped, and the children names are `[1, 2, 3]`.
15#[derive(Debug)]
16pub struct BoundedListNode {
17    node: Node,
18    index: usize,
19    capacity: usize,
20    items: VecDeque<Node>,
21}
22
23impl BoundedListNode {
24    /// Create a new BoundedListNode with capacity 1 or |capacity|, whichever is larger.
25    pub fn new(node: Node, capacity: usize) -> Self {
26        Self {
27            node,
28            index: 0,
29            capacity: std::cmp::max(capacity, 1),
30            items: VecDeque::with_capacity(capacity),
31        }
32    }
33
34    /// Returns how many children are in the `BoundedListNode`.
35    pub fn len(&self) -> usize {
36        self.items.len()
37    }
38
39    /// Returns whether or not the bounded list has no elements inside.
40    pub fn is_empty(&self) -> bool {
41        self.items.len() == 0
42    }
43
44    /// Returns the capacity of the `BoundedListNode`, the maximum number of child nodes.
45    pub fn capacity(&self) -> usize {
46        self.capacity
47    }
48
49    /// Create a new entry within a list and return a writer that creates properties or children
50    /// for this entry. The writer does not have to be kept for the created properties and
51    /// children to be maintained in the list.
52    ///
53    /// If creating new entry exceeds capacity of the list, the oldest entry is evicted.
54    ///
55    /// The `initialize` function will be used to atomically initialize all children and properties
56    /// under the node.
57    pub fn add_entry<F>(&mut self, initialize: F) -> &Node
58    where
59        F: FnOnce(&Node),
60    {
61        if self.items.len() >= self.capacity {
62            self.items.pop_front();
63        }
64
65        let entry_node = self.node.atomic_update(|node| {
66            let child = node.create_child(self.index.to_string());
67            initialize(&child);
68            child
69        });
70        self.items.push_back(entry_node);
71
72        self.index += 1;
73        self.items.back().unwrap()
74    }
75
76    /// Adopt an existing node as a new entry within a list, renaming it to the next index in the
77    /// list, and return a writer that creates properties or children for this entry. The writer
78    /// does not have to be kept for the created properties and children to be maintained in the
79    /// list.
80    ///
81    /// If adopting the entry exceeds capacity of the list, the oldest entry is evicted.
82    pub fn adopt_entry(&mut self, entry: Node) -> Result<&Node, Error> {
83        self.node.atomic_update(|node| {
84            node.adopt(&entry)?;
85            entry.rename(self.index.to_string())
86        })?;
87
88        if self.items.len() >= self.capacity {
89            self.items.pop_front();
90        }
91
92        self.items.push_back(entry);
93        self.index += 1;
94        Ok(self.items.back().unwrap())
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use assert_matches::assert_matches;
102    use diagnostics_assertions::assert_data_tree;
103    use fuchsia_inspect::Inspector;
104    use fuchsia_inspect::reader::{self, ReaderError};
105    use std::sync::mpsc;
106
107    #[fuchsia::test]
108    async fn test_bounded_list_node_basic() {
109        let inspector = Inspector::default();
110        let list_node = inspector.root().create_child("list_node");
111        let mut list_node = BoundedListNode::new(list_node, 3);
112        assert_eq!(list_node.capacity(), 3);
113        assert_eq!(list_node.len(), 0);
114        let _ = list_node.add_entry(|_| {});
115        assert_eq!(list_node.len(), 1);
116        assert_data_tree!(inspector, root: { list_node: { "0": {} } });
117        let _ = list_node.add_entry(|_| {});
118        assert_eq!(list_node.len(), 2);
119        assert_data_tree!(inspector, root: { list_node: { "0": {}, "1": {} } });
120    }
121
122    #[fuchsia::test]
123    async fn test_bounded_list_node_eviction() {
124        let inspector = Inspector::default();
125        let list_node = inspector.root().create_child("list_node");
126        let mut list_node = BoundedListNode::new(list_node, 3);
127        let _ = list_node.add_entry(|_| {});
128        let _ = list_node.add_entry(|_| {});
129        let _ = list_node.add_entry(|_| {});
130
131        assert_data_tree!(inspector, root: { list_node: { "0": {}, "1": {}, "2": {} } });
132        assert_eq!(list_node.len(), 3);
133
134        let _ = list_node.add_entry(|_| {});
135        assert_data_tree!(inspector, root: { list_node: { "1": {}, "2": {}, "3": {} } });
136        assert_eq!(list_node.len(), 3);
137
138        let _ = list_node.add_entry(|_| {});
139        assert_data_tree!(inspector, root: { list_node: { "2": {}, "3": {}, "4": {} } });
140        assert_eq!(list_node.len(), 3);
141    }
142
143    #[fuchsia::test]
144    async fn test_bounded_list_node_specified_zero_capacity() {
145        let inspector = Inspector::default();
146        let list_node = inspector.root().create_child("list_node");
147        let mut list_node = BoundedListNode::new(list_node, 0);
148        let _ = list_node.add_entry(|_| {});
149        assert_data_tree!(inspector, root: { list_node: { "0": {} } });
150        let _ = list_node.add_entry(|_| {});
151        assert_data_tree!(inspector, root: { list_node: { "1": {} } });
152    }
153
154    #[fuchsia::test]
155    async fn test_bounded_list_node_holds_its_values() {
156        let inspector = Inspector::default();
157        let list_node = inspector.root().create_child("list_node");
158        let mut list_node = BoundedListNode::new(list_node, 3);
159
160        {
161            let node_writer = list_node.add_entry(|_| {});
162            node_writer.record_string("str_key", "str_value");
163            node_writer.record_child("child", |child| child.record_int("int_key", 2));
164        } // <-- node_writer is dropped
165
166        // verify list node 0 is still in the tree
167        assert_data_tree!(inspector, root: {
168            list_node: {
169                "0": {
170                    str_key: "str_value",
171                    child: {
172                        int_key: 2i64,
173                    }
174                }
175            }
176        });
177    }
178
179    #[fuchsia::test]
180    async fn add_entry_is_atomic() {
181        let inspector = Inspector::default();
182        let list_node = inspector.root().create_child("list_node");
183        let mut list_node = BoundedListNode::new(list_node, 3);
184
185        let (sender, receiver) = mpsc::channel();
186        let (sender2, receiver2) = mpsc::channel();
187
188        let t = std::thread::spawn(move || {
189            list_node.add_entry(|node| {
190                node.record_string("key1", "value1");
191                sender.send(()).unwrap();
192                receiver2.recv().unwrap();
193                node.record_string("key2", "value2");
194            });
195            list_node
196        });
197
198        // Make sure we already called `add_entry`.
199        receiver.recv().unwrap();
200
201        // We can't read until the atomic transaction is completed.
202        assert_matches!(reader::read(&inspector).await, Err(ReaderError::InconsistentSnapshot));
203
204        // Let `add_entry` continue executing and wait for completion.
205        sender2.send(()).unwrap();
206
207        // Ensure we don't drop the list node.
208        let _list_node = t.join().unwrap();
209
210        // We can now read and we can see that everything was correctly created.
211        assert_data_tree!(inspector, root: {
212            list_node: {
213                "0": {
214                    key1: "value1",
215                    key2: "value2",
216                }
217            }
218        });
219    }
220
221    #[fuchsia::test]
222    async fn test_bounded_list_node_adopt_entry() {
223        let inspector = Inspector::default();
224        let list_node = inspector.root().create_child("list_node");
225        let mut list_node = BoundedListNode::new(list_node, 3);
226
227        let first = inspector.root().create_child("first");
228        first.record_int("val", 10);
229        assert_data_tree!(inspector, root: {
230            list_node: {},
231            first: { val: 10i64 },
232        });
233
234        let adopted = list_node.adopt_entry(first).unwrap();
235        adopted.record_string("extra", "hello");
236        assert_eq!(list_node.len(), 1);
237        assert_data_tree!(inspector, root: {
238            list_node: {
239                "0": {
240                    val: 10i64,
241                    extra: "hello",
242                },
243            },
244        });
245
246        list_node.add_entry(|n| n.record_int("val", 20));
247        let third = inspector.root().create_child("third");
248        third.record_int("val", 30);
249        list_node.adopt_entry(third).unwrap();
250        assert_eq!(list_node.len(), 3);
251        assert_data_tree!(inspector, root: {
252            list_node: {
253                "0": {
254                    val: 10i64,
255                    extra: "hello",
256                },
257                "1": {
258                    val: 20i64,
259                },
260                "2": {
261                    val: 30i64,
262                },
263            },
264        });
265
266        // Adopting a fourth entry should evict "0" and rename the adopted node to "3".
267        let fourth = inspector.root().create_child("fourth");
268        fourth.record_int("val", 40);
269        list_node.adopt_entry(fourth).unwrap();
270        assert_eq!(list_node.len(), 3);
271        assert_data_tree!(inspector, root: {
272            list_node: {
273                "1": {
274                    val: 20i64,
275                },
276                "2": {
277                    val: 30i64,
278                },
279                "3": {
280                    val: 40i64,
281                },
282            },
283        });
284    }
285
286    #[fuchsia::test]
287    async fn test_bounded_list_node_adopt_entry_error() {
288        let inspector = Inspector::default();
289        let parent = inspector.root().create_child("parent");
290        let list_node = parent.create_child("list_node");
291        let mut list_node = BoundedListNode::new(list_node, 1);
292
293        list_node.add_entry(|n| n.record_int("val", 1));
294        assert_eq!(list_node.len(), 1);
295
296        // Adopting from a different inspector VMO fails and preserves existing list state.
297        let other_inspector = Inspector::default();
298        let other_node = other_inspector.root().create_child("other");
299        assert_matches!(list_node.adopt_entry(other_node), Err(Error::AdoptionIntoWrongVmo));
300        assert_data_tree!(inspector, root: {
301            parent: {
302                list_node: {
303                    "0": {
304                        val: 1i64,
305                    },
306                },
307            },
308        });
309
310        // Adopting an ancestor fails and preserves existing list state.
311        assert_matches!(list_node.adopt_entry(parent.clone_weak()), Err(Error::AdoptAncestor));
312        assert_data_tree!(inspector, root: {
313            parent: {
314                list_node: {
315                    "0": {
316                        val: 1i64,
317                    },
318                },
319            },
320        });
321
322        // The next index is still 1.
323        let valid_node = inspector.root().create_child("valid");
324        valid_node.record_int("val", 2);
325        list_node.adopt_entry(valid_node).unwrap();
326        assert_data_tree!(inspector, root: {
327            parent: {
328                list_node: {
329                    "1": {
330                        val: 2i64,
331                    },
332                },
333            },
334        });
335    }
336}