inspect_stubs/
stubs.rs

1// Copyright 2023 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_inspect::Inspector;
6use fuchsia_sync::Mutex;
7use futures::future::BoxFuture;
8use std::collections::HashMap;
9use std::num::NonZeroU64;
10use std::panic::Location;
11use std::sync::LazyLock;
12
13static STUB_COUNTS: LazyLock<Mutex<HashMap<Invocation, Counts>>> =
14    LazyLock::new(|| Mutex::new(HashMap::new()));
15
16#[macro_export]
17macro_rules! track_stub {
18    (TODO($bug_url:literal), $message:expr, $flags:expr $(,)?) => {{
19        $crate::__track_stub_inner(
20            $crate::bug_ref!($bug_url),
21            $message,
22            Some($flags.into()),
23            std::panic::Location::caller(),
24        );
25    }};
26    (TODO($bug_url:literal), $message:expr $(,)?) => {{
27        $crate::__track_stub_inner(
28            $crate::bug_ref!($bug_url),
29            $message,
30            None,
31            std::panic::Location::caller(),
32        );
33    }};
34}
35
36#[macro_export]
37macro_rules! track_stub_log {
38    ($level:expr, TODO($bug_url:literal), $message:expr, $flags:expr $(,)?) => {{
39        $crate::__track_stub_inner_with_level(
40            $level,
41            $crate::bug_ref!($bug_url),
42            $message,
43            Some($flags.into()),
44            std::panic::Location::caller(),
45        );
46    }};
47    ($level:expr, TODO($bug_url:literal), $message:expr $(,)?) => {{
48        $crate::__track_stub_inner_with_level(
49            $level,
50            $crate::bug_ref!($bug_url),
51            $message,
52            None,
53            std::panic::Location::caller(),
54        );
55    }};
56}
57
58#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
59struct Invocation {
60    location: &'static Location<'static>,
61    message: &'static str,
62    bug: BugRef,
63}
64
65#[derive(Default)]
66struct Counts {
67    by_flags: HashMap<Option<u64>, u64>,
68}
69
70#[doc(hidden)]
71#[inline]
72pub fn __track_stub_inner(
73    bug: BugRef,
74    message: &'static str,
75    flags: Option<u64>,
76    location: &'static Location<'static>,
77) -> u64 {
78    __track_stub_inner_with_level(log::Level::Debug, bug, message, flags, location)
79}
80
81#[doc(hidden)]
82#[inline]
83pub fn __track_stub_inner_with_level(
84    level: log::Level,
85    bug: BugRef,
86    message: &'static str,
87    flags: Option<u64>,
88    location: &'static Location<'static>,
89) -> u64 {
90    let mut counts = STUB_COUNTS.lock();
91    let message_counts = counts.entry(Invocation { location, message, bug }).or_default();
92    let context_count = message_counts.by_flags.entry(flags).or_default();
93
94    // Log the first time we see a particular file/message/context tuple but don't risk spamming.
95    if *context_count == 0 {
96        match flags {
97            Some(flags) => {
98                log::log!(level, tag = "track_stub", location:%; "{bug} {message}: 0x{flags:x}");
99            }
100            None => {
101                log::log!(level, tag = "track_stub", location:%; "{bug} {message}");
102            }
103        }
104    }
105
106    *context_count += 1;
107    *context_count
108}
109
110pub fn track_stub_lazy_node_callback() -> BoxFuture<'static, Result<Inspector, anyhow::Error>> {
111    Box::pin(async {
112        let inspector = Inspector::default();
113        for (Invocation { location, message, bug }, context_counts) in STUB_COUNTS.lock().iter() {
114            inspector.root().atomic_update(|root| {
115                root.record_child(*message, |message_node| {
116                    message_node.record_string("file", location.file());
117                    message_node.record_uint("line", location.line().into());
118                    message_node.record_string("bug", bug.to_string());
119
120                    // Make a copy of the map so we can mutate it while recording values.
121                    let mut context_counts = context_counts.by_flags.clone();
122
123                    if let Some(no_context_count) = context_counts.remove(&None) {
124                        // If the track_stub callsite doesn't provide any context,
125                        // record the count as a property on the node without an intermediate.
126                        message_node.record_uint("count", no_context_count);
127                    }
128
129                    if !context_counts.is_empty() {
130                        message_node.record_child("counts", |counts_node| {
131                            for (context, count) in context_counts {
132                                if let Some(c) = context {
133                                    counts_node.record_uint(format!("0x{c:x}"), count);
134                                }
135                            }
136                        });
137                    }
138                });
139            });
140        }
141        Ok(inspector)
142    })
143}
144
145#[macro_export]
146macro_rules! bug_ref {
147    ($bug_url:literal) => {{
148        // Assign the value to a const to ensure we get compile-time validation of the URL.
149        const __REF: $crate::BugRef = match $crate::BugRef::from_str($bug_url) {
150            Some(b) => b,
151            None => panic!("bug references must have the form `https://fxbug.dev/123456789`"),
152        };
153        __REF
154    }};
155}
156
157#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
158pub struct BugRef {
159    number: u64,
160}
161
162impl BugRef {
163    #[doc(hidden)] // use bug_ref!() instead
164    pub const fn from_str(url: &'static str) -> Option<Self> {
165        let expected_prefix = b"https://fxbug.dev/";
166        let url = str::as_bytes(url);
167
168        if url.len() < expected_prefix.len() {
169            return None;
170        }
171        let (scheme_and_domain, number_str) = url.split_at(expected_prefix.len());
172        if number_str.is_empty() {
173            return None;
174        }
175
176        // The standard library doesn't seem to have a const string or slice equality function.
177        {
178            let mut i = 0;
179            while i < scheme_and_domain.len() {
180                if scheme_and_domain[i] != expected_prefix[i] {
181                    return None;
182                }
183                i += 1;
184            }
185        }
186
187        // The standard library doesn't seem to have a const base 10 string parser.
188        let mut number = 0;
189        {
190            let mut i = 0;
191            while i < number_str.len() {
192                number *= 10;
193                number += match number_str[i] {
194                    b'0' => 0,
195                    b'1' => 1,
196                    b'2' => 2,
197                    b'3' => 3,
198                    b'4' => 4,
199                    b'5' => 5,
200                    b'6' => 6,
201                    b'7' => 7,
202                    b'8' => 8,
203                    b'9' => 9,
204                    _ => return None,
205                };
206                i += 1;
207            }
208        }
209
210        assert!(number != 0, "Zero does not a valid bug number make.");
211
212        Some(Self { number })
213    }
214}
215
216impl From<NonZeroU64> for BugRef {
217    fn from(value: NonZeroU64) -> Self {
218        Self { number: value.get() }
219    }
220}
221
222impl Into<NonZeroU64> for BugRef {
223    fn into(self) -> NonZeroU64 {
224        NonZeroU64::new(self.number).unwrap()
225    }
226}
227
228impl std::fmt::Display for BugRef {
229    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230        write!(f, "https://fxbug.dev/{}", self.number)
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn valid_url_parses() {
240        assert_eq!(BugRef::from_str("https://fxbug.dev/1234567890").unwrap().number, 1234567890);
241    }
242
243    #[test]
244    fn missing_prefix_fails() {
245        assert_eq!(BugRef::from_str("1234567890"), None);
246    }
247
248    #[test]
249    fn missing_number_fails() {
250        assert_eq!(BugRef::from_str("https://fxbug.dev/"), None);
251    }
252
253    #[test]
254    fn short_prefixes_fail() {
255        assert_eq!(BugRef::from_str("b/1234567890"), None);
256        assert_eq!(BugRef::from_str("fxb/1234567890"), None);
257        assert_eq!(BugRef::from_str("fxbug.dev/1234567890"), None);
258    }
259}