use super::tier::{
classify_gpu_tier, gpu_min_bytes_for_tier, gpu_pattern_breakeven_for_tier, gpu_routing_profile,
gpu_solo_bytes_for_tier,
};
use super::{HardwareCaps, ScanBackend};
thread_local! {
pub(crate) static TEST_BACKEND_OVERRIDE: std::cell::RefCell<Option<Option<ScanBackend>>> = const { std::cell::RefCell::new(None) };
}
#[cfg(test)]
pub(crate) fn set_test_backend_override(val: Option<ScanBackend>) {
TEST_BACKEND_OVERRIDE.with(|cell| {
*cell.borrow_mut() = Some(val);
});
}
#[cfg(test)]
pub(crate) fn clear_test_backend_override() {
TEST_BACKEND_OVERRIDE.with(|cell| {
*cell.borrow_mut() = None;
});
}
#[must_use]
pub(crate) fn cpu_tier_backend(caps: &HardwareCaps) -> ScanBackend {
if caps.hyperscan_available {
ScanBackend::SimdCpu
} else {
ScanBackend::CpuFallback
}
}
#[derive(Debug, Clone, Copy)]
struct BackendWorkload {
bytes: u64,
pattern_count: usize,
large_chunk_bytes: Option<u64>,
}
impl BackendWorkload {
fn file(bytes: u64, pattern_count: usize) -> Self {
Self {
bytes,
pattern_count,
large_chunk_bytes: None,
}
}
#[cfg(test)]
fn batch(bytes: u64, pattern_count: usize, large_chunk_bytes: u64) -> Self {
Self {
bytes,
pattern_count,
large_chunk_bytes: Some(large_chunk_bytes),
}
}
fn gpu_dominates_dispatch_cost(self) -> bool {
match self.large_chunk_bytes {
None => true,
Some(large_chunk_bytes) => {
large_chunk_bytes > 0 && large_chunk_bytes.saturating_mul(2) >= self.bytes
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendRoutingReason {
TestOverride,
GpuDisabledByPolicy,
GpuProbeMiss,
GpuSoftwareRenderer,
GpuBatchNotDominant,
GpuThresholdNotMet,
GpuSelected,
}
impl BackendRoutingReason {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::TestOverride => "test_override",
Self::GpuDisabledByPolicy => "gpu_disabled_by_policy",
Self::GpuProbeMiss => "gpu_probe_miss",
Self::GpuSoftwareRenderer => "gpu_software_renderer",
Self::GpuBatchNotDominant => "gpu_batch_not_dominant",
Self::GpuThresholdNotMet => "gpu_threshold_not_met",
Self::GpuSelected => "gpu_selected",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BackendRoutingVerdict {
pub backend: ScanBackend,
pub reason: BackendRoutingReason,
pub workload_bytes: u64,
pub pattern_count: usize,
pub large_chunk_bytes: Option<u64>,
pub gpu_available: bool,
pub gpu_is_software: bool,
pub gpu_tier: &'static str,
pub gpu_min_bytes: u64,
pub gpu_solo_bytes: u64,
pub gpu_pattern_breakeven: usize,
}
impl BackendRoutingVerdict {
fn new(
caps: &HardwareCaps,
workload: BackendWorkload,
backend: ScanBackend,
reason: BackendRoutingReason,
) -> Self {
let profile = gpu_routing_profile(caps.gpu_name.as_deref());
Self {
backend,
reason,
workload_bytes: workload.bytes,
pattern_count: workload.pattern_count,
large_chunk_bytes: workload.large_chunk_bytes,
gpu_available: caps.gpu_available,
gpu_is_software: caps.gpu_is_software,
gpu_tier: profile.tier,
gpu_min_bytes: profile.min_bytes,
gpu_solo_bytes: profile.solo_bytes,
gpu_pattern_breakeven: profile.pattern_breakeven,
}
}
#[must_use]
pub fn reason_detail(self) -> String {
match self.reason {
BackendRoutingReason::TestOverride => "forced by test override".to_string(),
BackendRoutingReason::GpuDisabledByPolicy => {
"GPU disabled by resolved runtime policy".to_string()
}
BackendRoutingReason::GpuProbeMiss => {
"no usable GPU adapter reported by hardware probe".to_string()
}
BackendRoutingReason::GpuSoftwareRenderer => {
"GPU adapter is a software renderer and is slower than CPU/SIMD".to_string()
}
BackendRoutingReason::GpuBatchNotDominant => {
let Some(large) = self.large_chunk_bytes else {
return format!(
"large-chunk byte share is unavailable for workload bytes ({})",
self.workload_bytes
);
};
format!(
"large-chunk bytes ({large}) do not dominate workload bytes ({})",
self.workload_bytes
)
}
BackendRoutingReason::GpuThresholdNotMet => format!(
"GPU thresholds not met for tier {}: bytes={} min={} solo={} patterns={} pattern_floor={}",
self.gpu_tier,
self.workload_bytes,
self.gpu_min_bytes,
self.gpu_solo_bytes,
self.pattern_count,
self.gpu_pattern_breakeven
),
BackendRoutingReason::GpuSelected => format!(
"GPU thresholds met for tier {}: bytes={} min={} solo={} patterns={} pattern_floor={}",
self.gpu_tier,
self.workload_bytes,
self.gpu_min_bytes,
self.gpu_solo_bytes,
self.pattern_count,
self.gpu_pattern_breakeven
),
}
}
}
fn select_backend_for_workload(
caps: &HardwareCaps,
workload: BackendWorkload,
) -> BackendRoutingVerdict {
if let Some(forced) = test_backend_override() {
return BackendRoutingVerdict::new(
caps,
workload,
forced,
BackendRoutingReason::TestOverride,
);
}
let cpu_backend = cpu_tier_backend(caps);
if crate::gpu::gpu_disabled_by_policy() {
return BackendRoutingVerdict::new(
caps,
workload,
cpu_backend,
BackendRoutingReason::GpuDisabledByPolicy,
);
}
if !caps.gpu_available {
return BackendRoutingVerdict::new(
caps,
workload,
cpu_backend,
BackendRoutingReason::GpuProbeMiss,
);
}
if caps.gpu_is_software {
return BackendRoutingVerdict::new(
caps,
workload,
cpu_backend,
BackendRoutingReason::GpuSoftwareRenderer,
);
}
if !workload.gpu_dominates_dispatch_cost() {
return BackendRoutingVerdict::new(
caps,
workload,
cpu_backend,
BackendRoutingReason::GpuBatchNotDominant,
);
}
if gpu_could_engage(caps, workload.bytes, workload.pattern_count) {
return BackendRoutingVerdict::new(
caps,
workload,
ScanBackend::GpuWgpu,
BackendRoutingReason::GpuSelected,
);
}
BackendRoutingVerdict::new(
caps,
workload,
cpu_backend,
BackendRoutingReason::GpuThresholdNotMet,
)
}
#[must_use]
pub fn select_backend(
caps: &HardwareCaps,
workload_bytes: u64,
pattern_count: usize,
) -> ScanBackend {
select_backend_verdict(caps, workload_bytes, pattern_count).backend
}
#[must_use]
pub fn select_backend_verdict(
caps: &HardwareCaps,
workload_bytes: u64,
pattern_count: usize,
) -> BackendRoutingVerdict {
select_backend_for_workload(caps, BackendWorkload::file(workload_bytes, pattern_count))
}
#[must_use]
#[cfg(test)]
pub(crate) fn select_backend_for_batch(
caps: &HardwareCaps,
workload_bytes: u64,
pattern_count: usize,
large_chunk_bytes: u64,
) -> ScanBackend {
select_backend_for_batch_verdict(caps, workload_bytes, pattern_count, large_chunk_bytes).backend
}
#[must_use]
#[cfg(test)]
pub(crate) fn select_backend_for_batch_verdict(
caps: &HardwareCaps,
workload_bytes: u64,
pattern_count: usize,
large_chunk_bytes: u64,
) -> BackendRoutingVerdict {
select_backend_for_workload(
caps,
BackendWorkload::batch(workload_bytes, pattern_count, large_chunk_bytes),
)
}
#[must_use]
pub fn gpu_could_engage(caps: &HardwareCaps, workload_bytes: u64, pattern_count: usize) -> bool {
if !caps.gpu_available || caps.gpu_is_software {
return false;
}
let tier = classify_gpu_tier(caps.gpu_name.as_deref());
let solo = gpu_solo_bytes_for_tier(tier);
let min = gpu_min_bytes_for_tier(tier);
let pattern_floor = gpu_pattern_breakeven_for_tier(tier);
workload_bytes >= solo || (workload_bytes >= min && pattern_count >= pattern_floor)
}
#[cfg(test)]
pub(crate) fn forced_backend_override_for_test() -> Option<ScanBackend> {
test_backend_override()
}
pub(super) fn test_backend_override() -> Option<ScanBackend> {
TEST_BACKEND_OVERRIDE.with(|cell| *cell.borrow()).flatten()
}
pub const BACKEND_OVERRIDE_VALUES: [&str; 9] = [
"auto",
"gpu-cuda",
"gpu-cuda-region-presence",
"gpu-wgpu",
"gpu-wgpu-region-presence",
"simd",
"simd-regex",
"cpu",
"cpu-fallback",
];
pub fn parse_backend_str(raw: &str) -> Option<ScanBackend> {
match raw.trim().to_ascii_lowercase().as_str() {
"gpu-cuda" | "gpu-cuda-region-presence" => Some(ScanBackend::GpuCuda),
"gpu-wgpu" | "gpu-wgpu-region-presence" => Some(ScanBackend::GpuWgpu),
"simd" | "simd-regex" => Some(ScanBackend::SimdCpu),
"cpu" | "cpu-fallback" => Some(ScanBackend::CpuFallback),
_ => None,
}
}