1use flyweights::FlyByteStr;
11use fuchsia_inspect::{ArrayProperty, Inspector};
12use fuchsia_sync::Mutex;
13use futures::future::BoxFuture;
14use std::collections::{HashMap, HashSet};
15use std::hash::{Hash, Hasher};
16use std::num::NonZeroU64;
17use std::panic::Location;
18use std::sync::LazyLock;
19
20static STUB_COUNTS: LazyLock<Mutex<HashMap<Invocation, Counts>>> =
21 LazyLock::new(|| Mutex::new(HashMap::new()));
22
23static CONTEXT_NAME_CALLBACK: Mutex<Option<Box<dyn Fn() -> FlyByteStr + Send + Sync>>> =
24 Mutex::new(None);
25
26#[macro_export]
36macro_rules! track_stub {
37 (TODO($bug_url:literal), $message:expr, $flags:expr $(,)?) => {{
38 $crate::__track_stub_inner(
39 $crate::bug_ref!($bug_url),
40 $message,
41 Some($flags.into()),
42 std::panic::Location::caller(),
43 );
44 }};
45 (TODO($bug_url:literal), $message:expr $(,)?) => {{
46 $crate::__track_stub_inner(
47 $crate::bug_ref!($bug_url),
48 $message,
49 None,
50 std::panic::Location::caller(),
51 );
52 }};
53}
54
55#[macro_export]
66macro_rules! track_stub_log {
67 ($level:expr, TODO($bug_url:literal), $message:expr, $flags:expr $(,)?) => {{
68 $crate::__track_stub_inner_with_level(
69 $level,
70 $crate::bug_ref!($bug_url),
71 $message,
72 Some($flags.into()),
73 std::panic::Location::caller(),
74 );
75 }};
76 ($level:expr, TODO($bug_url:literal), $message:expr $(,)?) => {{
77 $crate::__track_stub_inner_with_level(
78 $level,
79 $crate::bug_ref!($bug_url),
80 $message,
81 None,
82 std::panic::Location::caller(),
83 );
84 }};
85}
86
87#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
91struct Invocation {
92 location: &'static Location<'static>,
93 message: String,
94 bug: BugRef,
95}
96
97trait InvocationLookup {
100 fn location(&self) -> &'static Location<'static>;
101 fn message(&self) -> &str;
102 fn bug(&self) -> BugRef;
103}
104
105impl Hash for dyn InvocationLookup + '_ {
106 fn hash<H: Hasher>(&self, state: &mut H) {
107 self.location().hash(state);
108 self.message().hash(state);
109 self.bug().hash(state);
110 }
111}
112
113impl PartialEq for dyn InvocationLookup + '_ {
114 fn eq(&self, other: &Self) -> bool {
115 self.location() == other.location()
116 && self.message() == other.message()
117 && self.bug() == other.bug()
118 }
119}
120
121impl Eq for dyn InvocationLookup + '_ {}
122
123impl InvocationLookup for Invocation {
124 fn location(&self) -> &'static Location<'static> {
125 self.location
126 }
127
128 fn message(&self) -> &str {
129 &self.message
130 }
131
132 fn bug(&self) -> BugRef {
133 self.bug
134 }
135}
136
137impl<'a> std::borrow::Borrow<dyn InvocationLookup + 'a> for Invocation {
138 fn borrow(&self) -> &(dyn InvocationLookup + 'a) {
139 self
140 }
141}
142
143struct InvocationKey<'a> {
149 location: &'static Location<'static>,
150 message: &'a str,
151 bug: BugRef,
152}
153
154impl<'a> InvocationLookup for InvocationKey<'a> {
155 fn location(&self) -> &'static Location<'static> {
156 self.location
157 }
158
159 fn message(&self) -> &str {
160 self.message
161 }
162
163 fn bug(&self) -> BugRef {
164 self.bug
165 }
166}
167
168#[derive(Default)]
169struct Counts {
170 by_flags: HashMap<Option<u64>, u64>,
171 contexts_seen: HashSet<FlyByteStr>,
172}
173
174#[doc(hidden)]
175#[inline]
176pub fn __track_stub_inner(
177 bug: BugRef,
178 message: &str,
179 flags: Option<u64>,
180 location: &'static Location<'static>,
181) -> u64 {
182 __track_stub_inner_with_level(log::Level::Debug, bug, message, flags, location)
183}
184
185#[doc(hidden)]
186#[inline]
187pub fn __track_stub_inner_with_level(
188 level: log::Level,
189 bug: BugRef,
190 message: &str,
191 flags: Option<u64>,
192 location: &'static Location<'static>,
193) -> u64 {
194 let mut counts = STUB_COUNTS.lock();
195 let key = InvocationKey { location, message, bug };
196
197 if let Some(message_counts) = counts.get_mut(&key as &dyn InvocationLookup) {
198 let context_count = message_counts.by_flags.entry(flags).or_default();
199 if let Some(current_context) = CONTEXT_NAME_CALLBACK.lock().as_ref().map(|cb| cb()) {
200 message_counts.contexts_seen.insert(current_context);
201 }
202 if *context_count == 0 {
203 match flags {
204 Some(flags) => {
205 log::log!(level, tag = "track_stub", location:%; "{bug} {message}: 0x{flags:x}");
206 }
207 None => {
208 log::log!(level, tag = "track_stub", location:%; "{bug} {message}");
209 }
210 }
211 }
212 *context_count += 1;
213 return *context_count;
214 }
215
216 match flags {
217 Some(flags) => {
218 log::log!(level, tag = "track_stub", location:%; "{bug} {message}: 0x{flags:x}");
219 }
220 None => {
221 log::log!(level, tag = "track_stub", location:%; "{bug} {message}");
222 }
223 }
224
225 let mut message_counts = Counts::default();
226 if let Some(current_context) = CONTEXT_NAME_CALLBACK.lock().as_ref().map(|cb| cb()) {
227 message_counts.contexts_seen.insert(current_context);
228 }
229 message_counts.by_flags.insert(flags, 1);
230 counts.insert(Invocation { location, message: String::from(message), bug }, message_counts);
231 1
232}
233
234pub fn register_context_name_callback(cb: impl Fn() -> FlyByteStr + Send + Sync + 'static) {
237 *CONTEXT_NAME_CALLBACK.lock() = Some(Box::new(cb));
238}
239
240pub fn track_stub_lazy_node_callback() -> BoxFuture<'static, Result<Inspector, anyhow::Error>> {
245 Box::pin(async {
246 let inspector = Inspector::default();
247 for (Invocation { location, message, bug }, context_counts) in STUB_COUNTS.lock().iter() {
248 inspector.root().atomic_update(|root| {
249 root.record_child(message, |message_node| {
250 message_node.record_string("file", location.file());
251 message_node.record_uint("line", location.line().into());
252 message_node.record_string("bug", bug.to_string());
253
254 if !context_counts.contexts_seen.is_empty() {
255 let mut contexts =
256 context_counts.contexts_seen.iter().cloned().collect::<Vec<_>>();
257 contexts.sort();
258 let contexts_prop =
259 message_node.create_string_array("contexts", contexts.len());
260 for (i, context) in contexts.iter().enumerate() {
261 contexts_prop.set(i, context.to_string());
262 }
263 message_node.record(contexts_prop);
264 }
265
266 let mut context_counts = context_counts.by_flags.clone();
268
269 if let Some(no_context_count) = context_counts.remove(&None) {
270 message_node.record_uint("count", no_context_count);
273 }
274
275 if !context_counts.is_empty() {
276 message_node.record_child("counts", |counts_node| {
277 for (context, count) in context_counts {
278 if let Some(c) = context {
279 counts_node.record_uint(format!("0x{c:x}"), count);
280 }
281 }
282 });
283 }
284 });
285 });
286 }
287 Ok(inspector)
288 })
289}
290
291#[macro_export]
295macro_rules! bug_ref {
296 ($bug_url:literal) => {{
297 const __REF: $crate::BugRef = match $crate::BugRef::from_str($bug_url) {
299 Some(b) => b,
300 None => panic!("bug references must have the form `https://fxbug.dev/123456789`"),
301 };
302 __REF
303 }};
304}
305
306#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
310pub struct BugRef {
311 number: u64,
312}
313
314impl BugRef {
315 #[doc(hidden)] pub const fn from_str(url: &'static str) -> Option<Self> {
317 let expected_prefix = b"https://fxbug.dev/";
318 let url = str::as_bytes(url);
319
320 if url.len() < expected_prefix.len() {
321 return None;
322 }
323 let (scheme_and_domain, number_str) = url.split_at(expected_prefix.len());
324 if number_str.is_empty() {
325 return None;
326 }
327
328 {
330 let mut i = 0;
331 while i < scheme_and_domain.len() {
332 if scheme_and_domain[i] != expected_prefix[i] {
333 return None;
334 }
335 i += 1;
336 }
337 }
338
339 let mut number = 0;
341 {
342 let mut i = 0;
343 while i < number_str.len() {
344 number *= 10;
345 number += match number_str[i] {
346 b'0' => 0,
347 b'1' => 1,
348 b'2' => 2,
349 b'3' => 3,
350 b'4' => 4,
351 b'5' => 5,
352 b'6' => 6,
353 b'7' => 7,
354 b'8' => 8,
355 b'9' => 9,
356 _ => return None,
357 };
358 i += 1;
359 }
360 }
361
362 if number != 0 { Some(Self { number }) } else { None }
363 }
364}
365
366impl From<NonZeroU64> for BugRef {
367 fn from(value: NonZeroU64) -> Self {
369 Self { number: value.get() }
370 }
371}
372
373impl Into<NonZeroU64> for BugRef {
374 fn into(self) -> NonZeroU64 {
376 NonZeroU64::new(self.number).unwrap()
377 }
378}
379
380impl std::fmt::Display for BugRef {
381 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383 write!(f, "https://fxbug.dev/{}", self.number)
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use diagnostics_assertions::assert_data_tree;
391
392 #[test]
393 fn valid_url_parses() {
394 assert_eq!(BugRef::from_str("https://fxbug.dev/1234567890").unwrap().number, 1234567890);
395 }
396
397 #[test]
398 fn missing_prefix_fails() {
399 assert_eq!(BugRef::from_str("1234567890"), None);
400 }
401
402 #[test]
403 fn missing_number_fails() {
404 assert_eq!(BugRef::from_str("https://fxbug.dev/"), None);
405 }
406
407 #[test]
408 fn short_prefixes_fail() {
409 assert_eq!(BugRef::from_str("b/1234567890"), None);
410 assert_eq!(BugRef::from_str("fxb/1234567890"), None);
411 assert_eq!(BugRef::from_str("fxbug.dev/1234567890"), None);
412 }
413
414 #[test]
415 fn invalid_characters_fail() {
416 assert_eq!(BugRef::from_str("https://fxbug.dev/123a45"), None);
417 }
418
419 #[test]
420 fn zero_bug_number_fails() {
421 assert_eq!(BugRef::from_str("https://fxbug.dev/0"), None);
422 }
423
424 #[fuchsia::test]
425 async fn test_track_stub() {
426 let inspector = Inspector::default();
427 inspector.root().record_lazy_child("stubs", track_stub_lazy_node_callback);
428
429 let call_stub = || {
430 track_stub!(TODO("https://fxbug.dev/1"), "test stub");
431 std::line!() as u64 - 1
432 };
433
434 let file = std::panic::Location::caller().file();
435 let line = call_stub();
436
437 assert_data_tree!(inspector, root: {
438 stubs: {
439 "test stub": {
440 bug: "https://fxbug.dev/1",
441 count: 1u64,
442 file: file,
443 line: line,
444 }
445 }
446 });
447
448 call_stub();
449 assert_data_tree!(inspector, root: {
450 stubs: {
451 "test stub": {
452 bug: "https://fxbug.dev/1",
453 count: 2u64,
454 file: file,
455 line: line,
456 }
457 }
458 });
459 }
460
461 #[fuchsia::test]
462 async fn test_track_stub_different_callsites() {
463 let inspector = Inspector::default();
464 inspector.root().record_lazy_child("stubs", track_stub_lazy_node_callback);
465
466 let loc1 = std::panic::Location::caller();
467 track_stub!(TODO("https://fxbug.dev/1"), "stub 1");
468 let loc2 = std::panic::Location::caller();
469 track_stub!(TODO("https://fxbug.dev/2"), "stub 2");
470
471 assert_data_tree!(inspector, root: {
472 stubs: {
473 "stub 1": {
474 bug: "https://fxbug.dev/1",
475 count: 1u64,
476 file: loc1.file(),
477 line: (loc1.line() + 1) as u64,
478 },
479 "stub 2": {
480 bug: "https://fxbug.dev/2",
481 count: 1u64,
482 file: loc2.file(),
483 line: (loc2.line() + 1) as u64,
484 }
485 }
486 });
487 }
488
489 #[fuchsia::test]
490 async fn test_track_stub_with_flags() {
491 let inspector = Inspector::default();
492 inspector.root().record_lazy_child("stubs", track_stub_lazy_node_callback);
493
494 let call_stub = |flags: u64| {
495 track_stub!(TODO("https://fxbug.dev/3"), "stub with flags", flags);
496 std::line!() - 1
497 };
498
499 let file = std::panic::Location::caller().file();
500 let line = call_stub(0x1);
501 call_stub(0x2);
502 call_stub(0x1);
503
504 assert_data_tree!(inspector, root: {
505 stubs: {
506 "stub with flags": {
507 bug: "https://fxbug.dev/3",
508 file: file,
509 line: line as u64,
510 counts: {
511 "0x1": 2u64,
512 "0x2": 1u64,
513 }
514 }
515 }
516 });
517 }
518
519 #[fuchsia::test]
520 async fn test_track_stub_with_context() {
521 let inspector = Inspector::default();
522 inspector.root().record_lazy_child("stubs", track_stub_lazy_node_callback);
523
524 let current_context = std::sync::Arc::new(Mutex::new("SHOULD NOT SHOW UP"));
525 let context_clone = current_context.clone();
526 register_context_name_callback(move || FlyByteStr::from(*context_clone.lock()));
527
528 let call_stub_with_context = |context| {
529 *current_context.lock() = context;
530 track_stub!(TODO("https://fxbug.dev/4"), "stub with context");
531 };
532 let line = std::line!() as u64 - 2;
533
534 call_stub_with_context("context1");
535 call_stub_with_context("context2");
536
537 assert_data_tree!(inspector, root: {
538 stubs: {
539 "stub with context": {
540 bug: "https://fxbug.dev/4",
541 count: 2u64,
542 file: std::file!(),
543 line: line,
544 contexts: vec!["context1", "context2"]
545 }
546 }
547 });
548 }
549}