fuchsia_inspect/
stats.rs

1// Copyright 2021 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//
5//! # Inspect stats node.
6//!
7//! Stats installs a lazy node (or a node with a snapshot of the current state of things)
8//! reporting stats about the Inspect being served by the component (such as size, number of
9//! dynamic children, etc) at the root of the hierarchy.
10//!
11//! Stats nodes are always at "fuchsia.inspect.Stats".
12//!
13//! # Examples
14//!
15//! ```
16//! /* This example shows automatically updated stats */
17//!
18//! use fuchsia_inspect::stats::InspectorExt;
19//!
20//! let inspector = /* the inspector of your choice */
21//! inspector.record_lazy_stats();  // A lazy node containing stats has been installed
22//! ```
23//!
24//! ```
25//! /* This example shows stats which must be manually updated */
26//! use fuchsia_inspect::stats;
27//!
28//! let inspector = ...;  // the inspector you want instrumented
29//! let stats = stats::StatsNode::new(&inspector);
30//! /* do stuff to inspector */
31//! /* ... */
32//! stats.update();  // update the stats node
33//! stats.record_data_to(root);  /* consume the stats node and persist the data */
34//! ```
35
36use super::private::InspectTypeInternal;
37use super::{Inspector, Node, Property, UintProperty};
38use futures::FutureExt;
39
40// The metric node name, as exposed by the stats node.
41const FUCHSIA_INSPECT_STATS: &str = "fuchsia.inspect.Stats";
42const CURRENT_SIZE_KEY: &str = "current_size";
43const MAXIMUM_SIZE_KEY: &str = "maximum_size";
44const UTILIZATION_PER_TEN_K_KEY: &str = "utilization_per_ten_k";
45const TOTAL_DYNAMIC_CHILDREN_KEY: &str = "total_dynamic_children";
46const ALLOCATED_BLOCKS_KEY: &str = "allocated_blocks";
47const DEALLOCATED_BLOCKS_KEY: &str = "deallocated_blocks";
48const FAILED_ALLOCATIONS_KEY: &str = "failed_allocations";
49
50/// InspectorExt provides a method for installing a "fuchsia.inspect.Stats" lazy node at the root
51/// of the Inspector's hierarchy.
52pub trait InspectorExt {
53    /// Creates a new stats node  that will expose the given `Inspector` stats.
54    fn record_lazy_stats(&self);
55}
56
57impl InspectorExt for Inspector {
58    fn record_lazy_stats(&self) {
59        let weak_root_node = self.root().clone_weak();
60        self.root().record_lazy_child(FUCHSIA_INSPECT_STATS, move || {
61            let weak_root_node = weak_root_node.clone_weak();
62            async move {
63                let local_inspector = Inspector::default();
64                let stats =
65                    StatsNode::from_nodes(weak_root_node, local_inspector.root().clone_weak());
66                stats.record_data_to(local_inspector.root());
67                Ok(local_inspector)
68            }
69            .boxed()
70        });
71    }
72}
73
74/// Contains information about inspect such as size and number of dynamic children.
75#[derive(Default)]
76pub struct StatsNode {
77    root_of_instrumented_inspector: Node,
78    stats_root: Node,
79    current_size: UintProperty,
80    maximum_size: UintProperty,
81    utilization_per_ten_k: UintProperty,
82    total_dynamic_children: UintProperty,
83    allocated_blocks: UintProperty,
84    deallocated_blocks: UintProperty,
85    failed_allocations: UintProperty,
86}
87
88impl StatsNode {
89    /// Takes a snapshot of the stats and writes them to the given parent.
90    ///
91    /// The returned `StatsNode` is RAII.
92    pub fn new(inspector: &Inspector) -> Self {
93        let stats_root = inspector.root().create_child(FUCHSIA_INSPECT_STATS);
94        Self::from_nodes(inspector.root().clone_weak(), stats_root)
95    }
96
97    /// Update the stats with the current state of the Inspector being instrumented.
98    pub fn update(&self) {
99        if let Some(stats) = self
100            .root_of_instrumented_inspector
101            .state()
102            .and_then(|outer_state| outer_state.try_lock().ok().map(|state| state.stats()))
103        {
104            // just piggy-backing on current_size; it doesn't matter how the atomic_update
105            // is triggered
106            self.current_size.atomic_update(|_| {
107                self.current_size.set(stats.current_size as u64);
108                self.maximum_size.set(stats.maximum_size as u64);
109                self.utilization_per_ten_k.set(
110                    ((stats.current_size as f64) / (stats.maximum_size as f64) * 10000.0) as u64,
111                );
112                self.total_dynamic_children.set(stats.total_dynamic_children as u64);
113                self.allocated_blocks.set(stats.allocated_blocks as u64);
114                self.deallocated_blocks.set(stats.deallocated_blocks as u64);
115                self.failed_allocations.set(stats.failed_allocations as u64);
116            });
117        }
118    }
119
120    /// Tie the lifetime of the statistics to the provided `fuchsia_inspect::Node`.
121    pub fn record_data_to(self, lifetime: &Node) {
122        lifetime.record(self.stats_root);
123        lifetime.record(self.current_size);
124        lifetime.record(self.maximum_size);
125        lifetime.record(self.utilization_per_ten_k);
126        lifetime.record(self.total_dynamic_children);
127        lifetime.record(self.allocated_blocks);
128        lifetime.record(self.deallocated_blocks);
129        lifetime.record(self.failed_allocations);
130    }
131
132    /// Write stats from the state of `root_of_instrumented_inspector` to `stats_root`
133    fn from_nodes(root_of_instrumented_inspector: Node, stats_root: Node) -> Self {
134        if let Some(stats) = root_of_instrumented_inspector
135            .state()
136            .and_then(|outer_state| outer_state.try_lock().ok().map(|state| state.stats()))
137        {
138            let mut n = stats_root.atomic_update(|stats_root| StatsNode {
139                root_of_instrumented_inspector,
140                current_size: stats_root.create_uint(CURRENT_SIZE_KEY, stats.current_size as u64),
141                maximum_size: stats_root.create_uint(MAXIMUM_SIZE_KEY, stats.maximum_size as u64),
142                utilization_per_ten_k: stats_root.create_uint(
143                    UTILIZATION_PER_TEN_K_KEY,
144                    ((stats.current_size as f64) / (stats.maximum_size as f64) * 10000.0) as u64,
145                ),
146                total_dynamic_children: stats_root
147                    .create_uint(TOTAL_DYNAMIC_CHILDREN_KEY, stats.total_dynamic_children as u64),
148                allocated_blocks: stats_root
149                    .create_uint(ALLOCATED_BLOCKS_KEY, stats.allocated_blocks as u64),
150                deallocated_blocks: stats_root
151                    .create_uint(DEALLOCATED_BLOCKS_KEY, stats.deallocated_blocks as u64),
152                failed_allocations: stats_root
153                    .create_uint(FAILED_ALLOCATIONS_KEY, stats.failed_allocations as u64),
154                ..StatsNode::default()
155            });
156            n.stats_root = stats_root;
157            n
158        } else {
159            StatsNode { root_of_instrumented_inspector, ..StatsNode::default() }
160        }
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use diagnostics_assertions::{assert_data_tree, assert_json_diff};
168    use inspect_format::constants;
169
170    #[fuchsia::test]
171    fn inspect_stats() {
172        let inspector = Inspector::default();
173        inspector.record_lazy_stats();
174
175        assert_data_tree!(inspector, root: {
176            "fuchsia.inspect.Stats": {
177                current_size: 4096u64,
178                maximum_size: constants::DEFAULT_VMO_SIZE_BYTES as u64,
179                utilization_per_ten_k: 156u64,
180                total_dynamic_children: 1u64,  // snapshot was taken before adding any lazy node.
181                allocated_blocks: 4u64,
182                deallocated_blocks: 0u64,
183                failed_allocations: 0u64,
184            },
185        });
186
187        inspector.root().record_lazy_child("foo", || {
188            async move {
189                let inspector = Inspector::default();
190                inspector.root().record_uint("a", 1);
191                Ok(inspector)
192            }
193            .boxed()
194        });
195        assert_data_tree!(inspector, root: {
196            foo: {
197                a: 1u64,
198            },
199            "fuchsia.inspect.Stats": {
200                current_size: 4096u64,
201                maximum_size: constants::DEFAULT_VMO_SIZE_BYTES as u64,
202                utilization_per_ten_k: 156u64,
203                total_dynamic_children: 2u64,
204                allocated_blocks: 7u64,
205                deallocated_blocks: 0u64,
206                failed_allocations: 0u64,
207            },
208        });
209
210        for i in 0..100 {
211            inspector.root().record_string(format!("testing-{}", i), "testing".repeat(i + 1));
212        }
213
214        {
215            let _ = inspector.root().create_int("drop", 1);
216        }
217
218        assert_data_tree!(inspector, root: contains {
219            "fuchsia.inspect.Stats": {
220                current_size: 61440u64,
221                maximum_size: constants::DEFAULT_VMO_SIZE_BYTES as u64,
222                utilization_per_ten_k: 2343u64,
223                total_dynamic_children: 2u64,
224                allocated_blocks: 309u64,
225                // 2 blocks are deallocated because of the "drop" int block and its
226                // STRING_REFERENCE
227                deallocated_blocks: 2u64,
228                failed_allocations: 0u64,
229            }
230        });
231
232        for i in 101..220 {
233            inspector.root().record_string(format!("testing-{}", i), "testing".repeat(i + 1));
234        }
235
236        assert_data_tree!(inspector, root: contains {
237            "fuchsia.inspect.Stats": {
238                current_size: 262144u64,
239                maximum_size: constants::DEFAULT_VMO_SIZE_BYTES as u64,
240                utilization_per_ten_k: 10000u64,
241                total_dynamic_children: 2u64,
242                allocated_blocks: 664u64,
243                deallocated_blocks: 4u64,
244                failed_allocations: 2u64,
245            }
246        });
247    }
248
249    #[fuchsia::test]
250    fn stats_are_updated() {
251        let inspector = Inspector::default();
252        let stats = super::StatsNode::new(&inspector);
253        assert_json_diff!(inspector, root: {
254            "fuchsia.inspect.Stats": {
255                current_size: 4096u64,
256                maximum_size: constants::DEFAULT_VMO_SIZE_BYTES as u64,
257                utilization_per_ten_k: 156u64,
258                total_dynamic_children: 0u64,
259                allocated_blocks: 3u64,
260                deallocated_blocks: 0u64,
261                failed_allocations: 0u64,
262            }
263        });
264
265        inspector.root().record_int("abc", 5);
266
267        // asserting that everything is the same, since we didn't call `stats.update()`
268        assert_json_diff!(inspector, root: {
269            abc: 5i64,
270            "fuchsia.inspect.Stats": {
271                current_size: 4096u64,
272                maximum_size: constants::DEFAULT_VMO_SIZE_BYTES as u64,
273                utilization_per_ten_k: 156u64,
274                total_dynamic_children: 0u64,
275                allocated_blocks: 3u64,
276                deallocated_blocks: 0u64,
277                failed_allocations: 0u64,
278            }
279        });
280
281        stats.update();
282
283        assert_json_diff!(inspector, root: {
284            abc: 5i64,
285            "fuchsia.inspect.Stats": {
286                current_size: 4096u64,
287                maximum_size: constants::DEFAULT_VMO_SIZE_BYTES as u64,
288                utilization_per_ten_k: 156u64,
289                total_dynamic_children: 0u64,
290                allocated_blocks: 19u64,
291                deallocated_blocks: 0u64,
292                failed_allocations: 0u64,
293            }
294        });
295    }
296
297    #[fuchsia::test]
298    fn recorded_stats_are_persisted() {
299        let inspector = Inspector::default();
300        {
301            let stats = super::StatsNode::new(&inspector);
302            stats.record_data_to(inspector.root());
303        }
304
305        assert_data_tree!(inspector, root: {
306            "fuchsia.inspect.Stats": {
307                current_size: 4096u64,
308                maximum_size: constants::DEFAULT_VMO_SIZE_BYTES as u64,
309                utilization_per_ten_k: 156u64,
310                total_dynamic_children: 0u64,
311                allocated_blocks: 3u64,
312                deallocated_blocks: 0u64,
313                failed_allocations: 0u64,
314            }
315        });
316    }
317}