Skip to main content

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