extern crate alloc;
use crate::math::ceil;
use alloc::string::String;
pub const DEFAULT_TARGET_TICKS: f64 = 50.0;
pub const DEFAULT_MAX_BATCH_K: usize = 20;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
pub struct PilotDecision {
pub batch_k: usize,
pub batching_enabled: bool,
pub ticks_per_batch: f64,
pub unmeasurable: Option<UnmeasurableInfo>,
pub rationale: String,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
pub struct UnmeasurableInfo {
pub operation_ns: f64,
pub threshold_ns: f64,
pub ticks_per_call: f64,
}
pub fn compute_batch_k(
median_cycles: u64,
cycles_per_ns: f64,
timer_resolution_ns: f64,
target_ticks_per_batch: f64,
max_batch_k: usize,
) -> PilotDecision {
let median_ns = median_cycles as f64 / cycles_per_ns;
let ticks_per_call = median_ns / timer_resolution_ns;
compute_batch_k_from_ticks(
ticks_per_call,
median_ns,
timer_resolution_ns,
target_ticks_per_batch,
max_batch_k,
)
}
pub fn compute_batch_k_from_ticks(
ticks_per_call: f64,
median_ns: f64,
timer_resolution_ns: f64,
target_ticks_per_batch: f64,
max_batch_k: usize,
) -> PilotDecision {
use alloc::format;
if ticks_per_call >= target_ticks_per_batch {
return PilotDecision {
batch_k: 1,
batching_enabled: false,
ticks_per_batch: ticks_per_call,
unmeasurable: None,
rationale: format!(
"no batching needed ({:.1} ticks/call >= {:.0} target)",
ticks_per_call, target_ticks_per_batch
),
};
}
let k_raw = ceil(target_ticks_per_batch / ticks_per_call) as usize;
let k_attempt = k_raw.clamp(1, max_batch_k);
let actual_ticks = ticks_per_call * k_attempt as f64;
if actual_ticks < target_ticks_per_batch {
let threshold_ns = timer_resolution_ns * target_ticks_per_batch / k_attempt as f64;
let rationale = format!(
"UNMEASURABLE: {:.1} ticks/batch < {:.0} minimum even at K={} (op ~{:.2}ns, threshold ~{:.2}ns)",
actual_ticks, target_ticks_per_batch, k_attempt, median_ns, threshold_ns
);
return PilotDecision {
batch_k: 1, batching_enabled: false,
ticks_per_batch: ticks_per_call,
unmeasurable: Some(UnmeasurableInfo {
operation_ns: median_ns,
threshold_ns,
ticks_per_call,
}),
rationale,
};
}
let rationale = format!(
"K={} ({:.1} ticks/batch, {:.2} ticks/call, timer res {:.1}ns)",
k_attempt, actual_ticks, ticks_per_call, timer_resolution_ns
);
PilotDecision {
batch_k: k_attempt,
batching_enabled: k_attempt > 1,
ticks_per_batch: actual_ticks,
unmeasurable: None,
rationale,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_batching_needed() {
let decision = compute_batch_k_from_ticks(
100.0, 1000.0, 10.0, 50.0, 20,
);
assert_eq!(decision.batch_k, 1);
assert!(!decision.batching_enabled);
assert!(decision.unmeasurable.is_none());
}
#[test]
fn test_batching_needed() {
let decision = compute_batch_k_from_ticks(
10.0, 100.0, 10.0, 50.0, 20,
);
assert_eq!(decision.batch_k, 5);
assert!(decision.batching_enabled);
assert!(decision.unmeasurable.is_none());
assert!((decision.ticks_per_batch - 50.0).abs() < 0.01);
}
#[test]
fn test_unmeasurable() {
let decision = compute_batch_k_from_ticks(
0.5, 5.0, 10.0, 50.0, 20, );
assert!(decision.unmeasurable.is_some());
let info = decision.unmeasurable.unwrap();
assert!((info.operation_ns - 5.0).abs() < 0.01);
}
#[test]
fn test_max_batch_k_cap() {
let decision = compute_batch_k_from_ticks(
5.0, 50.0, 10.0, 200.0, 20, );
assert!(decision.unmeasurable.is_some());
}
}