keyhog-scanner 0.5.85

keyhog-scanner: high-performance SIMD-accelerated secret detection engine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
//! Hardware capability probing with once-cached results.
//!
//! Detects CPU features (AVX-512, AVX2, NEON), GPU compute (wgpu/Vulkan),
//! Hyperscan availability, io_uring support, memory, and core counts.
//! All detection is done once at startup and cached for the process
//! lifetime.
//!
//! Split into focused submodules by hardware-probe responsibility:
//!
//!   * `thresholds` - GPU routing crossover constants consumed through
//!     the public tier lookup functions.
//!   * [`tier`] - GPU adapter classification + tier threshold profiles.
//!   * [`select`] - [`select_backend`] routing logic + env-override
//!     parsing.
//!   * [`banner`] - `startup_banner` formatter for the CLI header.
//!   * [`platform`] - per-OS detection of physical cores, memory,
//!     and io_uring availability.

use std::sync::OnceLock;

mod banner;
mod host_class;
pub(crate) mod platform;
pub(crate) mod select;
mod tier;

pub(crate) mod thresholds;

pub use banner::startup_banner;
pub use host_class::HostClass;
pub use select::{
    gpu_could_engage, parse_backend_str, select_backend, select_backend_verdict,
    BackendRoutingReason, BackendRoutingVerdict, BACKEND_OVERRIDE_VALUES,
};
pub use tier::{gpu_routing_profile, gpu_routing_profiles, GpuRoutingProfile};

/// Scan execution backend selected for a given workload.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ScanBackend {
    /// GPU region-presence phase 1 through VYRE's CUDA driver.
    GpuCuda,
    /// GPU region-presence phase 1 through VYRE's native Metal driver.
    GpuMetal,
    /// GPU region-presence phase 1 through VYRE's WGPU driver.
    GpuWgpu,
    /// Hyperscan NFA multi-pattern matching + SIMD prefilter.
    /// This is the primary high-throughput path on all platforms.
    SimdCpu,
    /// Pure CPU: Aho-Corasick literals plus Rust regex extraction. No
    /// Hyperscan or GPU execution.
    CpuFallback,
}

impl ScanBackend {
    /// Stable label for logs and CLI startup banner.
    #[must_use]
    pub fn label(self) -> &'static str {
        match self {
            Self::GpuCuda => "gpu-cuda-region-presence",
            Self::GpuMetal => "gpu-metal-region-presence",
            Self::GpuWgpu => "gpu-wgpu-region-presence",
            Self::SimdCpu => "simd-regex",
            Self::CpuFallback => "cpu-fallback",
        }
    }

    /// Whether this route executes on a physical GPU.
    #[must_use]
    pub const fn is_gpu(self) -> bool {
        matches!(self, Self::GpuCuda | Self::GpuMetal | Self::GpuWgpu)
    }
}

/// True when this build compiled a scan backend beyond the always-present scalar
/// [`ScanBackend::CpuFallback`]. Hyperscan (`simd`) and/or the GPU stack (`gpu`).
///
/// When this is `false` there is no routing choice: every scan can only run
/// `CpuFallback`, so autoroute calibration is neither needed nor possible and the
/// caller resolves the lone backend directly instead of failing closed. This is a
/// COMPILE-time fact, owned here in the scanner where the `simd`/`gpu` feature gates
/// actually live: a consumer's own feature flags can diverge (e.g. the CLI's
/// `ci-lean` enables `keyhog-scanner/simd` without the CLI's own `simd` feature), so
/// consumers MUST ask the scanner rather than checking their own `cfg!`.
#[must_use]
pub const fn multiple_backends_compiled() -> bool {
    simd_backend_compiled() || gpu_backend_compiled()
}

/// True when this scanner crate was compiled with the Hyperscan/SIMD backend.
/// Consumers must query this owner instead of their own Cargo feature namespace:
/// workspace feature unification can enable a dependency backend without
/// enabling a same-named feature on the consuming crate.
#[must_use]
pub const fn simd_backend_compiled() -> bool {
    cfg!(feature = "simd")
}

/// Runtime identity of the dynamically linked Hyperscan/Vectorscan library.
///
/// Autoroute persistence must bind SIMD measurements to the library that
/// actually executed them, not only to the fact that SIMD support compiled.
#[must_use]
pub fn hyperscan_runtime_identity() -> Option<String> {
    #[cfg(feature = "simd")]
    {
        Some(hyperscan::version().to_string())
    }
    #[cfg(not(feature = "simd"))]
    {
        None
    }
}

/// True when this scanner crate was compiled with the GPU backend stack.
/// Autoroute host identity and persisted build evidence use this dependency-owned
/// fact so a GPU-selected calibration can never be stored without GPU identity.
#[must_use]
pub const fn gpu_backend_compiled() -> bool {
    cfg!(feature = "gpu")
}

/// Canonical explanation string when GPU backend is not compiled into this binary.
#[must_use]
pub fn uncompiled_gpu_backend_explanation() -> &'static str {
    if !multiple_backends_compiled() {
        "compiled without GPU backend / single compiled backend"
    } else {
        "compiled without GPU backend"
    }
}

/// Canonical formatted GPU status label across CLI diagnostics (doctor, backend report, version).
#[must_use]
pub fn format_gpu_status(caps: &HardwareCaps) -> String {
    if caps.gpu_available {
        let name = caps.gpu_name.as_deref().unwrap_or("yes"); // LAW10: display-only label for an unnamed adapter
        if caps.gpu_is_software {
            format!("{name} (software renderer: disabled)")
        } else {
            name.to_string()
        }
    } else if let Some(name) = caps.gpu_name.as_deref() {
        if caps.gpu_is_software {
            format!("{name} (software renderer: disabled)")
        } else if !gpu_backend_compiled() {
            format!("{name} ({})", uncompiled_gpu_backend_explanation())
        } else {
            format!("{name} (runtime unavailable)")
        }
    } else if !gpu_backend_compiled() {
        if !multiple_backends_compiled() {
            "not detected (compiled without GPU backend / single compiled backend)".to_string()
        } else {
            "not detected (binary built without --features gpu)".to_string()
        }
    } else {
        "not detected".to_string()
    }
}

/// Single owner of the SIMD-tier label precedence chain.
///
/// The label reported by the startup banner, `keyhog backend`, `keyhog doctor`,
/// and the backend store must always agree, so the `"AVX-512" > "AVX2" > "NEON"
/// > "scalar"` precedence lives in exactly ONE place. Callers pass the three
/// probed CPU-feature booleans (typically `caps.has_avx512`, `caps.has_avx2`,
/// `caps.has_neon`) and receive the highest-priority label that is available.
///
/// Precedence is strict and independent of the lower bits: if `has_avx512` is
/// true the result is `"AVX-512"` regardless of the other two, and so on down
/// to `"scalar"` when none are present.
#[must_use]
pub const fn simd_label(has_avx512: bool, has_avx2: bool, has_neon: bool) -> &'static str {
    if has_avx512 {
        "AVX-512"
    } else if has_avx2 {
        "AVX2"
    } else if has_neon {
        "NEON"
    } else {
        "scalar"
    }
}

/// Hardware capabilities detected at startup.
#[derive(Debug, Clone)]
pub struct HardwareCaps {
    pub physical_cores: usize,
    pub logical_cores: usize,
    pub has_avx2: bool,
    pub has_avx512: bool,
    pub has_neon: bool,
    pub gpu_available: bool,
    pub gpu_name: Option<String>,
    /// WGPU's portable `max_buffer_size`, expressed in MiB and capped by
    /// KeyHog. The historical field name does not mean physical VRAM; WGPU can
    /// expose a virtual buffer limit larger than installed device memory.
    pub gpu_vram_mb: Option<u64>,
    pub gpu_runtime_identity: Option<String>,
    /// True when the GPU is a software renderer (llvmpipe/lavapipe) - always slower than CPU.
    pub gpu_is_software: bool,
    pub total_memory_mb: Option<u64>,
    pub io_uring_available: bool,
    /// True when the `simd` feature is compiled in AND Hyperscan initialized.
    pub hyperscan_available: bool,
    /// Runtime identity of the Hyperscan/Vectorscan library that produced these
    /// capabilities. `None` when `hyperscan_available` is false or the identity
    /// has not been probed yet.
    pub hyperscan_runtime_identity: Option<String>,
}

static HW_PROBE: OnceLock<HardwareCaps> = OnceLock::new();

/// Probe hardware once and cache the result.
pub fn probe_hardware() -> &'static HardwareCaps {
    HW_PROBE.get_or_init(|| detect_hardware(true))
}

/// Probe host CPU and memory capabilities without loading a GPU runtime.
///
/// Explicit CPU and SIMD routes do not admit a GPU peer, so initializing WGPU
/// or a native driver would only inflate startup latency and resident memory.
#[must_use]
pub fn probe_host_hardware() -> HardwareCaps {
    detect_hardware(false)
}

fn detect_hardware(include_gpu: bool) -> HardwareCaps {
    let logical_cores = keyhog_profile::logical_cpu_count();
    let physical_cores = platform::physical_core_count().unwrap_or(logical_cores); // LAW10: host/OS hardware probe parse failure => None/conservative default; perf-only, recall-irrelevant

    #[cfg(target_arch = "x86_64")]
    let (has_avx2, has_avx512, has_neon) = (
        std::arch::is_x86_feature_detected!("avx2"),
        std::arch::is_x86_feature_detected!("avx512f"),
        false,
    );
    #[cfg(target_arch = "aarch64")]
    let (has_avx2, has_avx512, has_neon) = (false, false, true);
    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
    let (has_avx2, has_avx512, has_neon) = (false, false, false);

    let (gpu_available, gpu_name, gpu_vram_mb, gpu_runtime_identity, gpu_is_software) =
        if include_gpu {
            let gpu_probe = crate::gpu::gpu_probe();
            (
                gpu_probe.available,
                gpu_probe.name,
                gpu_probe.buffer_limit_mb,
                gpu_probe.runtime_identity,
                gpu_probe.is_software,
            )
        } else {
            (false, None, None, None, false)
        };
    if gpu_is_software {
        tracing::warn!(
            gpu = ?gpu_name,
            "Software GPU detected: GPU scanning disabled (slower than CPU)"
        );
    }

    let hyperscan_available = cfg!(feature = "simd");
    let hyperscan_runtime_identity = hyperscan_available
        .then(hyperscan_runtime_identity)
        .flatten();
    let total_memory_mb = platform::detect_total_memory_mb();
    let io_uring_available = platform::detect_io_uring();

    let caps = HardwareCaps {
        physical_cores,
        logical_cores,
        has_avx2,
        has_avx512,
        has_neon,
        gpu_available,
        gpu_name: gpu_name.clone(),
        gpu_vram_mb,
        gpu_runtime_identity,
        gpu_is_software,
        total_memory_mb,
        io_uring_available,
        hyperscan_available,
        hyperscan_runtime_identity,
    };

    tracing::info!(
        physical_cores,
        logical_cores,
        gpu_available,
        gpu_name = ?gpu_name,
        has_avx512 = caps.has_avx512,
        has_avx2 = caps.has_avx2,
        has_neon = caps.has_neon,
        hyperscan = hyperscan_available,
        io_uring = io_uring_available,
        "hardware probe complete"
    );

    caps
}

#[cfg(test)]
#[doc(hidden)]
pub mod testing {
    pub use super::{
        gpu_could_engage, parse_backend_str, probe_hardware, select_backend,
        select_backend_verdict, startup_banner, BackendRoutingReason, BackendRoutingVerdict,
        HardwareCaps, ScanBackend,
    };

    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum GpuTier {
        High,
        Mid,
        Low,
    }

    fn from_inner(tier: super::tier::GpuTier) -> GpuTier {
        match tier {
            super::tier::GpuTier::High => GpuTier::High,
            super::tier::GpuTier::Mid => GpuTier::Mid,
            super::tier::GpuTier::Low => GpuTier::Low,
        }
    }

    fn to_inner(tier: GpuTier) -> super::tier::GpuTier {
        match tier {
            GpuTier::High => super::tier::GpuTier::High,
            GpuTier::Mid => super::tier::GpuTier::Mid,
            GpuTier::Low => super::tier::GpuTier::Low,
        }
    }

    /// Select the CPU scan backend (SIMD/scalar tier) for the given hardware
    /// capabilities, ignoring any GPU. Delegates to [`super::select`].
    pub fn cpu_tier_backend(caps: &HardwareCaps) -> ScanBackend {
        super::select::cpu_tier_backend(caps)
    }

    /// Classify a GPU adapter name into a routing [`GpuTier`] (High/Mid/Low).
    /// `None` (no adapter) classifies to the lowest tier.
    pub fn classify_gpu_tier(adapter_name: Option<&str>) -> GpuTier {
        from_inner(super::tier::classify_gpu_tier(adapter_name))
    }

    /// Minimum workload size (bytes) at which the GPU backend is allowed to
    /// engage at all for this tier (below it, CPU always wins).
    pub fn gpu_min_bytes_for_tier(tier: GpuTier) -> u64 {
        super::tier::gpu_min_bytes_for_tier(to_inner(tier))
    }

    /// Minimum workload size (bytes) at which the GPU runs *solo* (no CPU
    /// co-scan) for this tier.
    pub fn gpu_solo_bytes_for_tier(tier: GpuTier) -> u64 {
        super::tier::gpu_solo_bytes_for_tier(to_inner(tier))
    }

    /// Pattern-count break-even above which GPU scanning beats CPU at this tier.
    pub fn gpu_pattern_breakeven_for_tier(tier: GpuTier) -> usize {
        super::tier::gpu_pattern_breakeven_for_tier(to_inner(tier))
    }

    /// Choose the scan backend for a batch from hardware caps, total workload
    /// size, pattern count, and the largest single-chunk size.
    pub fn select_backend_for_batch(
        caps: &HardwareCaps,
        workload_bytes: u64,
        pattern_count: usize,
        large_chunk_bytes: u64,
    ) -> ScanBackend {
        super::select::select_backend_for_batch(
            caps,
            workload_bytes,
            pattern_count,
            large_chunk_bytes,
        )
    }

    /// Like [`select_backend_for_batch`] but returns the full
    /// [`BackendRoutingVerdict`] (the chosen backend plus the inputs and reason
    /// behind the decision) for diagnostics/telemetry.
    pub fn select_backend_for_batch_verdict(
        caps: &HardwareCaps,
        workload_bytes: u64,
        pattern_count: usize,
        large_chunk_bytes: u64,
    ) -> BackendRoutingVerdict {
        super::select::select_backend_for_batch_verdict(
            caps,
            workload_bytes,
            pattern_count,
            large_chunk_bytes,
        )
    }

    /// Test-only forced backend override (from the `KEYHOG_*` routing env), or
    /// `None` when routing is not overridden.
    pub fn forced_backend_override_for_test() -> Option<ScanBackend> {
        super::select::forced_backend_override_for_test()
    }

    /// Parse the physical CPU core count from `/proc/cpuinfo` contents (Linux).
    #[cfg(target_os = "linux")]
    pub fn linux_physical_cores_from_cpuinfo(content: &str) -> Option<usize> {
        super::platform::linux_physical_cores_from_cpuinfo(content)
    }

    /// Parse total system memory (MiB) from `/proc/meminfo` contents (Linux).
    #[cfg(target_os = "linux")]
    pub fn linux_total_memory_mb_from_meminfo(content: &str) -> Option<u64> {
        super::platform::linux_total_memory_mb_from_meminfo(content)
    }
}