1use std::path::Path;
7
8pub const BENCH_BUILD_PROFILE_ENV: &str = "PI_BENCH_BUILD_PROFILE";
10
11pub const BENCH_ALLOCATOR_ENV: &str = "PI_BENCH_ALLOCATOR";
13
14pub const BINARY_SIZE_RELEASE_BUDGET_MB: f64 = 22.0;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum AllocatorKind {
20 System,
22 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#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct AllocatorSelection {
39 pub requested: String,
41 pub requested_source: &'static str,
43 pub effective: AllocatorKind,
45 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#[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#[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#[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#[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#[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#[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 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 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 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}