Skip to main content

krabka_telemetry/profiling/
config.rs

1//! The profiling policy: the `clap`-flattened `ProfilingConfig`, the validated
2//! sampling frequency it carries, and the `ProfilingError` a bad policy raises.
3//!
4//! The policy lives apart from the routes and the admin server so that a
5//! service can parse and validate its profiling settings without building a
6//! router or binding a port.
7
8use {
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/// Profiling configuration or admin-server failure.
18#[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/// A positive, finite, whole-Hz sampling frequency accepted by `pprof`, whose
27/// frequency input is a signed `int`.
28#[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/// Process-local CPU and heap profiling policy.
41#[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    /// Validate related profiling bounds.
78    ///
79    /// # Errors
80    /// Returns an error when a default exceeds its maximum. Returns an error
81    /// when a maximum is below the compatible one-second request floor.
82    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}