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::{Arc, LazyLock};
19
20static STUB_COUNTS: LazyLock<Mutex<HashMap<Invocation, Counts>>> =
21 LazyLock::new(|| Mutex::new(HashMap::new()));
22
23static CONTEXT_NAME_CALLBACK: Mutex<Option<Arc<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 current_context = {
195 let cb = CONTEXT_NAME_CALLBACK.lock().clone();
196 cb.as_ref().map(|cb| cb())
197 };
198
199 let mut counts = STUB_COUNTS.lock();
200 let key = InvocationKey { location, message, bug };
201
202 if let Some(message_counts) = counts.get_mut(&key as &dyn InvocationLookup) {
203 let context_count = message_counts.by_flags.entry(flags).or_default();
204 if let Some(ref current_context) = current_context {
205 message_counts.contexts_seen.insert(current_context.clone());
206 }
207 if *context_count == 0 {
208 match flags {
209 Some(flags) => {
210 log::log!(level, tag = "track_stub", location:%; "{bug} {message}: 0x{flags:x}");
211 }
212 None => {
213 log::log!(level, tag = "track_stub", location:%; "{bug} {message}");
214 }
215 }
216 }
217 *context_count += 1;
218 return *context_count;
219 }
220
221 match flags {
222 Some(flags) => {
223 log::log!(level, tag = "track_stub", location:%; "{bug} {message}: 0x{flags:x}");
224 }
225 None => {
226 log::log!(level, tag = "track_stub", location:%; "{bug} {message}");
227 }
228 }
229
230 let mut message_counts = Counts::default();
231 if let Some(current_context) = current_context {
232 message_counts.contexts_seen.insert(current_context);
233 }
234 message_counts.by_flags.insert(flags, 1);
235 counts.insert(Invocation { location, message: String::from(message), bug }, message_counts);
236 1
237}
238
239pub fn register_context_name_callback(cb: impl Fn() -> FlyByteStr + Send + Sync + 'static) {
242 *CONTEXT_NAME_CALLBACK.lock() = Some(Arc::new(cb));
243}
244
245pub fn track_stub_lazy_node_callback() -> BoxFuture<'static, Result<Inspector, anyhow::Error>> {
250 Box::pin(async {
251 let inspector = Inspector::default();
252 for (Invocation { location, message, bug }, context_counts) in STUB_COUNTS.lock().iter() {
253 inspector.root().atomic_update(|root| {
254 root.record_child(message, |message_node| {
255 message_node.record_string("file", location.file());
256 message_node.record_uint("line", location.line().into());
257 message_node.record_string("bug", bug.to_string());
258
259 if !context_counts.contexts_seen.is_empty() {
260 let mut contexts =
261 context_counts.contexts_seen.iter().cloned().collect::<Vec<_>>();
262 contexts.sort();
263 let contexts_prop =
264 message_node.create_string_array("contexts", contexts.len());
265 for (i, context) in contexts.iter().enumerate() {
266 contexts_prop.set(i, context.to_string());
267 }
268 message_node.record(contexts_prop);
269 }
270
271 let mut context_counts = context_counts.by_flags.clone();
273
274 if let Some(no_context_count) = context_counts.remove(&None) {
275 message_node.record_uint("count", no_context_count);
278 }
279
280 if !context_counts.is_empty() {
281 message_node.record_child("counts", |counts_node| {
282 for (context, count) in context_counts {
283 if let Some(c) = context {
284 counts_node.record_uint(format!("0x{c:x}"), count);
285 }
286 }
287 });
288 }
289 });
290 });
291 }
292 Ok(inspector)
293 })
294}
295
296#[macro_export]
300macro_rules! bug_ref {
301 ($bug_url:literal) => {{
302 const __REF: $crate::BugRef = match $crate::BugRef::from_str($bug_url) {
304 Some(b) => b,
305 None => panic!("bug references must have the form `https://fxbug.dev/123456789`"),
306 };
307 __REF
308 }};
309}
310
311#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
315pub struct BugRef {
316 number: u64,
317}
318
319impl BugRef {
320 #[doc(hidden)] pub const fn from_str(url: &'static str) -> Option<Self> {
322 let expected_prefix = b"https://fxbug.dev/";
323 let url = str::as_bytes(url);
324
325 if url.len() < expected_prefix.len() {
326 return None;
327 }
328 let (scheme_and_domain, number_str) = url.split_at(expected_prefix.len());
329 if number_str.is_empty() {
330 return None;
331 }
332
333 {
335 let mut i = 0;
336 while i < scheme_and_domain.len() {
337 if scheme_and_domain[i] != expected_prefix[i] {
338 return None;
339 }
340 i += 1;
341 }
342 }
343
344 let mut number = 0;
346 {
347 let mut i = 0;
348 while i < number_str.len() {
349 number *= 10;
350 number += match number_str[i] {
351 b'0' => 0,
352 b'1' => 1,
353 b'2' => 2,
354 b'3' => 3,
355 b'4' => 4,
356 b'5' => 5,
357 b'6' => 6,
358 b'7' => 7,
359 b'8' => 8,
360 b'9' => 9,
361 _ => return None,
362 };
363 i += 1;
364 }
365 }
366
367 if number != 0 { Some(Self { number }) } else { None }
368 }
369}
370
371impl From<NonZeroU64> for BugRef {
372 fn from(value: NonZeroU64) -> Self {
374 Self { number: value.get() }
375 }
376}
377
378impl Into<NonZeroU64> for BugRef {
379 fn into(self) -> NonZeroU64 {
381 NonZeroU64::new(self.number).unwrap()
382 }
383}
384
385impl std::fmt::Display for BugRef {
386 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388 write!(f, "https://fxbug.dev/{}", self.number)
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use diagnostics_assertions::assert_data_tree;
396
397 #[test]
398 fn valid_url_parses() {
399 assert_eq!(BugRef::from_str("https://fxbug.dev/1234567890").unwrap().number, 1234567890);
400 }
401
402 #[test]
403 fn missing_prefix_fails() {
404 assert_eq!(BugRef::from_str("1234567890"), None);
405 }
406
407 #[test]
408 fn missing_number_fails() {
409 assert_eq!(BugRef::from_str("https://fxbug.dev/"), None);
410 }
411
412 #[test]
413 fn short_prefixes_fail() {
414 assert_eq!(BugRef::from_str("b/1234567890"), None);
415 assert_eq!(BugRef::from_str("fxb/1234567890"), None);
416 assert_eq!(BugRef::from_str("fxbug.dev/1234567890"), None);
417 }
418
419 #[test]
420 fn invalid_characters_fail() {
421 assert_eq!(BugRef::from_str("https://fxbug.dev/123a45"), None);
422 }
423
424 #[test]
425 fn zero_bug_number_fails() {
426 assert_eq!(BugRef::from_str("https://fxbug.dev/0"), None);
427 }
428
429 #[fuchsia::test]
430 async fn test_track_stub() {
431 let inspector = Inspector::default();
432 inspector.root().record_lazy_child("stubs", track_stub_lazy_node_callback);
433
434 let call_stub = || {
435 track_stub!(TODO("https://fxbug.dev/1"), "test stub");
436 std::line!() as u64 - 1
437 };
438
439 let file = std::panic::Location::caller().file();
440 let line = call_stub();
441
442 assert_data_tree!(inspector, root: {
443 stubs: {
444 "test stub": {
445 bug: "https://fxbug.dev/1",
446 count: 1u64,
447 file: file,
448 line: line,
449 }
450 }
451 });
452
453 call_stub();
454 assert_data_tree!(inspector, root: {
455 stubs: {
456 "test stub": {
457 bug: "https://fxbug.dev/1",
458 count: 2u64,
459 file: file,
460 line: line,
461 }
462 }
463 });
464 }
465
466 #[fuchsia::test]
467 async fn test_track_stub_different_callsites() {
468 let inspector = Inspector::default();
469 inspector.root().record_lazy_child("stubs", track_stub_lazy_node_callback);
470
471 let loc1 = std::panic::Location::caller();
472 track_stub!(TODO("https://fxbug.dev/1"), "stub 1");
473 let loc2 = std::panic::Location::caller();
474 track_stub!(TODO("https://fxbug.dev/2"), "stub 2");
475
476 assert_data_tree!(inspector, root: {
477 stubs: {
478 "stub 1": {
479 bug: "https://fxbug.dev/1",
480 count: 1u64,
481 file: loc1.file(),
482 line: (loc1.line() + 1) as u64,
483 },
484 "stub 2": {
485 bug: "https://fxbug.dev/2",
486 count: 1u64,
487 file: loc2.file(),
488 line: (loc2.line() + 1) as u64,
489 }
490 }
491 });
492 }
493
494 #[fuchsia::test]
495 async fn test_track_stub_with_flags() {
496 let inspector = Inspector::default();
497 inspector.root().record_lazy_child("stubs", track_stub_lazy_node_callback);
498
499 let call_stub = |flags: u64| {
500 track_stub!(TODO("https://fxbug.dev/3"), "stub with flags", flags);
501 std::line!() - 1
502 };
503
504 let file = std::panic::Location::caller().file();
505 let line = call_stub(0x1);
506 call_stub(0x2);
507 call_stub(0x1);
508
509 assert_data_tree!(inspector, root: {
510 stubs: {
511 "stub with flags": {
512 bug: "https://fxbug.dev/3",
513 file: file,
514 line: line as u64,
515 counts: {
516 "0x1": 2u64,
517 "0x2": 1u64,
518 }
519 }
520 }
521 });
522 }
523
524 #[fuchsia::test]
525 async fn test_track_stub_with_context() {
526 let inspector = Inspector::default();
527 inspector.root().record_lazy_child("stubs", track_stub_lazy_node_callback);
528
529 let current_context = std::sync::Arc::new(Mutex::new("SHOULD NOT SHOW UP"));
530 let context_clone = current_context.clone();
531 register_context_name_callback(move || FlyByteStr::from(*context_clone.lock()));
532
533 let call_stub_with_context = |context| {
534 *current_context.lock() = context;
535 track_stub!(TODO("https://fxbug.dev/4"), "stub with context");
536 };
537 let line = std::line!() as u64 - 2;
538
539 call_stub_with_context("context1");
540 call_stub_with_context("context2");
541
542 assert_data_tree!(inspector, root: {
543 stubs: {
544 "stub with context": {
545 bug: "https://fxbug.dev/4",
546 count: 2u64,
547 file: std::file!(),
548 line: line,
549 contexts: vec!["context1", "context2"]
550 }
551 }
552 });
553 }
554
555 #[fuchsia::test]
556 async fn test_track_stub_reentrant_context_callback() {
557 let entered = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
558 let entered_clone = entered.clone();
559 register_context_name_callback(move || {
560 if !entered_clone.swap(true, std::sync::atomic::Ordering::SeqCst) {
561 track_stub!(TODO("https://fxbug.dev/9991"), "nested stub");
562 }
563 FlyByteStr::from("reentrant_ctx")
564 });
565
566 track_stub!(TODO("https://fxbug.dev/9992"), "outer stub");
567 }
568}