Skip to main content

fuchsia_inspect_contrib/nodes/
dedupe_log.rs

1// Copyright 2026 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 std::collections::VecDeque;
6
7use fuchsia_inspect::{Node, NumericProperty, UintProperty};
8use fuchsia_inspect_derive::Unit;
9
10use crate::nodes::{BootTimeProperty, NodeTimeExt};
11
12/// The name of the property that holds the time at which the log entry was created.
13/// This property is used by toolings and should not be changed without considering
14/// how it affects toolings.
15const CREATED_AT_PROPERTY_NAME: &str = "Created@time";
16/// The name of the property that holds the time at which the log entry was last seen.
17/// This property is used by toolings and should not be changed without considering
18/// how it affects toolings.
19const LAST_SEEN_AT_PROPERTY_NAME: &str = "LastSeen@time";
20
21struct EntryData<T: Unit> {
22    _node: Node,
23    count: UintProperty,
24    last_seen: Option<BootTimeProperty>,
25    _time: BootTimeProperty,
26    _log: T::Data,
27}
28
29/// A Inspect node that holds an ordered, bounded list of log entries, with
30/// the ability to dedupe consecutive logs.
31pub struct DedupeLogNode<T: Unit + PartialEq> {
32    node: Node,
33    last_log: Option<T>,
34    entries: VecDeque<EntryData<T>>,
35    next_index: u64,
36    capacity: usize,
37}
38
39impl<T: Unit + PartialEq> DedupeLogNode<T> {
40    pub fn new(node: Node, capacity: usize) -> Self {
41        let capacity = std::cmp::max(capacity, 1);
42        Self {
43            node,
44            last_log: None,
45            entries: VecDeque::with_capacity(capacity),
46            next_index: 0,
47            capacity,
48        }
49    }
50
51    /// Insert |log| into `DedupeLogNode`.
52    ///
53    /// If |log| is equivalent to the last inserted log, the last inserted log
54    /// entry's count is incremented and last seen time is updated. If the new
55    /// log is not equivalent to the last inserted log, a new log entry is
56    /// inserted, and the oldest log entry is removed if capacity is exceeded.
57    pub fn insert(&mut self, log: T) {
58        if self.last_log.as_ref() == Some(&log) {
59            if let Some(entry) = self.entries.back_mut() {
60                entry.count.add(1);
61                if let Some(time_prop) = &entry.last_seen {
62                    time_prop.update();
63                } else {
64                    let now = zx::BootInstant::get();
65                    entry.last_seen =
66                        Some(entry._node.create_time_at(LAST_SEEN_AT_PROPERTY_NAME, now));
67                }
68            }
69            return;
70        }
71
72        if self.entries.len() >= self.capacity {
73            self.entries.pop_front();
74        }
75
76        let index_str = self.next_index.to_string();
77        self.next_index += 1;
78
79        let child_node = self.node.create_child(&index_str);
80
81        let now = zx::BootInstant::get();
82        let time_prop = child_node.create_time_at(CREATED_AT_PROPERTY_NAME, now);
83        let count_prop = child_node.create_uint("count", 1);
84        let log_data = log.inspect_create(&child_node, "log");
85
86        self.entries.push_back(EntryData {
87            _node: child_node,
88            count: count_prop,
89            last_seen: None,
90            _log: log_data,
91            _time: time_prop,
92        });
93
94        self.last_log = Some(log);
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use diagnostics_assertions::{AnyNumericProperty, assert_data_tree};
102    use fuchsia_inspect::Inspector;
103
104    #[fuchsia::test]
105    async fn test_insert_unique_items() {
106        let inspector = Inspector::default();
107        let log_node = inspector.root().create_child("log");
108        let mut dedupe_log = DedupeLogNode::new(log_node, 3);
109
110        dedupe_log.insert(111);
111        dedupe_log.insert(222);
112        dedupe_log.insert(333);
113
114        assert_data_tree!(inspector, root: {
115            log: {
116                "0": {
117                    "Created@time": AnyNumericProperty,
118                    count: 1u64,
119                    log: 111i64,
120                },
121                "1": {
122                    "Created@time": AnyNumericProperty,
123                    count: 1u64,
124                    log: 222i64,
125                },
126                "2": {
127                    "Created@time": AnyNumericProperty,
128                    count: 1u64,
129                    log: 333i64,
130                },
131            }
132        });
133    }
134
135    #[fuchsia::test]
136    async fn test_deduplication() {
137        let inspector = Inspector::default();
138        let log_node = inspector.root().create_child("log");
139        let mut dedupe_log = DedupeLogNode::new(log_node, 3);
140
141        dedupe_log.insert(111);
142        dedupe_log.insert(111);
143        dedupe_log.insert(111);
144
145        assert_data_tree!(inspector, root: {
146            log: {
147                "0": {
148                    "Created@time": AnyNumericProperty,
149                    "LastSeen@time": AnyNumericProperty,
150                    count: 3u64,
151                    log: 111i64,
152                },
153            }
154        });
155
156        // Insert a new item, then another dedupe
157        dedupe_log.insert(222);
158        dedupe_log.insert(222);
159
160        assert_data_tree!(inspector, root: {
161            log: {
162                "0": {
163                    "Created@time": AnyNumericProperty,
164                    "LastSeen@time": AnyNumericProperty,
165                    count: 3u64,
166                    log: 111i64,
167                },
168                "1": {
169                    "Created@time": AnyNumericProperty,
170                    "LastSeen@time": AnyNumericProperty,
171                    count: 2u64,
172                    log: 222i64,
173                },
174            }
175        });
176    }
177
178    #[fuchsia::test]
179    async fn test_eviction() {
180        let inspector = Inspector::default();
181        let log_node = inspector.root().create_child("log");
182        let mut dedupe_log = DedupeLogNode::new(log_node, 2);
183
184        dedupe_log.insert(111);
185        dedupe_log.insert(222);
186        dedupe_log.insert(333); // This will evict 111
187
188        assert_data_tree!(inspector, root: {
189            log: {
190                "1": {
191                    "Created@time": AnyNumericProperty,
192                    count: 1u64,
193                    log: 222i64,
194                },
195                "2": {
196                    "Created@time": AnyNumericProperty,
197                    count: 1u64,
198                    log: 333i64,
199                },
200            }
201        });
202    }
203
204    #[derive(PartialEq, Unit)]
205    struct Item {
206        num: u64,
207        string: String,
208    }
209
210    #[fuchsia::test]
211    async fn test_insert_custom_struct() {
212        let inspector = Inspector::default();
213        let log_node = inspector.root().create_child("log");
214        let mut dedupe_log = DedupeLogNode::new(log_node, 3);
215
216        dedupe_log.insert(Item { num: 1337u64, string: "42".to_string() });
217        dedupe_log.insert(Item { num: 1337u64, string: "42".to_string() }); // Deduplicates
218
219        assert_data_tree!(inspector, root: {
220            log: {
221                "0": {
222                    "Created@time": AnyNumericProperty,
223                    "LastSeen@time": AnyNumericProperty,
224                    count: 2u64,
225                    log: { num: 1337u64, string: "42".to_string() }
226                },
227            }
228        });
229    }
230}