fuchsia_async_inspect/
lib.rs

1// Copyright 2025 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 fuchsia_async::ScopeHandle;
6use fuchsia_async::instrument::{AtomicFutureHandle, Hooks, TaskInstrument};
7use fuchsia_inspect::{self as inspect, Node, NumericProperty, Property};
8use fuchsia_sync::Mutex;
9use std::any::Any;
10use std::collections::HashSet;
11use std::sync::Arc;
12use std::sync::atomic::{AtomicUsize, Ordering};
13
14fn get_unique_name(names: &mut HashSet<String>, base_name: String) -> String {
15    if names.insert(base_name.clone()) {
16        return base_name;
17    }
18    let mut i = 1;
19    loop {
20        let new_name = format!("{}_{}", base_name, i);
21        if names.insert(new_name.clone()) {
22            return new_name;
23        }
24        i += 1;
25    }
26}
27
28/// An implementation of `TaskInstrument` that uses Inspect.
29pub struct InspectTaskInstrument {
30    root: Node,
31    next_task_id: AtomicUsize,
32    child_names: Arc<Mutex<HashSet<String>>>,
33}
34
35struct TaskNode {
36    _node: Node,
37    polls: inspect::UintProperty,
38    completed: inspect::BoolProperty,
39    max_poll_duration_micros: inspect::UintProperty,
40}
41
42struct InspectHooks {
43    task_node: TaskNode,
44    poll_start_time: zx::BootInstant,
45    max_poll_duration_micros: u64,
46    name: String,
47    parent_names: Arc<Mutex<HashSet<String>>>,
48}
49
50impl Drop for InspectHooks {
51    fn drop(&mut self) {
52        self.parent_names.lock().remove(&self.name);
53    }
54}
55
56impl Hooks for InspectHooks {
57    fn task_completed(&mut self) {
58        self.task_node.completed.set(true);
59    }
60
61    fn task_poll_end(&mut self) {
62        let duration = fuchsia_async::BootInstant::now().into_zx() - self.poll_start_time;
63        let duration_micros = duration.into_micros() as u64;
64        if duration_micros > self.max_poll_duration_micros {
65            self.max_poll_duration_micros = duration_micros;
66            self.task_node.max_poll_duration_micros.set(duration_micros);
67        }
68    }
69
70    fn task_poll_start(&mut self) {
71        self.poll_start_time = fuchsia_async::BootInstant::now().into_zx();
72        self.task_node.polls.add(1);
73    }
74}
75
76struct ScopeInspect {
77    node: Node,
78    child_names: Arc<Mutex<HashSet<String>>>,
79    name: String,
80    parent_names: Arc<Mutex<HashSet<String>>>,
81}
82
83impl Drop for ScopeInspect {
84    fn drop(&mut self) {
85        self.parent_names.lock().remove(&self.name);
86    }
87}
88
89/// A configuration object for `InspectTaskInstrument`.
90///
91/// This struct allows for future expansion of configuration options without breaking
92/// the existing API.
93pub struct InspectTaskConfiguration {
94    /// The root inspect node under which task and scope nodes will be created.
95    pub inspect_root: Node,
96}
97
98impl InspectTaskConfiguration {
99    pub fn new(inspect_root: Node) -> Self {
100        Self { inspect_root }
101    }
102}
103
104impl InspectTaskInstrument {
105    /// Create a new `InspectTaskInstrument`.
106    pub fn new(config: InspectTaskConfiguration) -> Arc<Self> {
107        Arc::new(Self {
108            root: config.inspect_root,
109            next_task_id: AtomicUsize::new(0),
110            child_names: Arc::new(Mutex::new(HashSet::new())),
111        })
112    }
113}
114
115impl TaskInstrument for InspectTaskInstrument {
116    fn task_created<'a>(&self, parent_scope: &ScopeHandle, task: &mut AtomicFutureHandle<'a>) {
117        let id = self.next_task_id.fetch_add(1, Ordering::Relaxed);
118        let base_name = format!("task_{}", id);
119        let (parent_node, parent_names) = parent_scope
120            .instrument_data()
121            .and_then(|scope| scope.downcast_ref::<ScopeInspect>())
122            .map(|scope| (&scope.node, Arc::clone(&scope.child_names)))
123            .unwrap_or_else(|| (&self.root, Arc::clone(&self.child_names)));
124        let name = {
125            let mut names = parent_names.lock();
126            get_unique_name(&mut names, base_name)
127        };
128        let node = parent_node.create_child(&name);
129        let polls = node.create_uint("polls", 0);
130        let completed = node.create_bool("completed", false);
131        let max_poll_duration_micros = node.create_uint("max_poll_duration_micros", 0);
132        let task_node = TaskNode { _node: node, polls, completed, max_poll_duration_micros };
133        task.add_hooks(InspectHooks {
134            task_node,
135            poll_start_time: zx::BootInstant::get(),
136            max_poll_duration_micros: 0,
137            name,
138            parent_names,
139        })
140    }
141
142    fn scope_created(
143        &self,
144        scope_name: &str,
145        parent_scope: Option<&ScopeHandle>,
146    ) -> Box<dyn Any + Send + Sync> {
147        let (parent_node, parent_names) = parent_scope
148            .map(|scope| scope.instrument_data())
149            .flatten()
150            .and_then(|scope| scope.downcast_ref::<ScopeInspect>())
151            .map(|scope| (&scope.node, Arc::clone(&scope.child_names)))
152            .unwrap_or_else(|| (&self.root, Arc::clone(&self.child_names)));
153        let name = {
154            let mut names = parent_names.lock();
155            get_unique_name(&mut names, scope_name.to_string())
156        };
157        let node = parent_node.create_child(&name);
158        Box::new(ScopeInspect {
159            node,
160            child_names: Arc::new(Mutex::new(HashSet::new())),
161            name,
162            parent_names,
163        })
164    }
165}
166
167pub fn default_root() -> Node {
168    fuchsia_inspect::component::inspector()
169        .root()
170        .create_child("fuchsia.inspect.AsyncInstrumentation")
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use diagnostics_assertions::assert_data_tree;
177    use fuchsia_async::{self as fasync, EHandle, MonotonicInstant};
178    use std::pin::pin;
179
180    #[fuchsia::test]
181    fn test_max_poll_duration() {
182        let inspector = inspect::Inspector::default();
183        let instrument = InspectTaskInstrument::new(InspectTaskConfiguration::new(
184            inspector.root().clone_weak(),
185        ));
186        let mut exec =
187            fasync::TestExecutorBuilder::new().fake_time(true).instrument(instrument).build();
188
189        exec.set_fake_time(MonotonicInstant::from_nanos(0));
190        let mut fut = pin!(async {
191            let child_scope = fuchsia_async::Scope::new_with_name("task_1");
192            let child_task = fuchsia_async::Task::local(async move {
193                futures::future::pending::<()>().await;
194            });
195            assert_data_tree!(inspector, root: {
196                root: {
197                    // Our task
198                    task_0:{
199                        polls: 1u64,
200                        completed: false,
201                        max_poll_duration_micros: 0u64,
202                    },
203                    // Task that was spawned under us
204                    task_1:{},
205                    task_1_1:{
206                        polls: 0u64,
207                        completed: false,
208                        max_poll_duration_micros: 0u64,
209                    },
210                }
211            });
212            // Drop the task, which should remove it from Inspect
213            drop(child_task);
214            drop(child_scope);
215            // Dropping a task doesn't immediately drop it,
216            // but in a TestExecutor it should be dropped
217            // after polling again.
218            fuchsia_async::yield_now().await;
219            assert_data_tree!(inspector, root: {
220                root: {
221                    // Our task
222                    task_0:{
223                        polls: 2u64,
224                        completed: false,
225                        max_poll_duration_micros: 0u64,
226                    },
227                }
228            });
229            // Wait 200 microseconds
230            EHandle::local().set_fake_time(MonotonicInstant::from_nanos(1000 * 200));
231            fuchsia_async::yield_now().await;
232            assert_data_tree!(inspector, root: {
233                root: {
234                    // Our task
235                    task_0:{
236                        polls: 3u64,
237                        completed: false,
238                        max_poll_duration_micros: 200u64,
239                    },
240                }
241            });
242            let scope = fuchsia_async::Scope::new_with_name("test scope");
243            let _scope_2 = fuchsia_async::Scope::new_with_name("test scope");
244            let _scope_3 = fuchsia_async::Scope::new_with_name("test scope");
245
246            let child_scope = scope.new_child_with_name("test child scope");
247            let _child_task = child_scope.spawn(async move {});
248            // Assert that we have a completed future with the correct hierarchy
249            assert_data_tree!(inspector, root: {
250                root: {
251                    // Our task
252                    task_0:{
253                        polls: 3u64,
254                        completed: false,
255                        max_poll_duration_micros: 200u64,
256                    },
257                    "test scope":{
258                        "test child scope":{
259                            task_2:{
260                                polls: 0u64,
261                                completed: false,
262                                max_poll_duration_micros: 0u64,
263                            }
264                        }
265                    },
266                    "test scope_1":{},
267                    "test scope_2":{},
268                }
269            });
270        });
271        let _ = exec.run_until_stalled(&mut fut);
272    }
273}