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