1use crate::telemetry::NetworkEventMetadata;
6use fidl_fuchsia_net_policy_socketproxy as fnp_socketproxy;
7use fuchsia_inspect::Node as InspectNode;
8use fuchsia_inspect_contrib::nodes::LruCacheNode;
9use fuchsia_inspect_derive::Unit;
10use windowed_stats::experimental::inspect::{InspectSender, InspectedTimeMatrix};
11use windowed_stats::experimental::series::interpolation::LastSample;
12use windowed_stats::experimental::series::metadata::{BitSetMap, BitSetNode};
13use windowed_stats::experimental::series::statistic::Union;
14use windowed_stats::experimental::series::{SamplingProfile, TimeMatrix};
15
16pub struct NetworkPropertiesProcessor<S: InspectSender> {
17 default_network_detailed_matrix: InspectedTimeMatrix<u64>,
18 default_network_type_matrix: InspectedTimeMatrix<u64>,
19 inspect_metadata_node: InspectMetadataNode,
20 connectivity_matrices: Vec<Option<NetworkConnectivityTimeSeries<S>>>,
21 inspect_metadata_path: String,
22}
23
24struct NetworkConnectivityTimeSeries<S> {
25 network_id: u64,
26 _client: S,
27 matrix: InspectedTimeMatrix<u64>,
28}
29
30const METADATA_NODE_NAME: &str = "metadata";
31
32impl<S: InspectSender> NetworkPropertiesProcessor<S> {
33 pub fn new(parent: &InspectNode, parent_path: &str, client: &S) -> Self {
34 let inspect_metadata_node = parent.create_child(METADATA_NODE_NAME);
35 let inspect_metadata_path = format!("{}/{}", parent_path, METADATA_NODE_NAME);
36 let detailed_time_matrix = TimeMatrix::<Union<u64>, LastSample>::new(
37 SamplingProfile::granular(),
38 LastSample::or(0),
39 );
40 let default_network_detailed_matrix = client.inspect_time_matrix_with_metadata(
41 "default_network_detailed",
42 detailed_time_matrix,
43 BitSetNode::from_path(format!(
44 "{}/{}",
45 inspect_metadata_path,
46 InspectMetadataNode::NETWORK_REGISTRY
47 )),
48 );
49
50 let types_time_matrix = TimeMatrix::<Union<u64>, LastSample>::new(
51 SamplingProfile::granular(),
52 LastSample::or(0),
53 );
54 let default_network_type_matrix = client.inspect_time_matrix_with_metadata(
55 "default_network_type",
56 types_time_matrix,
57 BitSetNode::from_path(format!(
58 "{}/{}",
59 inspect_metadata_path,
60 InspectMetadataNode::NETWORK_TYPES
61 )),
62 );
63
64 let mut connectivity_matrices = Vec::with_capacity(NETWORKS_METADATA_CACHE_SIZE);
65 for _ in 0..NETWORKS_METADATA_CACHE_SIZE {
66 connectivity_matrices.push(None);
67 }
68
69 Self {
70 default_network_detailed_matrix,
71 default_network_type_matrix,
72 inspect_metadata_node: InspectMetadataNode::new(inspect_metadata_node),
73 connectivity_matrices,
74 inspect_metadata_path,
75 }
76 }
77
78 pub fn log_default_network_lost(&mut self) {
79 self.default_network_detailed_matrix.fold_or_log_error(0);
80 self.default_network_type_matrix.fold_or_log_error(0);
81 }
82
83 pub fn log_default_network_changed(&mut self, metadata: NetworkEventMetadata) {
84 let data = NetworkData::from(metadata);
85 let types_mapped_id =
86 self.inspect_metadata_node.network_types.insert(data.transport.clone());
87 self.default_network_type_matrix.fold_or_log_error(1u64 << types_mapped_id);
88
89 let detailed_mapped_id = self.inspect_metadata_node.network_registry.insert(data);
90 self.default_network_detailed_matrix.fold_or_log_error(1u64 << detailed_mapped_id);
91 }
92
93 pub fn log_network_changed(
94 &mut self,
95 metadata: crate::telemetry::NetworkEventMetadata,
96 client: &S,
97 ) {
98 let network_id = metadata.id;
99 let connectivity_state = metadata.connectivity_state;
100
101 let data = NetworkData::from(metadata);
102 let detailed_mapped_id = self.inspect_metadata_node.network_registry.insert(data);
103
104 let connectivity_state = match connectivity_state {
105 Some(state) => state,
106 None => return,
107 };
108
109 let bit_index = match connectivity_state_to_bit_index(connectivity_state) {
110 Some(index) => index,
111 None => return,
112 };
113
114 let needs_new_matrix = match self.connectivity_matrices.get(detailed_mapped_id) {
116 Some(Some(time_series)) => time_series.network_id != network_id,
117 Some(None) | None => true,
118 };
119
120 if needs_new_matrix {
121 self.connectivity_matrices[detailed_mapped_id] = None;
123
124 let node_name = format!("network_{}", network_id);
125 let scoped_client = client.clone_with_child(&node_name);
126
127 let time_matrix = TimeMatrix::<Union<u64>, LastSample>::new(
128 SamplingProfile::highly_granular(),
129 LastSample::or(0),
130 );
131
132 let matrix = scoped_client.inspect_time_matrix_with_metadata(
133 "connectivity",
134 time_matrix,
135 BitSetNode::from_path(format!(
136 "{}/connectivity_states",
137 self.inspect_metadata_path
138 )),
139 );
140
141 self.connectivity_matrices[detailed_mapped_id] =
142 Some(NetworkConnectivityTimeSeries { network_id, _client: scoped_client, matrix });
143 }
144
145 if let Some(ts) = &self.connectivity_matrices[detailed_mapped_id] {
146 ts.matrix.fold_or_log_error(1u64 << bit_index);
147 }
148 }
149}
150
151#[derive(Unit, PartialEq, Eq, Hash)]
152struct NetworkData {
153 pub id: u64,
154 pub name: String,
155 pub transport: String,
156 pub is_fuchsia_provisioned: bool,
157}
158
159impl From<NetworkEventMetadata> for NetworkData {
160 fn from(metadata: NetworkEventMetadata) -> Self {
161 let NetworkEventMetadata { id, name, transport, is_fuchsia_provisioned, .. } = metadata;
162 Self {
163 id: id,
164 name: name.unwrap_or_else(|| "unknown".to_string()),
165 transport: format!("{:?}", transport),
166 is_fuchsia_provisioned,
167 }
168 }
169}
170
171fn get_ordered_connectivity_states() -> [&'static str; 4] {
172 ["NoConnectivity", "LocalConnectivity", "PartialConnectivity", "FullConnectivity"]
173}
174
175fn connectivity_state_to_bit_index(state: fnp_socketproxy::ConnectivityState) -> Option<u8> {
176 match state {
177 fnp_socketproxy::ConnectivityState::NoConnectivity => Some(0),
178 fnp_socketproxy::ConnectivityState::LocalConnectivity => Some(1),
179 fnp_socketproxy::ConnectivityState::PartialConnectivity => Some(2),
180 fnp_socketproxy::ConnectivityState::FullConnectivity => Some(3),
181 _ => None,
182 }
183}
184
185const NETWORKS_METADATA_CACHE_SIZE: usize = 16;
186const NETWORK_TYPES_CACHE_SIZE: usize = 8;
187
188struct InspectMetadataNode {
191 _node: InspectNode,
192 network_registry: LruCacheNode<NetworkData>,
193 network_types: LruCacheNode<String>,
194 _connectivity_states: InspectNode,
195}
196
197impl InspectMetadataNode {
198 const NETWORK_REGISTRY: &'static str = "network_registry";
199 const NETWORK_TYPES: &'static str = "network_types";
200
201 fn new(inspect_node: InspectNode) -> Self {
202 let network_registry = LruCacheNode::new(
205 inspect_node.create_child(Self::NETWORK_REGISTRY),
206 NETWORKS_METADATA_CACHE_SIZE,
207 );
208
209 let network_types = LruCacheNode::new(
211 inspect_node.create_child(Self::NETWORK_TYPES),
212 NETWORK_TYPES_CACHE_SIZE,
213 );
214
215 let connectivity_states = inspect_node.create_child("connectivity_states");
216 let connectivity_metadata = BitSetMap::from_ordered(get_ordered_connectivity_states());
217 connectivity_metadata.record(&connectivity_states);
218
219 Self {
220 _node: inspect_node,
221 network_registry,
222 network_types,
223 _connectivity_states: connectivity_states,
224 }
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use diagnostics_assertions::{AnyBytesProperty, assert_data_tree};
232 use fidl_fuchsia_net_policy_socketproxy as fnp_socketproxy;
233 use fuchsia_inspect::Inspector;
234 use fuchsia_inspect::reader::DiagnosticsHierarchy;
235 use futures::task::Poll;
236 use std::pin::pin;
237 use windowed_stats::experimental::clock::Timed;
238 use windowed_stats::experimental::inspect::TimeMatrixClient;
239 use windowed_stats::experimental::testing::{MockTimeMatrixClient, TimeMatrixCall};
240
241 pub struct TestHelper {
242 pub inspector: Inspector,
243 pub inspect_node: InspectNode,
244 pub parent_path: String,
245 pub mock_time_matrix_client: MockTimeMatrixClient,
246
247 pub exec: fuchsia_async::TestExecutor,
249 }
250
251 impl TestHelper {
252 pub fn get_inspect_data_tree(&mut self) -> DiagnosticsHierarchy {
253 let read_fut = fuchsia_inspect::reader::read(&self.inspector);
254 let mut read_fut = pin!(read_fut);
255 match self.exec.run_until_stalled(&mut read_fut) {
256 Poll::Pending => {
257 panic!("Unexpected pending state");
258 }
259 Poll::Ready(result) => result.expect("failed to get hierarchy"),
260 }
261 }
262 }
263
264 pub fn setup_test() -> TestHelper {
265 let exec = fuchsia_async::TestExecutor::new_with_fake_time();
266 exec.set_fake_time(fuchsia_async::MonotonicInstant::from_nanos(0));
267
268 let inspector = Inspector::default();
269 let inspect_node = inspector.root().create_child("test_stats");
270 let parent_path = "root/test_stats".to_string();
271
272 TestHelper {
273 inspector,
274 inspect_node,
275 parent_path,
276 mock_time_matrix_client: MockTimeMatrixClient::new(),
277 exec,
278 }
279 }
280
281 fn log_network_events<S: InspectSender>(processor: &mut NetworkPropertiesProcessor<S>) {
282 let eth_metadata = NetworkEventMetadata {
283 id: 0,
284 name: Some("eth0".to_string()),
285 transport: fnp_socketproxy::NetworkType::Ethernet,
286 is_fuchsia_provisioned: true,
287 connectivity_state: None,
288 };
289
290 let wlan_metadata = NetworkEventMetadata {
291 id: 1,
292 name: Some("wlan0".to_string()),
293 transport: fnp_socketproxy::NetworkType::Wifi,
294 is_fuchsia_provisioned: false,
295 connectivity_state: None,
296 };
297
298 processor.log_default_network_changed(eth_metadata);
299 processor.log_default_network_lost();
300 processor.log_default_network_changed(wlan_metadata);
301 }
302
303 #[fuchsia::test]
304 fn log_default_network_time_series_calls() {
305 let harness = setup_test();
306 let mut processor = NetworkPropertiesProcessor::new(
307 &harness.inspect_node,
308 &harness.parent_path,
309 &harness.mock_time_matrix_client,
310 );
311 log_network_events(&mut processor);
312
313 let mut time_matrix_calls = harness.mock_time_matrix_client.drain_calls();
314 assert_eq!(
315 &time_matrix_calls.drain::<u64>("default_network_detailed")[..],
316 &[
317 TimeMatrixCall::Fold(Timed::now(1 << 0)),
318 TimeMatrixCall::Fold(Timed::now(0)),
319 TimeMatrixCall::Fold(Timed::now(1 << 1)),
320 ]
321 );
322 assert_eq!(
323 &time_matrix_calls.drain::<u64>("default_network_type")[..],
324 &[
325 TimeMatrixCall::Fold(Timed::now(1 << 0)),
326 TimeMatrixCall::Fold(Timed::now(0)),
327 TimeMatrixCall::Fold(Timed::now(1 << 1)),
328 ]
329 );
330 }
331
332 #[fuchsia::test]
333 fn log_network_connectivity_time_series_calls() {
334 let harness = setup_test();
335 let mut processor = NetworkPropertiesProcessor::new(
336 &harness.inspect_node,
337 &harness.parent_path,
338 &harness.mock_time_matrix_client,
339 );
340
341 let eth_metadata = NetworkEventMetadata {
342 id: 0,
343 name: Some("eth0".to_string()),
344 transport: fnp_socketproxy::NetworkType::Ethernet,
345 is_fuchsia_provisioned: true,
346 connectivity_state: Some(fnp_socketproxy::ConnectivityState::FullConnectivity),
347 };
348
349 let wlan_metadata = NetworkEventMetadata {
350 id: 1,
351 name: Some("wlan0".to_string()),
352 transport: fnp_socketproxy::NetworkType::Wifi,
353 is_fuchsia_provisioned: false,
354 connectivity_state: Some(fnp_socketproxy::ConnectivityState::LocalConnectivity),
355 };
356
357 processor.log_network_changed(eth_metadata, &harness.mock_time_matrix_client);
358 processor.log_network_changed(wlan_metadata, &harness.mock_time_matrix_client);
359
360 let mut time_matrix_calls = harness.mock_time_matrix_client.drain_calls();
361
362 assert_eq!(
364 &time_matrix_calls.drain::<u64>("network_0/connectivity")[..],
365 &[TimeMatrixCall::Fold(Timed::now(1 << 3))]
366 );
367
368 assert_eq!(
370 &time_matrix_calls.drain::<u64>("network_1/connectivity")[..],
371 &[TimeMatrixCall::Fold(Timed::now(1 << 1))]
372 );
373 }
374
375 #[fuchsia::test]
376 fn log_network_connectivity_inspect_tree() {
377 let mut harness = setup_test();
378 let time_matrix_client =
379 TimeMatrixClient::new(harness.inspect_node.create_child("time_series"));
380 let mut processor = NetworkPropertiesProcessor::new(
381 &harness.inspect_node,
382 &harness.parent_path,
383 &time_matrix_client,
384 );
385
386 let eth_metadata = NetworkEventMetadata {
387 id: 0,
388 name: Some("eth0".to_string()),
389 transport: fnp_socketproxy::NetworkType::Ethernet,
390 is_fuchsia_provisioned: true,
391 connectivity_state: Some(fnp_socketproxy::ConnectivityState::FullConnectivity),
392 };
393
394 processor.log_network_changed(eth_metadata, &time_matrix_client);
395
396 let hierarchy = harness.get_inspect_data_tree();
397
398 assert_data_tree!(
399 @executor harness.exec,
400 hierarchy,
401 root: contains {
402 test_stats: contains {
403 metadata: contains {
404 connectivity_states: contains {
405 index: contains {
406 "0": "NoConnectivity",
407 "1": "LocalConnectivity",
408 "2": "PartialConnectivity",
409 "3": "FullConnectivity",
410 }
411 }
412 },
413 time_series: contains {
414 network_0: contains {
415 connectivity: contains {
416 "type": "bitset",
417 "data": AnyBytesProperty,
418 metadata: {
419 index_node_path: "root/test_stats/metadata/connectivity_states",
420 }
421 }
422 }
423 }
424 }
425 }
426 );
427 }
428
429 #[fuchsia::test]
430 fn log_default_network_inspect_tree() {
431 let mut harness = setup_test();
432 let time_matrix_client =
433 TimeMatrixClient::new(harness.inspect_node.create_child("time_series"));
434 let mut processor = NetworkPropertiesProcessor::new(
435 &harness.inspect_node,
436 &harness.parent_path,
437 &time_matrix_client,
438 );
439 log_network_events(&mut processor);
440
441 let hierarchy = harness.get_inspect_data_tree();
442
443 assert_data_tree!(
444 @executor harness.exec,
445 hierarchy,
446 root: contains {
447 test_stats: contains {
448 metadata: contains {
449 network_registry: contains {
450 "0": contains {
451 data: {
452 id: 0u64,
453 name: "eth0",
454 transport: "Ethernet",
455 is_fuchsia_provisioned: true,
456 }
457 },
458 "1": contains {
459 data: {
460 id: 1u64,
461 name: "wlan0",
462 transport: "Wifi",
463 is_fuchsia_provisioned: false,
464 }
465 }
466 },
467 network_types: contains {
468 "0": contains {
469 data: "Ethernet",
470 },
471 "1": contains {
472 data: "Wifi",
473 }
474 }
475 },
476 time_series: contains {
477 default_network_detailed: {
478 "type": "bitset",
479 "data": AnyBytesProperty,
480 metadata: {
481 index_node_path: "root/test_stats/metadata/network_registry",
482 }
483 },
484 default_network_type: {
485 "type": "bitset",
486 "data": AnyBytesProperty,
487 metadata: {
488 index_node_path: "root/test_stats/metadata/network_types",
489 }
490 },
491 }
492 }
493 }
494 );
495 }
496
497 #[fuchsia::test]
498 fn log_network_connectivity_eviction() {
499 let mut harness = setup_test();
500 let time_matrix_client =
501 TimeMatrixClient::new(harness.inspect_node.create_child("time_series"));
502 let mut processor = NetworkPropertiesProcessor::new(
503 &harness.inspect_node,
504 &harness.parent_path,
505 &time_matrix_client,
506 );
507
508 for i in 0..16 {
510 let metadata = NetworkEventMetadata {
511 id: i as u64,
512 name: Some(format!("eth{}", i)),
513 transport: fnp_socketproxy::NetworkType::Ethernet,
514 is_fuchsia_provisioned: true,
515 connectivity_state: Some(fnp_socketproxy::ConnectivityState::FullConnectivity),
516 };
517 processor.log_network_changed(metadata, &time_matrix_client);
518 }
519
520 let hierarchy_before = harness.get_inspect_data_tree();
522 assert_data_tree!(
523 @executor harness.exec,
524 hierarchy_before,
525 root: contains {
526 test_stats: contains {
527 time_series: contains {
528 network_0: contains {}
529 }
530 }
531 }
532 );
533
534 let metadata = NetworkEventMetadata {
536 id: 16,
537 name: Some("eth16".to_string()),
538 transport: fnp_socketproxy::NetworkType::Ethernet,
539 is_fuchsia_provisioned: true,
540 connectivity_state: Some(fnp_socketproxy::ConnectivityState::LocalConnectivity),
541 };
542 processor.log_network_changed(metadata, &time_matrix_client);
543
544 let hierarchy = harness.get_inspect_data_tree();
545
546 assert_data_tree!(
548 @executor harness.exec,
549 hierarchy,
550 root: contains {
551 test_stats: contains {
552 time_series: contains {
553 network_16: contains {}
554 }
555 }
556 }
557 );
558
559 let test_stats = hierarchy.children.iter().find(|c| c.name == "test_stats").unwrap();
562 let time_series = test_stats.children.iter().find(|c| c.name == "time_series").unwrap();
563 assert!(
564 !time_series.children.iter().any(|c| c.name == "network_0"),
565 "network_0 was not evicted from Inspect!"
566 );
567 }
568}