1use fuchsia_async as fasync;
43use fuchsia_inspect::{Node, component};
44use fuchsia_inspect_derive::{IValue, Inspect, WithInspect};
45use futures::StreamExt;
46use futures::channel::mpsc::UnboundedReceiver;
47#[cfg(test)]
48use futures::channel::mpsc::UnboundedSender;
49use settings_common::service_context::ExternalServiceEvent;
50use settings_common::trace;
51use settings_inspect_utils::managed_inspect_map::ManagedInspectMap;
52use settings_inspect_utils::managed_inspect_queue::ManagedInspectQueue;
53
54const COMPLETED_CALLS_KEY: &str = "completed_calls";
56
57const PENDING_CALLS_KEY: &str = "pending_calls";
59
60const MAX_COMPLETED_CALLS: usize = 10;
63
64const MAX_PENDING_CALLS: usize = 10;
67
68#[derive(Debug, Default, Inspect)]
70struct ExternalApiCallInfo {
71 inspect_node: Node,
73
74 request: IValue<String>,
76
77 response: IValue<String>,
79
80 request_timestamp: IValue<String>,
82
83 response_timestamp: IValue<String>,
85}
86
87impl ExternalApiCallInfo {
88 fn new(
89 request: &str,
90 response: &str,
91 request_timestamp: &str,
92 response_timestamp: &str,
93 ) -> Self {
94 let mut info = Self::default();
95 info.request.iset(request.to_string());
96 info.response.iset(response.to_string());
97 info.request_timestamp.iset(request_timestamp.to_string());
98 info.response_timestamp.iset(response_timestamp.to_string());
99 info
100 }
101}
102
103#[derive(Default, Inspect)]
104struct ExternalApiCallsWrapper {
105 inspect_node: Node,
106 count: IValue<u64>,
108 calls: ManagedInspectMap<ManagedInspectQueue<ExternalApiCallInfo>>,
110 event_counts: ManagedInspectMap<IValue<u64>>,
112}
113
114pub(crate) struct ExternalApiInspectAgent {
117 api_calls: ManagedInspectMap<ExternalApiCallsWrapper>,
158 event_rx: Option<UnboundedReceiver<ExternalServiceEvent>>,
159 #[cfg(test)]
160 done_tx: Option<UnboundedSender<()>>,
161}
162
163impl ExternalApiInspectAgent {
164 pub fn new(event_rx: UnboundedReceiver<ExternalServiceEvent>) -> Self {
165 Self::create_with_node(
166 event_rx,
167 component::inspector().root().create_child("external_apis"),
168 #[cfg(test)]
169 None,
170 )
171 }
172
173 fn create_with_node(
175 event_rx: UnboundedReceiver<ExternalServiceEvent>,
176 node: Node,
177 #[cfg(test)] done_tx: Option<UnboundedSender<()>>,
178 ) -> Self {
179 ExternalApiInspectAgent {
180 api_calls: ManagedInspectMap::<ExternalApiCallsWrapper>::with_node(node),
181 event_rx: Some(event_rx),
182 #[cfg(test)]
183 done_tx,
184 }
185 }
186
187 pub fn initialize(mut self) {
188 fasync::Task::local({
189 async move {
190 let id = fuchsia_trace::Id::new();
191 trace!(id, c"external_api_inspect_agent");
192 let mut event_rx = self.event_rx.take().unwrap();
193 while let Some(event) = event_rx.next().await {
194 self.process_direct_event(event);
195 #[cfg(test)]
196 if let Some(done_tx) = &self.done_tx {
197 let _ = done_tx.unbounded_send(());
198 }
199 }
200 }
201 })
202 .detach();
203 }
204
205 fn process_direct_event(&mut self, event: ExternalServiceEvent) {
206 match event {
207 ExternalServiceEvent::Created(protocol, timestamp) => {
208 let count = self.get_count(protocol) + 1;
209 let info = ExternalApiCallInfo::new("connect", "none", "none", ×tamp);
210 self.add_info(protocol, COMPLETED_CALLS_KEY, "Created", info, count);
211 }
212 ExternalServiceEvent::ApiCall(protocol, request, timestamp) => {
213 let count = self.get_count(protocol) + 1;
214 let info = ExternalApiCallInfo::new(&request, "none", ×tamp, "none");
215 self.add_info(protocol, PENDING_CALLS_KEY, "ApiCall", info, count);
216 }
217 ExternalServiceEvent::ApiResponse(
218 protocol,
219 response,
220 request,
221 request_timestamp,
222 response_timestamp,
223 ) => {
224 let count = self.get_count(protocol) + 1;
225 let info = ExternalApiCallInfo::new(
226 &request,
227 &response,
228 &request_timestamp,
229 &response_timestamp,
230 );
231 self.remove_pending(protocol, &info);
232 self.add_info(protocol, COMPLETED_CALLS_KEY, "ApiResponse", info, count);
233 }
234 ExternalServiceEvent::ApiError(
235 protocol,
236 error,
237 request,
238 request_timestamp,
239 error_timestamp,
240 ) => {
241 let count = self.get_count(protocol) + 1;
242 let info = ExternalApiCallInfo::new(
243 &request,
244 &error,
245 &request_timestamp,
246 &error_timestamp,
247 );
248 self.remove_pending(protocol, &info);
249 self.add_info(protocol, COMPLETED_CALLS_KEY, "ApiError", info, count);
250 }
251 ExternalServiceEvent::Closed(
252 protocol,
253 request,
254 request_timestamp,
255 response_timestamp,
256 ) => {
257 let count = self.get_count(protocol) + 1;
258 let info = ExternalApiCallInfo::new(
259 &request,
260 "closed",
261 &request_timestamp,
262 &response_timestamp,
263 );
264 self.remove_pending(protocol, &info);
265 self.add_info(protocol, COMPLETED_CALLS_KEY, "Closed", info, count);
266 }
267 }
268 }
269
270 fn get_count(&mut self, protocol: &str) -> u64 {
273 self.ensure_protocol_exists(protocol);
274 *self.api_calls.get(protocol).expect("Wrapper should exist").count
275 }
276
277 fn ensure_protocol_exists(&mut self, protocol: &str) {
280 let _ = self
281 .api_calls
282 .get_or_insert_with(protocol.to_string(), ExternalApiCallsWrapper::default);
283 }
284
285 fn ensure_queue_exists(&mut self, protocol: &str, queue_key: &'static str, queue_size: usize) {
289 self.ensure_protocol_exists(protocol);
290
291 let protocol_map = self.api_calls.get_mut(protocol).expect("Protocol entry should exist");
292 let _ = protocol_map.calls.get_or_insert_with(queue_key.to_string(), || {
293 ManagedInspectQueue::<ExternalApiCallInfo>::new(queue_size)
294 });
295 }
296
297 fn add_info(
300 &mut self,
301 protocol: &str,
302 queue_key: &'static str,
303 event_type: &str,
304 info: ExternalApiCallInfo,
305 count: u64,
306 ) {
307 self.ensure_queue_exists(
308 protocol,
309 queue_key,
310 if queue_key == COMPLETED_CALLS_KEY { MAX_COMPLETED_CALLS } else { MAX_PENDING_CALLS },
311 );
312 let wrapper = self.api_calls.get_mut(protocol).expect("Protocol entry should exist");
313 {
314 let mut wrapper_guard = wrapper.count.as_mut();
315 *wrapper_guard += 1;
316 }
317 let event_count =
318 wrapper.event_counts.get_or_insert_with(event_type.to_string(), || IValue::new(0));
319 {
320 let mut event_count_guard = event_count.as_mut();
321 *event_count_guard += 1;
322 }
323
324 let queue = wrapper.calls.get_mut(queue_key).expect("Queue should exist");
325 let key = format!("{count:020}");
326 queue.push(
327 &key,
328 info.with_inspect(queue.inspect_node(), &key)
329 .expect("Failed to create ExternalApiCallInfo node"),
330 );
331 }
332
333 fn remove_pending(&mut self, protocol: &str, info: &ExternalApiCallInfo) {
337 let wrapper = self.api_calls.get_mut(protocol).expect("Protocol entry should exist");
338 let pending_queue =
339 wrapper.calls.get_mut(PENDING_CALLS_KEY).expect("Pending queue should exist");
340 let req_timestamp = &*info.request_timestamp;
341 pending_queue.retain(|pending| &*pending.request_timestamp != req_timestamp);
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348 use diagnostics_assertions::assert_data_tree;
349 use fuchsia_inspect::Inspector;
350 use futures::channel::mpsc;
351
352 const MOCK_PROTOCOL_NAME: &str = "fuchsia.external.FakeAPI";
353
354 #[fuchsia::test(allow_stalls = false)]
355 async fn test_inspect_create_connection() {
356 let inspector = Inspector::default();
357 let inspect_node = inspector.root().create_child("external_apis");
358
359 let (tx, rx) = mpsc::unbounded();
360 let (done_tx, mut done_rx) = mpsc::unbounded();
361 let agent = ExternalApiInspectAgent::create_with_node(rx, inspect_node, Some(done_tx));
362 agent.initialize();
363
364 let connection_created_event =
365 ExternalServiceEvent::Created(MOCK_PROTOCOL_NAME, "0.000000".into());
366
367 let _ = tx.unbounded_send(connection_created_event);
368 let _ = done_rx.next().await;
369
370 assert_data_tree!(inspector, root: {
371 external_apis: {
372 "fuchsia.external.FakeAPI": {
373 "calls": {
374 "completed_calls": {
375 "00000000000000000001": {
376 request: "connect",
377 response: "none",
378 request_timestamp: "none",
379 response_timestamp: "0.000000",
380 },
381 },
382 },
383 "count": 1u64,
384 "event_counts": {
385 "Created": 1u64,
386 },
387 },
388 },
389 });
390 }
391
392 #[fuchsia::test(allow_stalls = false)]
393 async fn test_inspect_pending() {
394 let inspector = Inspector::default();
395 let inspect_node = inspector.root().create_child("external_apis");
396
397 let (tx, rx) = mpsc::unbounded();
398 let (done_tx, mut done_rx) = mpsc::unbounded();
399 let agent = ExternalApiInspectAgent::create_with_node(rx, inspect_node, Some(done_tx));
400 agent.initialize();
401
402 let api_call_event = ExternalServiceEvent::ApiCall(
403 MOCK_PROTOCOL_NAME,
404 "set_manual_brightness(0.6)".into(),
405 "0.000000".into(),
406 );
407
408 let _ = tx.unbounded_send(api_call_event.clone());
409 let _ = done_rx.next().await;
410
411 assert_data_tree!(inspector, root: {
412 external_apis: {
413 "fuchsia.external.FakeAPI": {
414 "calls": {
415 "pending_calls": {
416 "00000000000000000001": {
417 request: "set_manual_brightness(0.6)",
418 response: "none",
419 request_timestamp: "0.000000",
420 response_timestamp: "none",
421 },
422 },
423 },
424 "count": 1u64,
425 "event_counts": {
426 "ApiCall": 1u64,
427 },
428 },
429 },
430 });
431 }
432
433 #[fuchsia::test(allow_stalls = false)]
434 async fn test_inspect_success_response() {
435 let inspector = Inspector::default();
436 let inspect_node = inspector.root().create_child("external_apis");
437
438 let (tx, rx) = mpsc::unbounded();
439 let (done_tx, mut done_rx) = mpsc::unbounded();
440 let agent = ExternalApiInspectAgent::create_with_node(rx, inspect_node, Some(done_tx));
441 agent.initialize();
442
443 let api_call_event = ExternalServiceEvent::ApiCall(
444 MOCK_PROTOCOL_NAME,
445 "set_manual_brightness(0.6)".into(),
446 "0.000000".into(),
447 );
448 let api_response_event = ExternalServiceEvent::ApiResponse(
449 MOCK_PROTOCOL_NAME,
450 "Ok(None)".into(),
451 "set_manual_brightness(0.6)".into(),
452 "0.000000".into(),
453 "0.129987".into(),
454 );
455
456 let _ = tx.unbounded_send(api_call_event.clone());
457 let _ = done_rx.next().await;
458 assert_data_tree!(inspector, root: {
459 external_apis: {
460 "fuchsia.external.FakeAPI": {
461 "calls": {
462 "pending_calls": {
463 "00000000000000000001": {
464 request: "set_manual_brightness(0.6)",
465 response: "none",
466 request_timestamp: "0.000000",
467 response_timestamp: "none",
468 },
469 },
470 },
471 "count": 1u64,
472 "event_counts": {
473 "ApiCall": 1u64,
474 },
475 },
476 },
477 });
478
479 let _ = tx.unbounded_send(api_response_event.clone());
480 let _ = done_rx.next().await;
481
482 assert_data_tree!(inspector, root: {
483 external_apis: {
484 "fuchsia.external.FakeAPI": {
485 "calls": {
486 "pending_calls": {},
487 "completed_calls": {
488 "00000000000000000002": {
489 request: "set_manual_brightness(0.6)",
490 response: "Ok(None)",
491 request_timestamp: "0.000000",
492 response_timestamp: "0.129987",
493 },
494 },
495 },
496 "count": 2u64,
497 "event_counts": {
498 "ApiCall": 1u64,
499 "ApiResponse": 1u64,
500 },
501 },
502 },
503 });
504 }
505
506 #[fuchsia::test(allow_stalls = false)]
507 async fn test_inspect_error() {
508 let inspector = Inspector::default();
509 let inspect_node = inspector.root().create_child("external_apis");
510
511 let (tx, rx) = mpsc::unbounded();
512 let (done_tx, mut done_rx) = mpsc::unbounded();
513 let agent = ExternalApiInspectAgent::create_with_node(rx, inspect_node, Some(done_tx));
514 agent.initialize();
515
516 let api_call_event = ExternalServiceEvent::ApiCall(
517 MOCK_PROTOCOL_NAME,
518 "set_manual_brightness(0.6)".into(),
519 "0.000000".into(),
520 );
521 let error_event = ExternalServiceEvent::ApiError(
522 MOCK_PROTOCOL_NAME,
523 "Err(INTERNAL_ERROR)".into(),
524 "set_manual_brightness(0.6)".into(),
525 "0.000000".into(),
526 "0.129987".into(),
527 );
528
529 let _ = tx.unbounded_send(api_call_event.clone());
530 let _ = done_rx.next().await;
531
532 assert_data_tree!(inspector, root: {
533 external_apis: {
534 "fuchsia.external.FakeAPI": {
535 "calls": {
536 "pending_calls": {
537 "00000000000000000001": {
538 request: "set_manual_brightness(0.6)",
539 response: "none",
540 request_timestamp: "0.000000",
541 response_timestamp: "none",
542 },
543 },
544 },
545 "count": 1u64,
546 "event_counts": {
547 "ApiCall": 1u64,
548 },
549 },
550 },
551 });
552
553 let _ = tx.unbounded_send(error_event.clone());
554 let _ = done_rx.next().await;
555
556 assert_data_tree!(inspector, root: {
557 external_apis: {
558 "fuchsia.external.FakeAPI": {
559 "calls": {
560 "pending_calls": {},
561 "completed_calls": {
562 "00000000000000000002": {
563 request: "set_manual_brightness(0.6)",
564 response: "Err(INTERNAL_ERROR)",
565 request_timestamp: "0.000000",
566 response_timestamp: "0.129987",
567 },
568 },
569 },
570 "count": 2u64,
571 "event_counts": {
572 "ApiCall": 1u64,
573 "ApiError": 1u64,
574 },
575 },
576 },
577 });
578 }
579
580 #[fuchsia::test(allow_stalls = false)]
581 async fn test_inspect_channel_closed() {
582 let inspector = Inspector::default();
583 let inspect_node = inspector.root().create_child("external_apis");
584
585 let (tx, rx) = mpsc::unbounded();
586 let (done_tx, mut done_rx) = mpsc::unbounded();
587 let agent = ExternalApiInspectAgent::create_with_node(rx, inspect_node, Some(done_tx));
588 agent.initialize();
589
590 let api_call_event = ExternalServiceEvent::ApiCall(
591 MOCK_PROTOCOL_NAME,
592 "set_manual_brightness(0.6)".into(),
593 "0.000000".into(),
594 );
595 let closed_event = ExternalServiceEvent::Closed(
596 MOCK_PROTOCOL_NAME,
597 "set_manual_brightness(0.6)".into(),
598 "0.000000".into(),
599 "0.129987".into(),
600 );
601
602 let _ = tx.unbounded_send(api_call_event.clone());
603 let _ = done_rx.next().await;
604
605 assert_data_tree!(inspector, root: {
606 external_apis: {
607 "fuchsia.external.FakeAPI": {
608 "calls": {
609 "pending_calls": {
610 "00000000000000000001": {
611 request: "set_manual_brightness(0.6)",
612 response: "none",
613 request_timestamp: "0.000000",
614 response_timestamp: "none",
615 },
616 },
617 },
618 "count": 1u64,
619 "event_counts": {
620 "ApiCall": 1u64,
621 },
622 },
623 },
624 });
625
626 let _ = tx.unbounded_send(closed_event.clone());
627 let _ = done_rx.next().await;
628
629 assert_data_tree!(inspector, root: {
630 external_apis: {
631 "fuchsia.external.FakeAPI": {
632 "calls": {
633 "pending_calls": {},
634 "completed_calls": {
635 "00000000000000000002": {
636 request: "set_manual_brightness(0.6)",
637 response: "closed",
638 request_timestamp: "0.000000",
639 response_timestamp: "0.129987",
640 },
641 },
642 },
643 "count": 2u64,
644 "event_counts": {
645 "ApiCall": 1u64,
646 "Closed": 1u64,
647 },
648 },
649 },
650 });
651 }
652
653 #[fuchsia::test(allow_stalls = false)]
654 async fn test_inspect_multiple_requests() {
655 let inspector = Inspector::default();
656 let inspect_node = inspector.root().create_child("external_apis");
657
658 let (tx, rx) = mpsc::unbounded();
659 let (done_tx, mut done_rx) = mpsc::unbounded();
660 let agent = ExternalApiInspectAgent::create_with_node(rx, inspect_node, Some(done_tx));
661 agent.initialize();
662
663 let api_call_event = ExternalServiceEvent::ApiCall(
664 MOCK_PROTOCOL_NAME,
665 "set_manual_brightness(0.6)".into(),
666 "0.000000".into(),
667 );
668 let api_response_event = ExternalServiceEvent::ApiResponse(
669 MOCK_PROTOCOL_NAME,
670 "Ok(None)".into(),
671 "set_manual_brightness(0.6)".into(),
672 "0.000000".into(),
673 "0.129987".into(),
674 );
675
676 let api_call_event_2 = ExternalServiceEvent::ApiCall(
677 MOCK_PROTOCOL_NAME,
678 "set_manual_brightness(0.7)".into(),
679 "0.139816".into(),
680 );
681 let api_response_event_2 = ExternalServiceEvent::ApiResponse(
682 MOCK_PROTOCOL_NAME,
683 "Ok(None)".into(),
684 "set_manual_brightness(0.7)".into(),
685 "0.139816".into(),
686 "0.141235".into(),
687 );
688
689 let _ = tx.unbounded_send(api_call_event.clone());
690 let _ = done_rx.next().await;
691 assert_data_tree!(inspector, root: {
692 external_apis: {
693 "fuchsia.external.FakeAPI": {
694 "calls": {
695 "pending_calls": {
696 "00000000000000000001": {
697 request: "set_manual_brightness(0.6)",
698 response: "none",
699 request_timestamp: "0.000000",
700 response_timestamp: "none",
701 },
702 },
703 },
704 "count": 1u64,
705 "event_counts": {
706 "ApiCall": 1u64,
707 },
708 },
709 },
710 });
711
712 let _ = tx.unbounded_send(api_response_event.clone());
713 let _ = done_rx.next().await;
714
715 assert_data_tree!(inspector, root: {
716 external_apis: {
717 "fuchsia.external.FakeAPI": {
718 "calls": {
719 "pending_calls": {},
720 "completed_calls": {
721 "00000000000000000002": {
722 request: "set_manual_brightness(0.6)",
723 response: "Ok(None)",
724 request_timestamp: "0.000000",
725 response_timestamp: "0.129987",
726 },
727 },
728 },
729 "count": 2u64,
730 "event_counts": {
731 "ApiCall": 1u64,
732 "ApiResponse": 1u64,
733 },
734 },
735 },
736 });
737
738 let _ = tx.unbounded_send(api_call_event_2.clone());
739 let _ = done_rx.next().await;
740 assert_data_tree!(inspector, root: {
741 external_apis: {
742 "fuchsia.external.FakeAPI": {
743 "calls": {
744 "pending_calls": {
745 "00000000000000000003": {
746 request: "set_manual_brightness(0.7)",
747 response: "none",
748 request_timestamp: "0.139816",
749 response_timestamp: "none",
750 },
751 },
752 "completed_calls": {
753 "00000000000000000002": {
754 request: "set_manual_brightness(0.6)",
755 response: "Ok(None)",
756 request_timestamp: "0.000000",
757 response_timestamp: "0.129987",
758 },
759 },
760 },
761 "count": 3u64,
762 "event_counts": {
763 "ApiCall": 2u64,
764 "ApiResponse": 1u64,
765 },
766 },
767 },
768 });
769
770 let _ = tx.unbounded_send(api_response_event_2.clone());
771 let _ = done_rx.next().await;
772
773 assert_data_tree!(inspector, root: {
774 external_apis: {
775 "fuchsia.external.FakeAPI": {
776 "calls": {
777 "pending_calls": {},
778 "completed_calls": {
779 "00000000000000000002": {
780 request: "set_manual_brightness(0.6)",
781 response: "Ok(None)",
782 request_timestamp: "0.000000",
783 response_timestamp: "0.129987",
784 },
785 "00000000000000000004": {
786 request: "set_manual_brightness(0.7)",
787 response: "Ok(None)",
788 request_timestamp: "0.139816",
789 response_timestamp: "0.141235",
790 },
791 },
792 },
793 "count": 4u64,
794 "event_counts": {
795 "ApiCall": 2u64,
796 "ApiResponse": 2u64,
797 },
798 },
799 },
800 });
801 }
802}