Skip to main content

netcfg/telemetry/processors/
network_properties.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 crate::telemetry::NetworkEventMetadata;
6use fuchsia_inspect::Node as InspectNode;
7use fuchsia_inspect_contrib::nodes::LruCacheNode;
8use fuchsia_inspect_derive::Unit;
9use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
10use windowed_stats::experimental::series::interpolation::LastSample;
11use windowed_stats::experimental::series::metadata::BitSetNode;
12use windowed_stats::experimental::series::statistic::Union;
13use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
14
15pub struct NetworkPropertiesProcessor {
16    default_network_detailed_matrix: InspectedTimeMatrix<u64>,
17    default_network_type_matrix: InspectedTimeMatrix<u64>,
18    inspect_metadata_node: InspectMetadataNode,
19}
20
21const METADATA_NODE_NAME: &str = "metadata";
22
23impl NetworkPropertiesProcessor {
24    pub fn new<S: InspectSender>(parent: &InspectNode, parent_path: &str, client: &S) -> Self {
25        let inspect_metadata_node = parent.create_child(METADATA_NODE_NAME);
26        let inspect_metadata_path = format!("{}/{}", parent_path, METADATA_NODE_NAME);
27        let detailed_time_matrix = TimeMatrix::<Union<u64>, LastSample>::new(
28            SamplingProfile::granular(),
29            LastSample::or(0),
30        );
31        let default_network_detailed_matrix = client.inspect_time_matrix_with_metadata(
32            "default_network_detailed",
33            detailed_time_matrix,
34            BitSetNode::from_path(format!(
35                "{}/{}",
36                inspect_metadata_path,
37                InspectMetadataNode::NETWORK_REGISTRY
38            )),
39        );
40
41        let types_time_matrix = TimeMatrix::<Union<u64>, LastSample>::new(
42            SamplingProfile::granular(),
43            LastSample::or(0),
44        );
45        let default_network_type_matrix = client.inspect_time_matrix_with_metadata(
46            "default_network_type",
47            types_time_matrix,
48            BitSetNode::from_path(format!(
49                "{}/{}",
50                inspect_metadata_path,
51                InspectMetadataNode::NETWORK_TYPES
52            )),
53        );
54
55        Self {
56            default_network_detailed_matrix,
57            default_network_type_matrix,
58            inspect_metadata_node: InspectMetadataNode::new(inspect_metadata_node),
59        }
60    }
61
62    pub fn log_default_network_lost(&mut self) {
63        self.default_network_detailed_matrix.fold_or_log_error(0);
64        self.default_network_type_matrix.fold_or_log_error(0);
65    }
66
67    pub fn log_default_network_changed(&mut self, metadata: NetworkEventMetadata) {
68        let data = NetworkData::from(metadata);
69        let types_mapped_id =
70            self.inspect_metadata_node.network_types.insert(data.transport.clone());
71        self.default_network_type_matrix.fold_or_log_error(1u64 << types_mapped_id);
72
73        let detailed_mapped_id = self.inspect_metadata_node.network_registry.insert(data);
74        self.default_network_detailed_matrix.fold_or_log_error(1u64 << detailed_mapped_id);
75    }
76}
77
78#[derive(Unit, PartialEq, Eq, Hash)]
79struct NetworkData {
80    pub id: u64,
81    pub name: String,
82    pub transport: String,
83    pub is_fuchsia_provisioned: bool,
84}
85
86impl From<NetworkEventMetadata> for NetworkData {
87    fn from(metadata: NetworkEventMetadata) -> Self {
88        let NetworkEventMetadata { id, name, transport, is_fuchsia_provisioned } = metadata;
89        Self {
90            id: id,
91            name: name.unwrap_or_else(|| "unknown".to_string()),
92            transport: format!("{:?}", transport),
93            is_fuchsia_provisioned,
94        }
95    }
96}
97
98const NETWORKS_METADATA_CACHE_SIZE: usize = 16;
99const NETWORK_TYPES_CACHE_SIZE: usize = 8;
100
101// Holds the inspect node children for the metadata that correlates to
102// bits in the default network bitsets.
103struct InspectMetadataNode {
104    _node: InspectNode,
105    network_registry: LruCacheNode<NetworkData>,
106    network_types: LruCacheNode<String>,
107}
108
109impl InspectMetadataNode {
110    const NETWORK_REGISTRY: &'static str = "network_registry";
111    const NETWORK_TYPES: &'static str = "network_types";
112
113    fn new(inspect_node: InspectNode) -> Self {
114        // Record the network registry, which is dynamically updated as networks
115        // are added.
116        let network_registry = LruCacheNode::new(
117            inspect_node.create_child(Self::NETWORK_REGISTRY),
118            NETWORKS_METADATA_CACHE_SIZE,
119        );
120
121        // Record the observed network types for the default network type time matrix.
122        let network_types = LruCacheNode::new(
123            inspect_node.create_child(Self::NETWORK_TYPES),
124            NETWORK_TYPES_CACHE_SIZE,
125        );
126
127        Self { _node: inspect_node, network_registry, network_types }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use diagnostics_assertions::{AnyBytesProperty, assert_data_tree};
135    use fidl_fuchsia_net_policy_socketproxy as fnp_socketproxy;
136    use fuchsia_inspect::Inspector;
137    use fuchsia_inspect::reader::DiagnosticsHierarchy;
138    use futures::task::Poll;
139    use std::pin::pin;
140    use windowed_stats::experimental::clock::Timed;
141    use windowed_stats::experimental::inspect::TimeMatrixClient;
142    use windowed_stats::experimental::testing::{MockTimeMatrixClient, TimeMatrixCall};
143
144    pub struct TestHelper {
145        pub inspector: Inspector,
146        pub inspect_node: InspectNode,
147        pub parent_path: String,
148        pub mock_time_matrix_client: MockTimeMatrixClient,
149
150        // Note: keep the executor field last in the struct so it gets dropped last.
151        pub exec: fuchsia_async::TestExecutor,
152    }
153
154    impl TestHelper {
155        pub fn get_inspect_data_tree(&mut self) -> DiagnosticsHierarchy {
156            let read_fut = fuchsia_inspect::reader::read(&self.inspector);
157            let mut read_fut = pin!(read_fut);
158            match self.exec.run_until_stalled(&mut read_fut) {
159                Poll::Pending => {
160                    panic!("Unexpected pending state");
161                }
162                Poll::Ready(result) => result.expect("failed to get hierarchy"),
163            }
164        }
165    }
166
167    pub fn setup_test() -> TestHelper {
168        let exec = fuchsia_async::TestExecutor::new_with_fake_time();
169        exec.set_fake_time(fuchsia_async::MonotonicInstant::from_nanos(0));
170
171        let inspector = Inspector::default();
172        let inspect_node = inspector.root().create_child("test_stats");
173        let parent_path = "root/test_stats".to_string();
174
175        TestHelper {
176            inspector,
177            inspect_node,
178            parent_path,
179            mock_time_matrix_client: MockTimeMatrixClient::new(),
180            exec,
181        }
182    }
183
184    fn log_network_events(processor: &mut NetworkPropertiesProcessor) {
185        let eth_metadata = NetworkEventMetadata {
186            id: 0,
187            name: Some("eth0".to_string()),
188            transport: fnp_socketproxy::NetworkType::Ethernet,
189            is_fuchsia_provisioned: true,
190        };
191
192        let wlan_metadata = NetworkEventMetadata {
193            id: 1,
194            name: Some("wlan0".to_string()),
195            transport: fnp_socketproxy::NetworkType::Wifi,
196            is_fuchsia_provisioned: false,
197        };
198
199        processor.log_default_network_changed(eth_metadata);
200        processor.log_default_network_lost();
201        processor.log_default_network_changed(wlan_metadata);
202    }
203
204    #[fuchsia::test]
205    fn log_default_network_time_series_calls() {
206        let harness = setup_test();
207        let mut processor = NetworkPropertiesProcessor::new(
208            &harness.inspect_node,
209            &harness.parent_path,
210            &harness.mock_time_matrix_client,
211        );
212        log_network_events(&mut processor);
213
214        let mut time_matrix_calls = harness.mock_time_matrix_client.drain_calls();
215        assert_eq!(
216            &time_matrix_calls.drain::<u64>("default_network_detailed")[..],
217            &[
218                TimeMatrixCall::Fold(Timed::now(1 << 0)),
219                TimeMatrixCall::Fold(Timed::now(0)),
220                TimeMatrixCall::Fold(Timed::now(1 << 1)),
221            ]
222        );
223        assert_eq!(
224            &time_matrix_calls.drain::<u64>("default_network_type")[..],
225            &[
226                TimeMatrixCall::Fold(Timed::now(1 << 0)),
227                TimeMatrixCall::Fold(Timed::now(0)),
228                TimeMatrixCall::Fold(Timed::now(1 << 1)),
229            ]
230        );
231    }
232
233    #[fuchsia::test]
234    fn log_default_network_inspect_tree() {
235        let mut harness = setup_test();
236        let time_matrix_client =
237            TimeMatrixClient::new(harness.inspect_node.create_child("time_series"));
238        let mut processor = NetworkPropertiesProcessor::new(
239            &harness.inspect_node,
240            &harness.parent_path,
241            &time_matrix_client,
242        );
243        log_network_events(&mut processor);
244
245        let hierarchy = harness.get_inspect_data_tree();
246
247        assert_data_tree!(
248            @executor harness.exec,
249            hierarchy,
250            root: contains {
251                test_stats: contains {
252                    metadata: contains {
253                        network_registry: contains {
254                            "0": contains {
255                                data: {
256                                    id: 0u64,
257                                    name: "eth0",
258                                    transport: "Ethernet",
259                                    is_fuchsia_provisioned: true,
260                                }
261                            },
262                            "1": contains {
263                                data: {
264                                    id: 1u64,
265                                    name: "wlan0",
266                                    transport: "Wifi",
267                                    is_fuchsia_provisioned: false,
268                                }
269                            }
270                        },
271                        network_types: contains {
272                            "0": contains {
273                                data: "Ethernet",
274                            },
275                            "1": contains {
276                                data: "Wifi",
277                            }
278                        }
279                    },
280                    time_series: contains {
281                        default_network_detailed: {
282                            "type": "bitset",
283                            "data": AnyBytesProperty,
284                            metadata: {
285                                index_node_path: "root/test_stats/metadata/network_registry",
286                            }
287                        },
288                        default_network_type: {
289                            "type": "bitset",
290                            "data": AnyBytesProperty,
291                            metadata: {
292                                index_node_path: "root/test_stats/metadata/network_types",
293                            }
294                        },
295                    }
296                }
297            }
298        );
299    }
300}