Skip to main content

pi/
perf_build.rs

1//! Shared performance-build metadata helpers for benchmark tooling.
2//!
3//! These helpers keep profile and allocator reporting consistent across
4//! benchmark binaries, regression tests, and shell harnesses.
5
6use std::path::Path;
7
8/// Environment variable that overrides benchmark build-profile metadata.
9pub const BENCH_BUILD_PROFILE_ENV: &str = "PI_BENCH_BUILD_PROFILE";
10
11/// Environment variable that requests an allocator label for benchmark runs.
12pub const BENCH_ALLOCATOR_ENV: &str = "PI_BENCH_ALLOCATOR";
13
14/// Release binary-size budget (MB) shared by perf regression and budget gates.
15pub const BINARY_SIZE_RELEASE_BUDGET_MB: f64 = 22.0;
16
17/// Effective allocator compiled into the current binary.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum AllocatorKind {
20    /// The platform/system allocator.
21    System,
22    /// `tikv-jemallocator` via the `jemalloc` Cargo feature.
23    Jemalloc,
24}
25
26impl AllocatorKind {
27    #[must_use]
28    pub const fn as_str(self) -> &'static str {
29        match self {
30            Self::System => "system",
31            Self::Jemalloc => "jemalloc",
32        }
33    }
34}
35
36/// Benchmark allocator selection metadata.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct AllocatorSelection {
39    /// Requested allocator token (normalized).
40    pub requested: String,
41    /// Source of `requested` (`env` or `default`).
42    pub requested_source: &'static str,
43    /// Effective allocator compiled into this binary.
44    pub effective: AllocatorKind,
45    /// Optional explanation when request/effective do not match.
46    pub fallback_reason: Option<String>,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50enum RequestedAllocator {
51    Auto,
52    System,
53    Jemalloc,
54    Unknown,
55}
56
57/// Returns the allocator compiled into the current binary.
58#[must_use]
59pub const fn compiled_allocator() -> AllocatorKind {
60    if cfg!(all(feature = "jemalloc", not(target_env = "msvc"))) {
61        AllocatorKind::Jemalloc
62    } else {
63        AllocatorKind::System
64    }
65}
66
67/// Resolves benchmark allocator metadata from [`BENCH_ALLOCATOR_ENV`].
68#[must_use]
69pub fn resolve_bench_allocator() -> AllocatorSelection {
70    let raw_value = std::env::var(BENCH_ALLOCATOR_ENV).ok();
71    resolve_bench_allocator_from(raw_value.as_deref())
72}
73
74/// Resolves benchmark allocator metadata from an optional raw token.
75#[must_use]
76pub fn resolve_bench_allocator_from(raw_value: Option<&str>) -> AllocatorSelection {
77    let requested_raw = raw_value
78        .map(str::trim)
79        .filter(|value| !value.is_empty())
80        .map_or_else(|| "auto".to_string(), str::to_ascii_lowercase);
81    let requested_source = if raw_value
82        .map(str::trim)
83        .is_some_and(|value| !value.is_empty())
84    {
85        "env"
86    } else {
87        "default"
88    };
89
90    let requested_kind = match requested_raw.as_str() {
91        "auto" | "default" => RequestedAllocator::Auto,
92        "system" | "native" => RequestedAllocator::System,
93        "jemalloc" | "je" => RequestedAllocator::Jemalloc,
94        _ => RequestedAllocator::Unknown,
95    };
96
97    let effective = compiled_allocator();
98    let fallback_reason = match requested_kind {
99        RequestedAllocator::System if effective == AllocatorKind::Jemalloc => {
100            Some("system requested but binary was built with --features jemalloc".to_string())
101        }
102        RequestedAllocator::Jemalloc if effective != AllocatorKind::Jemalloc => {
103            Some("jemalloc requested but binary was built without --features jemalloc".to_string())
104        }
105        RequestedAllocator::Unknown => Some(format!(
106            "unknown allocator '{requested_raw}'; using compiled allocator '{}'",
107            effective.as_str()
108        )),
109        RequestedAllocator::Auto | RequestedAllocator::System | RequestedAllocator::Jemalloc => {
110            None
111        }
112    };
113
114    let requested = match requested_kind {
115        RequestedAllocator::System => "system".to_string(),
116        RequestedAllocator::Jemalloc => "jemalloc".to_string(),
117        RequestedAllocator::Auto => "auto".to_string(),
118        RequestedAllocator::Unknown => requested_raw,
119    };
120
121    AllocatorSelection {
122        requested,
123        requested_source,
124        effective,
125        fallback_reason,
126    }
127}
128
129/// Detects the benchmark build profile for reporting.
130#[must_use]
131pub fn detect_build_profile() -> String {
132    let env_profile = std::env::var(BENCH_BUILD_PROFILE_ENV).ok();
133    let current_exe = std::env::current_exe().ok();
134    detect_build_profile_from(
135        env_profile.as_deref(),
136        current_exe.as_deref(),
137        cfg!(debug_assertions),
138    )
139}
140
141/// Detects build profile with injectable dependencies for tests.
142#[must_use]
143pub fn detect_build_profile_from(
144    env_profile: Option<&str>,
145    current_exe: Option<&Path>,
146    debug_assertions: bool,
147) -> String {
148    if let Some(value) = env_profile.map(str::trim).filter(|value| !value.is_empty()) {
149        return value.to_string();
150    }
151
152    if let Some(profile) = current_exe.and_then(profile_from_target_path) {
153        return profile;
154    }
155
156    if debug_assertions {
157        "debug".to_string()
158    } else {
159        "release".to_string()
160    }
161}
162
163/// Attempts to derive Cargo profile from a binary path under `target/`.
164#[must_use]
165pub fn profile_from_target_path(path: &Path) -> Option<String> {
166    let components: Vec<String> = path
167        .components()
168        .filter_map(|component| match component {
169            std::path::Component::Normal(part) => Some(part.to_string_lossy().into_owned()),
170            _ => None,
171        })
172        .collect();
173
174    let target_idx = components
175        .iter()
176        .rposition(|component| component == "target")?;
177    let tail = components.get(target_idx + 1..)?;
178    if tail.len() < 2 {
179        return None;
180    }
181
182    let profile_idx = if tail.len() >= 3 && tail[tail.len() - 2] == "deps" {
183        tail.len().checked_sub(3)?
184    } else {
185        tail.len().checked_sub(2)?
186    };
187
188    let candidate = tail.get(profile_idx)?.trim();
189    if candidate.is_empty() {
190        return None;
191    }
192
193    Some(candidate.to_string())
194}
195
196#[cfg(test)]
197mod tests {
198    use super::{
199        AllocatorKind, BENCH_ALLOCATOR_ENV, detect_build_profile_from, profile_from_target_path,
200        resolve_bench_allocator_from,
201    };
202    use std::path::Path;
203
204    #[test]
205    fn detect_build_profile_prefers_env_override() {
206        let profile = detect_build_profile_from(Some("perf"), None, true);
207        assert_eq!(profile, "perf");
208    }
209
210    #[test]
211    fn detect_build_profile_from_target_path_detects_profile() {
212        let path = Path::new("/tmp/repo/target/perf/pijs_workload");
213        let profile = detect_build_profile_from(None, Some(path), true);
214        assert_eq!(profile, "perf");
215    }
216
217    #[test]
218    fn detect_build_profile_falls_back_to_debug_or_release() {
219        assert_eq!(detect_build_profile_from(None, None, true), "debug");
220        assert_eq!(detect_build_profile_from(None, None, false), "release");
221    }
222
223    #[test]
224    fn profile_from_target_path_detects_release_deps_binary() {
225        let path = Path::new("/tmp/repo/target/release/deps/pijs_workload-abc123");
226        assert_eq!(profile_from_target_path(path).as_deref(), Some("release"));
227    }
228
229    #[test]
230    fn profile_from_target_path_returns_none_outside_target() {
231        let path = Path::new("/tmp/repo/bin/pijs_workload");
232        assert_eq!(profile_from_target_path(path), None);
233    }
234
235    #[test]
236    fn allocator_unknown_token_fails_closed_to_compiled_allocator() {
237        let resolved = resolve_bench_allocator_from(Some("weird"));
238        assert_eq!(resolved.requested, "weird");
239        assert_eq!(resolved.requested_source, "env");
240        assert_eq!(resolved.effective, super::compiled_allocator());
241        assert!(resolved.fallback_reason.is_some());
242    }
243
244    #[test]
245    fn allocator_auto_defaults_to_compiled_allocator() {
246        let resolved = resolve_bench_allocator_from(None);
247        assert_eq!(resolved.requested, "auto");
248        assert_eq!(resolved.requested_source, "default");
249        assert_eq!(resolved.effective, super::compiled_allocator());
250        assert!(resolved.fallback_reason.is_none());
251    }
252
253    #[test]
254    fn allocator_jemalloc_request_reports_compile_time_mismatch() {
255        let resolved = resolve_bench_allocator_from(Some("jemalloc"));
256        assert_eq!(resolved.requested, "jemalloc");
257        if cfg!(feature = "jemalloc") {
258            assert_eq!(resolved.effective, AllocatorKind::Jemalloc);
259            assert!(resolved.fallback_reason.is_none());
260        } else {
261            assert_eq!(resolved.effective, AllocatorKind::System);
262            assert!(
263                resolved.fallback_reason.is_some(),
264                "{BENCH_ALLOCATOR_ENV}=jemalloc should report fallback without feature"
265            );
266        }
267    }
268
269    #[test]
270    fn allocator_system_request_reports_compile_time_mismatch() {
271        let resolved = resolve_bench_allocator_from(Some("system"));
272        assert_eq!(resolved.requested, "system");
273        if cfg!(feature = "jemalloc") {
274            assert_eq!(resolved.effective, AllocatorKind::Jemalloc);
275            assert!(resolved.fallback_reason.is_some());
276        } else {
277            assert_eq!(resolved.effective, AllocatorKind::System);
278            assert!(resolved.fallback_reason.is_none());
279        }
280    }
281
282    // ── Property tests ──
283
284    mod proptest_perf_build {
285        use super::*;
286        use proptest::prelude::*;
287
288        proptest! {
289            #[test]
290            fn resolve_allocator_effective_is_always_compiled(
291                raw_value in prop::option::of("[a-z]{0,20}"),
292            ) {
293                let resolved = resolve_bench_allocator_from(raw_value.as_deref());
294                assert!(
295                    resolved.effective == super::super::compiled_allocator(),
296                    "effective allocator must always be compiled allocator"
297                );
298            }
299
300            #[test]
301            fn resolve_allocator_known_tokens_have_no_unknown_fallback(
302                token in prop::sample::select(vec![
303                    "auto", "default", "system", "native", "jemalloc", "je",
304                ]),
305            ) {
306                let resolved = resolve_bench_allocator_from(Some(token));
307                // Known tokens never produce "unknown allocator" fallback
308                if let Some(reason) = &resolved.fallback_reason {
309                    assert!(
310                        !reason.starts_with("unknown allocator"),
311                        "known token '{token}' should not produce unknown fallback: {reason}"
312                    );
313                }
314            }
315
316            #[test]
317            fn resolve_allocator_unknown_tokens_always_have_fallback(
318                token in "[a-z]{3,10}".prop_filter(
319                    "must not be known",
320                    |t| !matches!(t.as_str(), "auto" | "default" | "system" | "native" | "jemalloc" | "je"),
321                ),
322            ) {
323                let resolved = resolve_bench_allocator_from(Some(&token));
324                assert!(
325                    resolved.fallback_reason.is_some(),
326                    "unknown token '{token}' must produce a fallback reason"
327                );
328                assert!(
329                    resolved.requested == token,
330                    "unknown token should be passed through as-is"
331                );
332            }
333
334            #[test]
335            fn resolve_allocator_empty_or_whitespace_defaults_to_auto(
336                value in prop::sample::select(vec!["", " ", "  ", "\t"]),
337            ) {
338                let resolved = resolve_bench_allocator_from(Some(value));
339                assert!(
340                    resolved.requested == "auto",
341                    "empty/whitespace should default to 'auto', got '{}'",
342                    resolved.requested,
343                );
344                assert!(resolved.requested_source == "default");
345            }
346
347            #[test]
348            fn resolve_allocator_none_defaults_to_auto(_dummy in Just(())) {
349                let resolved = resolve_bench_allocator_from(None);
350                assert!(resolved.requested == "auto");
351                assert!(resolved.requested_source == "default");
352                assert!(resolved.fallback_reason.is_none());
353            }
354
355            #[test]
356            fn profile_from_target_path_requires_target_dir(
357                dir in "[a-z]{1,10}",
358                binary in "[a-z_]{1,10}",
359            ) {
360                // Paths without "target" component always return None
361                let path_str = format!("/{dir}/{binary}");
362                let path = Path::new(&path_str);
363                assert!(
364                    profile_from_target_path(path).is_none(),
365                    "path without 'target' should return None: {path_str}"
366                );
367            }
368
369            #[test]
370            fn profile_from_target_path_extracts_profile(
371                profile in "[a-z]{3,10}",
372                binary in "[a-z_]{3,10}",
373            ) {
374                let path_str = format!("/repo/target/{profile}/{binary}");
375                let path = Path::new(&path_str);
376                let result = profile_from_target_path(path);
377                assert!(
378                    result == Some(profile.clone()),
379                    "expected Some(\"{profile}\"), got {result:?} for path {path_str}"
380                );
381            }
382
383            #[test]
384            fn detect_build_profile_env_overrides_all(
385                env_val in "[a-z]{1,15}",
386            ) {
387                let result = detect_build_profile_from(
388                    Some(&env_val),
389                    Some(Path::new("/target/release/bin")),
390                    true,
391                );
392                assert!(
393                    result == env_val,
394                    "env override should take priority: expected '{env_val}', got '{result}'"
395                );
396            }
397
398            #[test]
399            fn allocator_kind_as_str_is_stable(
400                kind in prop::sample::select(vec![
401                    AllocatorKind::System,
402                    AllocatorKind::Jemalloc,
403                ]),
404            ) {
405                let s1 = kind.as_str();
406                let s2 = kind.as_str();
407                assert!(s1 == s2, "as_str must be deterministic");
408                assert!(
409                    s1 == "system" || s1 == "jemalloc",
410                    "as_str must return known value: {s1}"
411                );
412            }
413        }
414    }
415}