krabka_telemetry/profiling/
config.rs1use {
9 clap::Args,
10 krabka_macros::RefinedNewtype,
11 krabka_units::{Time, parse, per_sec, secs},
12 refined_type::rule::GreaterI32,
13 std::io,
14 thiserror::Error,
15};
16
17#[derive(Debug, Error)]
19pub enum ProfilingError {
20 #[error("invalid profiling configuration: {0}")]
21 Config(String),
22 #[error(transparent)]
23 Io(#[from] io::Error),
24}
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq, RefinedNewtype)]
29#[refined(
30 rule(GreaterI32<0>),
31 quantity = Frequency,
32 label = "profiling sample frequency",
33 getter = hertz,
34 quantity_getter = frequency,
35 default = per_sec(99),
36 from_str
37)]
38pub struct ProfilingSampleFrequency(i32);
39
40#[derive(Args, Clone, Debug, PartialEq, krabka_macros::FieldDefaults)]
42pub struct ProfilingConfig {
43 #[arg(long, env = "KRABKA_PROFILING_CPU_DEFAULT_DURATION", default_value = "30s", value_parser = parse::positive_time)]
44 #[default(secs(30))]
45 pub profiling_cpu_default_duration: Time,
46 #[arg(long, env = "KRABKA_PROFILING_CPU_MAX_DURATION", default_value = "60s", value_parser = parse::positive_time)]
47 #[default(secs(60))]
48 pub profiling_cpu_max_duration: Time,
49 #[arg(
50 long,
51 env = "KRABKA_PROFILING_CPU_SAMPLE_FREQUENCY",
52 default_value = "99Hz"
53 )]
54 pub profiling_cpu_sample_frequency: ProfilingSampleFrequency,
55 #[arg(long, env = "KRABKA_PROFILING_HEAP_DEFAULT_DURATION", default_value = "5s", value_parser = parse::positive_time)]
56 #[default(secs(5))]
57 pub profiling_heap_default_duration: Time,
58 #[arg(long, env = "KRABKA_PROFILING_HEAP_MAX_DURATION", default_value = "30s", value_parser = parse::positive_time)]
59 #[default(secs(30))]
60 pub profiling_heap_max_duration: Time,
61 #[arg(
62 long,
63 env = "KRABKA_PROFILING_NATIVE_FRAME_BLOCKLIST",
64 default_value = "libc,libgcc,pthread,vdso",
65 value_delimiter = ','
66 )]
67 #[default(vec![
68 "libc".to_owned(),
69 "libgcc".to_owned(),
70 "pthread".to_owned(),
71 "vdso".to_owned(),
72 ])]
73 pub profiling_native_frame_blocklist: Vec<String>,
74}
75
76impl ProfilingConfig {
77 pub fn validate(&self) -> Result<(), String> {
83 if self.profiling_cpu_default_duration > self.profiling_cpu_max_duration {
84 return Err("profiling CPU default duration exceeds maximum".to_owned());
85 }
86 if self.profiling_heap_default_duration > self.profiling_heap_max_duration {
87 return Err("profiling heap default duration exceeds maximum".to_owned());
88 }
89 if self.profiling_cpu_max_duration < secs(1) || self.profiling_heap_max_duration < secs(1) {
90 return Err("profiling maximum duration must be at least 1s".to_owned());
91 }
92 Ok(())
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use {
99 super::*,
100 assert2::assert,
101 clap::Parser,
102 krabka_units::{Frequency, convert::FrequencyExt as _},
103 };
104
105 #[derive(Debug, Parser)]
106 struct TestCli {
107 #[command(flatten)]
108 profiling: ProfilingConfig,
109 }
110
111 #[test]
112 fn profiling_config_defaults_and_overrides() {
113 let defaults = TestCli::parse_from(["test"]).profiling;
114 assert!(defaults == ProfilingConfig::default());
115
116 let configured = TestCli::try_parse_from([
117 "test",
118 "--profiling-cpu-default-duration=2s",
119 "--profiling-cpu-max-duration=3s",
120 "--profiling-cpu-sample-frequency=101Hz",
121 "--profiling-heap-default-duration=4s",
122 "--profiling-heap-max-duration=5s",
123 "--profiling-native-frame-blocklist=libc,custom",
124 ])
125 .expect("valid profiling policy")
126 .profiling;
127 assert!(configured.profiling_cpu_default_duration == secs(2));
128 assert!(configured.profiling_cpu_max_duration == secs(3));
129 assert!(
130 configured.profiling_cpu_sample_frequency.frequency() == Frequency::from_per_sec(101.0)
131 );
132 assert!(configured.profiling_heap_default_duration == secs(4));
133 assert!(configured.profiling_heap_max_duration == secs(5));
134 assert!(configured.profiling_native_frame_blocklist == ["libc", "custom"]);
135 assert!(configured.validate().is_ok());
136 }
137
138 #[yare::parameterized(
139 cpu_default_zero = { "--profiling-cpu-default-duration=0s" },
140 cpu_max_negative = { "--profiling-cpu-max-duration=-1s" },
141 cpu_frequency_zero = { "--profiling-cpu-sample-frequency=0Hz" },
142 cpu_frequency_fractional = { "--profiling-cpu-sample-frequency=1.5Hz" },
143 heap_default_zero = { "--profiling-heap-default-duration=0s" },
144 heap_max_negative = { "--profiling-heap-max-duration=-1s" },
145 )]
146 fn profiling_config_rejects_invalid_values(argument: &str) {
147 assert2::assert!(TestCli::try_parse_from(["test", argument]).is_err());
148 }
149
150 #[test]
151 fn profiling_config_rejects_inconsistent_duration_bounds() {
152 let cpu_bounds = TestCli::parse_from([
153 "test",
154 "--profiling-cpu-default-duration=2s",
155 "--profiling-cpu-max-duration=1s",
156 ]);
157 assert!(cpu_bounds.profiling.validate().is_err());
158
159 let heap_bounds = TestCli::parse_from([
160 "test",
161 "--profiling-heap-default-duration=2s",
162 "--profiling-heap-max-duration=1s",
163 ]);
164 assert!(heap_bounds.profiling.validate().is_err());
165 }
166}