settings/agent/inspect/
external_apis.rs

1// Copyright 2022 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//! The external_apis mod defines the [ExternalApisInspectAgent], which is responsible for recording
6//! external API requests and responses to Inspect. Since API usages might happen before agent
7//! lifecycle states are communicated (due to agent priority ordering), the
8//! [ExternalApisInspectAgent] begins listening to requests immediately after creation.
9//!
10//! Example Inspect structure:
11//!
12//! ```text
13//! {
14//!   "fuchsia.external.FakeAPI": {
15//!     "pending_calls": {
16//!       "00000000000000000005": {
17//!         request: "set_manual_brightness(0.7)",
18//!         response: "None",
19//!         request_timestamp: "19.002716",
20//!         response_timestamp: "None",
21//!       },
22//!     },
23//!     "calls": {
24//!       "00000000000000000002": {
25//!         request: "set_manual_brightness(0.6)",
26//!         response: "Ok(None)",
27//!         request_timestamp: "18.293864",
28//!         response_timestamp: "18.466811",
29//!       },
30//!       "00000000000000000004": {
31//!         request: "set_manual_brightness(0.8)",
32//!         response: "Ok(None)",
33//!         request_timestamp: "18.788366",
34//!         response_timestamp: "18.915355",
35//!       },
36//!     },
37//!   },
38//!   ...
39//! }
40//! ```
41
42use 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
54/// The key for the queue for completed calls per protocol.
55const COMPLETED_CALLS_KEY: &str = "completed_calls";
56
57/// The key for the queue for pending calls per protocol.
58const PENDING_CALLS_KEY: &str = "pending_calls";
59
60/// The maximum number of recently completed calls that will be kept in
61/// inspect per protocol.
62const MAX_COMPLETED_CALLS: usize = 10;
63
64/// The maximum number of still pending calls that will be kept in
65/// inspect per protocol.
66const MAX_PENDING_CALLS: usize = 10;
67
68// TODO(https://fxbug.dev/42060063): Explore reducing size of keys in inspect.
69#[derive(Debug, Default, Inspect)]
70struct ExternalApiCallInfo {
71    /// Node of this info.
72    inspect_node: Node,
73
74    /// The request sent via the external API.
75    request: IValue<String>,
76
77    /// The response received by the external API.
78    response: IValue<String>,
79
80    /// The timestamp at which the request was sent.
81    request_timestamp: IValue<String>,
82
83    /// The timestamp at which the response was received.
84    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    /// The number of total calls that have been made on this protocol.
107    count: IValue<u64>,
108    /// The most recent pending and completed calls per-protocol.
109    calls: ManagedInspectMap<ManagedInspectQueue<ExternalApiCallInfo>>,
110    /// The external api event counts.
111    event_counts: ManagedInspectMap<IValue<u64>>,
112}
113
114/// The [SettingTypeUsageInspectAgent] is responsible for listening to requests to external
115/// APIs and recording their requests and responses to Inspect.
116pub(crate) struct ExternalApiInspectAgent {
117    /// Map from the API call type to its most recent calls.
118    ///
119    /// Example structure:
120    /// ```text
121    /// {
122    ///   "fuchsia.ui.brightness.Control": {
123    ///     "count": 6,
124    ///     "calls": {
125    ///       "pending_calls": {
126    ///         "00000000000000000006": {
127    ///           request: "set_manual_brightness(0.7)",
128    ///           response: "None",
129    ///           request_timestamp: "19.002716",
130    ///           response_timestamp: "None",
131    ///         },
132    ///       }],
133    ///       "completed_calls": [{
134    ///         "00000000000000000003": {
135    ///           request: "set_manual_brightness(0.6)",
136    ///           response: "Ok(None)",
137    ///           request_timestamp: "18.293864",
138    ///           response_timestamp: "18.466811",
139    ///         },
140    ///         "00000000000000000005": {
141    ///           request: "set_manual_brightness(0.8)",
142    ///           response: "Ok(None)",
143    ///           request_timestamp: "18.788366",
144    ///           response_timestamp: "18.915355",
145    ///         },
146    ///       },
147    ///     },
148    ///     "event_counts": {
149    ///       "Connect": 1,
150    ///       "ApiCall": 3,
151    ///       "ApiResponse": 2,
152    ///     }
153    ///   },
154    ///   ...
155    /// }
156    /// ```
157    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    /// Creates the `ExternalApiInspectAgent` with the Inspect `node`.
174    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", &timestamp);
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", &timestamp, "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    /// Retrieves the total call count for the given `protocol`. Implicitly
271    /// calls `ensure_protocol_exists`.
272    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    /// Ensures that an entry exists for the given `protocol`, adding a new one if
278    /// it does not yet exist.
279    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    /// Ensures that an entry exists for the given `protocol`, and `queue_key` adding a
286    /// new queue of max size `queue_size` if one does not yet exist. Implicitly calls
287    /// `ensure_protocol_exists`.
288    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    /// Inserts the given `info` into the entry at `protocol` and `queue_key`, incrementing
298    /// the total call count to the protocol's wrapper entry.
299    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    /// Removes the call with the same request timestamp from the `protocol`'s pending
334    /// call queue, indicating that the call has completed. Should be called along with
335    /// `add_info` to add the completed call.
336    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}