1#[cfg(feature = "std")]
54use std::sync::OnceLock;
55
56#[allow(clippy::struct_excessive_bools)]
60#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
61pub struct ProfileFlags {
62 pub memory: bool,
63 pub cpu: bool,
64 pub cascade: bool,
65 pub heap: bool,
66 pub jsonl: bool,
67 pub detail: bool,
68}
69
70impl ProfileFlags {
71 fn parse(value: &str) -> Self {
72 let mut f = Self::default();
73 for tok in value.split(',') {
74 let t = tok.trim();
75 if t.eq_ignore_ascii_case("memory") || t.eq_ignore_ascii_case("mem") {
76 f.memory = true;
77 } else if t.eq_ignore_ascii_case("cpu") || t.eq_ignore_ascii_case("perf") {
78 f.cpu = true;
79 } else if t.eq_ignore_ascii_case("cascade") || t.eq_ignore_ascii_case("css") {
80 f.cascade = true;
81 } else if t.eq_ignore_ascii_case("heap") {
82 f.heap = true;
83 } else if t.eq_ignore_ascii_case("jsonl") {
84 f.jsonl = true;
85 } else if t.eq_ignore_ascii_case("detail") {
86 f.detail = true;
87 }
88 }
89 f
90 }
91}
92
93#[cfg(feature = "std")]
94#[inline]
95pub fn flags() -> ProfileFlags {
96 static FLAGS: OnceLock<ProfileFlags> = OnceLock::new();
97 *FLAGS.get_or_init(|| {
98 std::env::var("AZ_PROFILE")
99 .map(|v| ProfileFlags::parse(&v))
100 .unwrap_or_default()
101 })
102}
103
104#[cfg(not(feature = "std"))]
106#[inline]
107pub fn flags() -> ProfileFlags {
108 let _ = ProfileFlags::parse;
109 ProfileFlags::default()
110}
111
112#[cfg(feature = "std")]
115#[inline]
116pub fn out_path() -> Option<&'static str> {
117 static PATH: OnceLock<Option<String>> = OnceLock::new();
118 PATH.get_or_init(|| std::env::var("AZ_PROFILE_OUT").ok())
119 .as_deref()
120}
121
122#[cfg(not(feature = "std"))]
124#[inline]
125pub fn out_path() -> Option<&'static str> {
126 None
127}
128
129#[inline]
130#[must_use] pub fn memory_enabled() -> bool { flags().memory }
131
132#[inline]
133#[must_use] pub fn cpu_enabled() -> bool { flags().cpu }
134
135#[inline]
136#[must_use] pub fn cascade_enabled() -> bool { flags().cascade }
137
138#[inline]
139#[must_use] pub fn heap_enabled() -> bool { flags().heap }
140
141#[inline]
142#[must_use] pub fn jsonl_enabled() -> bool { flags().jsonl }
143
144#[inline]
145#[must_use] pub fn detail_enabled() -> bool { flags().detail }
146
147#[cfg(test)]
148mod tests {
149 use super::ProfileFlags;
150
151 #[test]
152 fn parse_single_token() {
153 let f = ProfileFlags::parse("cpu");
154 assert!(f.cpu && !f.memory && !f.heap);
155 }
156
157 #[test]
158 fn parse_multiple_tokens() {
159 let f = ProfileFlags::parse("heap,jsonl,detail");
160 assert!(f.heap && f.jsonl && f.detail);
161 assert!(!f.cpu && !f.memory);
162 }
163
164 #[test]
165 fn parse_is_case_insensitive_and_trims() {
166 let f = ProfileFlags::parse(" Heap , JSONL ");
167 assert!(f.heap && f.jsonl);
168 }
169
170 #[test]
171 fn parse_ignores_unknown_tokens() {
172 let f = ProfileFlags::parse("cpu,bogus,heap");
173 assert!(f.cpu && f.heap);
174 }
175
176 #[test]
177 fn parse_accepts_aliases() {
178 let f = ProfileFlags::parse("mem,perf,css");
179 assert!(f.memory && f.cpu && f.cascade);
180 }
181}
182
183#[cfg(test)]
184#[allow(clippy::bool_assert_comparison)]
185mod autotest_generated {
186 use alloc::{
187 string::{String, ToString},
188 vec::Vec,
189 };
190
191 use super::*;
192
193 const CANONICAL: [&str; 6] = ["memory", "cpu", "cascade", "heap", "jsonl", "detail"];
197
198 const ALIASES: [(&str, &str); 3] = [("mem", "memory"), ("perf", "cpu"), ("css", "cascade")];
200
201 fn field(f: &ProfileFlags, idx: usize) -> bool {
203 match idx {
204 0 => f.memory,
205 1 => f.cpu,
206 2 => f.cascade,
207 3 => f.heap,
208 4 => f.jsonl,
209 5 => f.detail,
210 _ => unreachable!("CANONICAL has 6 entries"),
211 }
212 }
213
214 fn encode(f: ProfileFlags) -> String {
217 let mut parts: Vec<&str> = Vec::new();
218 if f.memory {
219 parts.push("memory");
220 }
221 if f.cpu {
222 parts.push("cpu");
223 }
224 if f.cascade {
225 parts.push("cascade");
226 }
227 if f.heap {
228 parts.push("heap");
229 }
230 if f.jsonl {
231 parts.push("jsonl");
232 }
233 if f.detail {
234 parts.push("detail");
235 }
236 parts.join(",")
237 }
238
239 fn from_mask(mask: u8) -> ProfileFlags {
241 ProfileFlags {
242 memory: mask & 0b00_0001 != 0,
243 cpu: mask & 0b00_0010 != 0,
244 cascade: mask & 0b00_0100 != 0,
245 heap: mask & 0b00_1000 != 0,
246 jsonl: mask & 0b01_0000 != 0,
247 detail: mask & 0b10_0000 != 0,
248 }
249 }
250
251 fn any_set(f: &ProfileFlags) -> bool {
252 f.memory || f.cpu || f.cascade || f.heap || f.jsonl || f.detail
253 }
254
255 fn assert_flags_are_justified(input: &str, f: &ProfileFlags) {
259 let lower = input.to_ascii_lowercase();
260 if f.memory {
261 assert!(lower.contains("memory") || lower.contains("mem"));
262 }
263 if f.cpu {
264 assert!(lower.contains("cpu") || lower.contains("perf"));
265 }
266 if f.cascade {
267 assert!(lower.contains("cascade") || lower.contains("css"));
268 }
269 if f.heap {
270 assert!(lower.contains("heap"));
271 }
272 if f.jsonl {
273 assert!(lower.contains("jsonl"));
274 }
275 if f.detail {
276 assert!(lower.contains("detail"));
277 }
278 }
279
280 #[test]
283 fn parse_empty_input_is_all_off() {
284 assert_eq!(ProfileFlags::parse(""), ProfileFlags::default());
285 assert!(!any_set(&ProfileFlags::parse("")));
286 }
287
288 #[test]
289 fn parse_whitespace_only_is_all_off() {
290 for input in [
291 " ",
292 " ",
293 "\t",
294 "\n",
295 "\r\n",
296 "\t\n",
297 " \t\r\n\x0c ",
298 "\u{a0}", "\u{2003}", "\u{3000}", ] {
302 let f = ProfileFlags::parse(input);
303 assert_eq!(f, ProfileFlags::default(), "input {input:?} set a flag");
304 }
305 }
306
307 #[test]
308 fn parse_separators_only_is_all_off() {
309 for input in [",", ",,", ",,,,,,,,,,", " , , ", "\t,\n,\r", ",,cpu,,"] {
310 let f = ProfileFlags::parse(input);
311 assert_eq!(f.memory, false);
312 assert_eq!(f.cascade, false);
313 assert_eq!(f.heap, false);
314 assert_eq!(f.jsonl, false);
315 assert_eq!(f.detail, false);
316 }
317 assert!(ProfileFlags::parse(",,cpu,,").cpu);
319 assert!(!ProfileFlags::parse(",,,,").cpu);
320 }
321
322 #[test]
325 fn parse_garbage_never_panics_and_sets_nothing() {
326 for input in [
327 "\0",
328 "\0\0\0",
329 "cpu\0", "%s%n%s%n",
331 "../../etc/passwd",
332 "{\"cpu\":true}",
333 "-1",
334 "--cpu",
335 "cpu=1",
336 "cpu=true",
337 "CPU;HEAP", "cpu heap", "cpu\tjsonl",
340 "cpux",
341 "xcpu",
342 "cp",
343 "c,p,u",
344 "\u{7f}\u{1}\u{2}",
345 "\\x63\\x70\\x75",
346 ] {
347 let f = ProfileFlags::parse(input);
348 assert_eq!(
349 f,
350 ProfileFlags::default(),
351 "garbage input {input:?} should set no flags, got {f:?}"
352 );
353 }
354 }
355
356 #[test]
357 fn parse_leading_trailing_junk_is_trimmed_or_rejected() {
358 assert!(ProfileFlags::parse(" cpu ").cpu);
360 assert!(ProfileFlags::parse("\t\ncpu\r\n").cpu);
361 assert!(ProfileFlags::parse(" heap , jsonl ").heap);
362 assert!(ProfileFlags::parse(" heap , jsonl ").jsonl);
363
364 assert_eq!(ProfileFlags::parse("cpu;garbage"), ProfileFlags::default());
367 assert_eq!(ProfileFlags::parse("garbage;cpu"), ProfileFlags::default());
368 assert_eq!(ProfileFlags::parse("'cpu'"), ProfileFlags::default());
369 assert_eq!(ProfileFlags::parse("\"cpu\""), ProfileFlags::default());
370
371 let f = ProfileFlags::parse("garbage,cpu,;;;,heap");
373 assert!(f.cpu && f.heap);
374 assert!(!f.memory && !f.cascade && !f.jsonl && !f.detail);
375 }
376
377 #[test]
380 fn parse_boundary_numeric_strings_are_ignored() {
381 for input in [
382 "0",
383 "-0",
384 "+0",
385 "1",
386 "9223372036854775807", "-9223372036854775808", "9223372036854775808", "18446744073709551615", "18446744073709551616", "340282366920938463463374607431768211456",
392 "1.7976931348623157e308", "5e-324", "1e309", "NaN",
396 "nan",
397 "inf",
398 "-inf",
399 "infinity",
400 "0x7fffffffffffffff",
401 "0b1111",
402 "1e",
403 ".",
404 "..",
405 ] {
406 let f = ProfileFlags::parse(input);
407 assert_eq!(
408 f,
409 ProfileFlags::default(),
410 "numeric-ish input {input:?} should set no flags, got {f:?}"
411 );
412 }
413 }
414
415 #[test]
416 fn parse_numeric_tokens_mixed_with_valid_tokens_do_not_corrupt_flags() {
417 let f = ProfileFlags::parse("NaN,cpu,inf,-0,9223372036854775807,heap,1e309");
418 assert!(f.cpu && f.heap);
419 assert!(!f.memory && !f.cascade && !f.jsonl && !f.detail);
420 }
421
422 #[test]
425 fn parse_extremely_long_single_token_does_not_panic_or_hang() {
426 let huge: String = std::iter::repeat_n('a', 1_000_000).collect();
427 assert_eq!(ProfileFlags::parse(&huge), ProfileFlags::default());
428
429 let mut prefixed = String::from("cpu");
432 prefixed.push_str(&huge);
433 assert_eq!(ProfileFlags::parse(&prefixed), ProfileFlags::default());
434 }
435
436 #[test]
437 fn parse_million_separators_does_not_panic_or_hang() {
438 let commas: String = std::iter::repeat_n(',', 1_000_000).collect();
439 assert_eq!(ProfileFlags::parse(&commas), ProfileFlags::default());
440
441 let mut with_token = commas.clone();
443 with_token.push_str("cpu");
444 assert!(ProfileFlags::parse(&with_token).cpu);
445 }
446
447 #[test]
448 fn parse_repeated_token_250k_times_is_idempotent() {
449 let repeated = "cpu,".repeat(250_000);
450 let f = ProfileFlags::parse(&repeated);
451 assert!(f.cpu);
452 assert_eq!(f, ProfileFlags::parse("cpu"));
455 }
456
457 #[test]
458 fn parse_deeply_nested_brackets_does_not_stack_overflow() {
459 let depth = 10_000;
460 let mut nested = String::new();
461 for _ in 0..depth {
462 nested.push('[');
463 }
464 nested.push_str("cpu");
465 for _ in 0..depth {
466 nested.push(']');
467 }
468 assert_eq!(ProfileFlags::parse(&nested), ProfileFlags::default());
471
472 let nested_csv = "[,".repeat(depth);
474 assert_eq!(ProfileFlags::parse(&nested_csv), ProfileFlags::default());
475 }
476
477 #[test]
478 fn parse_many_distinct_unknown_tokens_does_not_hang() {
479 let mut s = String::new();
480 for i in 0..100_000u32 {
481 s.push_str(&i.to_string());
482 s.push(',');
483 }
484 s.push_str("detail");
485 let f = ProfileFlags::parse(&s);
486 assert!(f.detail);
487 assert!(!f.cpu && !f.memory && !f.cascade && !f.heap && !f.jsonl);
488 }
489
490 #[test]
493 fn parse_unicode_does_not_panic_and_matches_exactly() {
494 for input in [
496 "\u{1F600}", "\u{1F600},\u{1F4A9}",
498 "cpu\u{301}", "\u{301}cpu",
500 "\u{feff}cpu", "cpu", "СРU", "cpü",
504 "HEAP",
505 "日本語,中文,한국어",
506 "\u{202e}cpu", "e\u{301}\u{301}\u{301}",
508 ] {
509 let f = ProfileFlags::parse(input);
510 assert_eq!(
511 f,
512 ProfileFlags::default(),
513 "unicode input {input:?} should set no flags, got {f:?}"
514 );
515 }
516 }
517
518 #[test]
519 fn parse_trims_unicode_whitespace_around_ascii_tokens() {
520 assert!(ProfileFlags::parse("\u{a0}cpu\u{a0}").cpu);
523 assert!(ProfileFlags::parse("\u{2003}heap\u{2003}").heap);
524 assert!(ProfileFlags::parse("\u{3000}jsonl").jsonl);
525 }
526
527 #[test]
528 fn parse_unicode_mixed_with_valid_tokens_keeps_valid_ones() {
529 let f = ProfileFlags::parse("\u{1F600},cpu,日本語,heap,\u{202e}");
530 assert!(f.cpu && f.heap);
531 assert!(!f.memory && !f.cascade && !f.jsonl && !f.detail);
532 }
533
534 #[test]
537 fn parse_each_canonical_token_sets_exactly_one_flag() {
538 for (idx, tok) in CANONICAL.iter().enumerate() {
539 let f = ProfileFlags::parse(tok);
540 assert!(field(&f, idx), "token {tok:?} did not set its own flag");
541 let leaked = (0..CANONICAL.len())
542 .filter(|other| *other != idx)
543 .filter(|other| field(&f, *other))
544 .count();
545 assert_eq!(leaked, 0, "token {tok:?} leaked into another flag: {f:?}");
546 }
547 }
548
549 #[test]
550 fn parse_each_alias_is_equivalent_to_its_canonical_token() {
551 for (alias, canonical) in ALIASES {
552 assert_eq!(
553 ProfileFlags::parse(alias),
554 ProfileFlags::parse(canonical),
555 "alias {alias:?} != canonical {canonical:?}"
556 );
557 }
558 }
559
560 #[test]
561 fn parse_is_case_insensitive_for_every_token() {
562 for (idx, tok) in CANONICAL.iter().enumerate() {
563 for variant in [tok.to_ascii_uppercase(), tok.to_ascii_lowercase()] {
564 let f = ProfileFlags::parse(&variant);
565 assert!(field(&f, idx), "case variant {variant:?} did not match");
566 }
567 }
568 let all = ProfileFlags::parse("MEMORY,CPU,CaScAdE,HeAp,jSoNl,DETAIL");
569 assert_eq!(all, from_mask(0b11_1111));
570 }
571
572 #[test]
573 fn parse_is_order_independent() {
574 let a = ProfileFlags::parse("cpu,heap,detail");
575 let b = ProfileFlags::parse("detail,heap,cpu");
576 let c = ProfileFlags::parse("heap,detail,cpu");
577 assert_eq!(a, b);
578 assert_eq!(b, c);
579 }
580
581 #[test]
582 fn parse_is_monotone_unknown_tokens_never_unset_a_flag() {
583 let base = ProfileFlags::parse("cpu,heap");
584 for junk in ["bogus", "", " ", "\u{1F600}", "NaN", "-cpu", "heap;"] {
585 let mut with_junk = String::from("cpu,heap,");
586 with_junk.push_str(junk);
587 let f = ProfileFlags::parse(&with_junk);
588 assert!(f.cpu && f.heap, "junk {junk:?} cleared a flag: {f:?}");
589 assert_eq!(f, base, "junk {junk:?} changed the flag set");
590 }
591 }
592
593 #[test]
594 fn parse_jsonl_does_not_implicitly_enable_heap() {
595 let f = ProfileFlags::parse("jsonl");
598 assert!(f.jsonl);
599 assert!(!f.heap);
600
601 let d = ProfileFlags::parse("detail");
603 assert!(d.detail);
604 assert!(!d.heap);
605 }
606
607 #[test]
610 fn round_trip_all_64_flag_combinations() {
611 for mask in 0..64u8 {
612 let original = from_mask(mask);
613 let encoded = encode(original);
614 let decoded = ProfileFlags::parse(&encoded);
615 assert_eq!(
616 decoded, original,
617 "round-trip failed for mask {mask:#08b} (encoded {encoded:?})"
618 );
619 }
620 }
621
622 #[test]
623 fn round_trip_survives_whitespace_and_case_mangling() {
624 for mask in 0..64u8 {
625 let original = from_mask(mask);
626 let encoded = encode(original);
627 let mangled: Vec<String> = encoded
629 .split(',')
630 .filter(|s| !s.is_empty())
631 .map(|t| {
632 let mut s = String::from(" ");
633 s.push_str(&t.to_ascii_uppercase());
634 s.push_str(" \t");
635 s
636 })
637 .collect();
638 let decoded = ProfileFlags::parse(&mangled.join(","));
639 assert_eq!(decoded, original, "mangled round-trip failed for {mask:#08b}");
640 }
641 }
642
643 #[test]
644 fn round_trip_is_stable_under_re_encoding() {
645 for mask in 0..64u8 {
646 let f = from_mask(mask);
647 let once = encode(f);
648 let twice = encode(ProfileFlags::parse(&once));
649 assert_eq!(once, twice, "encode is not a fixed point for {mask:#08b}");
650 }
651 }
652
653 #[test]
656 fn parse_fuzz_is_deterministic_and_never_invents_flags() {
657 const PIECES: [&str; 24] = [
658 "cpu", "CPU", "mem", "memory", "cascade", "css", "heap", "jsonl", "detail", "perf",
659 ",", ";", " ", "\t", "\n", "", "x", "0", "NaN", "\u{1F600}", "\u{301}", "\u{a0}", "=",
660 "-",
661 ];
662
663 let mut state: u64 = 0x2545_F491_4F6C_DD1D;
665 let mut next = move || {
666 state = state
667 .wrapping_mul(6_364_136_223_846_793_005)
668 .wrapping_add(1_442_695_040_888_963_407);
669 (state >> 33) as usize
670 };
671
672 for _ in 0..2_000 {
673 let len = next() % 24;
674 let mut input = String::new();
675 for _ in 0..len {
676 input.push_str(PIECES[next() % PIECES.len()]);
677 }
678
679 let f = ProfileFlags::parse(&input);
680 assert_eq!(f, ProfileFlags::parse(&input), "parse is not deterministic");
682 assert_flags_are_justified(&input, &f);
684 let lower = input.to_ascii_lowercase();
686 if !["memory", "mem", "cpu", "perf", "cascade", "css", "heap", "jsonl", "detail"]
687 .iter()
688 .any(|t| lower.contains(t))
689 {
690 assert_eq!(f, ProfileFlags::default(), "flags set for {input:?}");
691 }
692 }
693 }
694
695 #[test]
698 fn default_flags_are_all_off() {
699 let d = ProfileFlags::default();
700 assert!(!any_set(&d));
701 assert_eq!(d, ProfileFlags::parse(""));
702 }
703
704 #[test]
705 fn flags_is_cached_and_stable_across_calls() {
706 let first = flags();
710 for _ in 0..1_000 {
711 assert_eq!(flags(), first, "flags() is not stable across calls");
712 }
713 }
714
715 #[test]
716 fn predicates_agree_with_flags() {
717 let f = flags();
718 assert_eq!(memory_enabled(), f.memory);
719 assert_eq!(cpu_enabled(), f.cpu);
720 assert_eq!(cascade_enabled(), f.cascade);
721 assert_eq!(heap_enabled(), f.heap);
722 assert_eq!(jsonl_enabled(), f.jsonl);
723 assert_eq!(detail_enabled(), f.detail);
724 }
725
726 #[test]
727 fn predicates_are_idempotent() {
728 for _ in 0..100 {
729 assert_eq!(memory_enabled(), memory_enabled());
730 assert_eq!(cpu_enabled(), cpu_enabled());
731 assert_eq!(cascade_enabled(), cascade_enabled());
732 assert_eq!(heap_enabled(), heap_enabled());
733 assert_eq!(jsonl_enabled(), jsonl_enabled());
734 assert_eq!(detail_enabled(), detail_enabled());
735 }
736 }
737
738 #[test]
739 fn out_path_does_not_panic_and_is_cached() {
740 let first = out_path();
741 for _ in 0..100 {
742 assert_eq!(out_path(), first, "out_path() is not stable across calls");
743 }
744 if let Some(p) = first {
747 assert_eq!(out_path().map(str::as_ptr), Some(p.as_ptr()));
748 }
749 }
750
751 #[cfg(not(feature = "std"))]
753 #[test]
754 fn nostd_profiling_is_always_off() {
755 assert_eq!(flags(), ProfileFlags::default());
756 assert_eq!(out_path(), None);
757 assert!(!memory_enabled());
758 assert!(!cpu_enabled());
759 assert!(!cascade_enabled());
760 assert!(!heap_enabled());
761 assert!(!jsonl_enabled());
762 assert!(!detail_enabled());
763 }
764
765 #[cfg(feature = "std")]
769 #[test]
770 fn std_flags_match_env_or_default_and_never_change() {
771 let observed = flags();
772 let expected_now = std::env::var("AZ_PROFILE")
773 .map(|v| ProfileFlags::parse(&v))
774 .unwrap_or_default();
775 assert_eq!(observed, expected_now);
778 assert_eq!(flags(), observed);
779 }
780}