1use 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 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 let mut context_counts = context_counts.by_flags.clone();
122
123 if let Some(no_context_count) = context_counts.remove(&None) {
124 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 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)] 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 {
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 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}