1use super::*;
2use crate::hw_probe::ScanBackend;
3
4#[inline]
5fn scan_deadline_expired(deadline: Option<std::time::Instant>) -> bool {
6 let expired = crate::deadline::expired(deadline);
7 if expired {
8 crate::telemetry::record_chunk_deadline_abort();
9 }
10 expired
11}
12
13fn backend_driver_name(backend: ScanBackend) -> &'static str {
14 match backend {
15 ScanBackend::GpuCuda => "cuda",
16 ScanBackend::GpuMetal => "metal",
17 ScanBackend::GpuWgpu => "wgpu",
18 _ => "",
19 }
20}
21
22#[cfg(test)]
32#[derive(Default)]
33pub(crate) struct Phase2PoolBreakdown {
34 pub(crate) generic_entropy_real: usize,
35 pub(crate) generic_entropy_homoglyph: usize,
36 pub(crate) vendor_real: usize,
37 pub(crate) vendor_homoglyph: usize,
38 pub(crate) vendor_real_ids: Vec<String>,
39}
40
41impl CompiledScanner {
42 #[must_use]
45 pub fn default_execution_route(&self) -> crate::ScanExecutionRoute {
46 self.execution_route_for_backend(ScanBackend::CpuFallback)
47 }
48
49 #[must_use]
50 pub fn execution_route_for_backend(&self, backend: ScanBackend) -> crate::ScanExecutionRoute {
51 crate::ScanExecutionRoute {
52 decode_backend: if backend.is_gpu() {
53 ScanBackend::CpuFallback
54 } else {
55 backend
56 },
57 phase2_plain_localizer: self.tuning.phase2_plain_localizer_enabled(),
58 phase2_keyword_localizer: true,
59 }
60 }
61
62 pub fn prepare_autoroute_calibration_gpu_artifact(&self) -> std::result::Result<(), String> {
67 let eligible_gpu = self
68 .gpu_backend_candidates()
69 .into_iter()
70 .filter(|candidate| candidate.is_eligible())
71 .collect::<Vec<_>>();
72 if eligible_gpu.is_empty() {
73 self.autoroute_gpu_shared_cold_ns
74 .store(0, std::sync::atomic::Ordering::Relaxed);
75 return Ok(());
76 }
77 if self.gpu_matcher().is_none() {
78 return Err(
79 "eligible GPU peers exist but the shared literal program could not be prepared"
80 .to_string(),
81 );
82 }
83 if self
84 .autoroute_gpu_shared_cold_ns
85 .load(std::sync::atomic::Ordering::Acquire)
86 == 0
87 {
88 return Err(
89 "the shared GPU literal program initialized without recording its preparation duration"
90 .to_string(),
91 );
92 }
93 #[cfg(feature = "gpu")]
94 for candidate in eligible_gpu {
95 let backend_id = candidate.driver_id.ok_or_else(|| {
96 "eligible GPU peer has no driver identity during phase-2 preparation".to_string()
97 })?;
98 let _catalog = self.phase2_gpu_dfa_catalog(Some(backend_id));
99 if self.phase2_gpu_dfa.preparation_ns(Some(backend_id)) == 0 {
100 return Err(format!(
101 "the {backend_id} phase-2 GPU program initialized without recording its preparation duration"
102 ));
103 }
104 }
105 Ok(())
106 }
107
108 pub fn initialize_simd_backend(&self) -> std::result::Result<(), String> {
110 self.try_initialize_simd_backend().map_err(str::to_owned)
111 }
112
113 #[must_use]
115 pub fn simd_initialization_ns(&self) -> Option<u128> {
116 #[cfg(feature = "simd")]
117 {
118 let ns = self
119 .simd_initialization_ns
120 .load(std::sync::atomic::Ordering::Acquire);
121 return (self.simd_backend_initialized() && ns > 0).then_some(ns as u128);
122 }
123 #[cfg(not(feature = "simd"))]
124 {
125 None
126 }
127 }
128
129 pub fn reset_autoroute_calibration_gpu_workload(&self) -> std::result::Result<(), String> {
133 #[cfg(feature = "gpu")]
134 {
135 self.reset_gpu_resident_literal_for_calibration()?;
136 }
137 Ok(())
138 }
139
140 #[must_use]
141 pub fn autoroute_calibration_gpu_shared_cold_ns(&self) -> u128 {
142 self.autoroute_gpu_shared_cold_ns
143 .load(std::sync::atomic::Ordering::Acquire) as u128
144 }
145
146 #[must_use]
149 pub fn autoroute_calibration_gpu_backend_cold_ns(&self, backend: ScanBackend) -> Option<u128> {
150 #[cfg(feature = "gpu")]
151 {
152 let candidate = self
153 .gpu_backend_candidates()
154 .into_iter()
155 .find(|candidate| candidate.backend == backend && candidate.is_eligible())?;
156 let preparation_ns = self.phase2_gpu_dfa.preparation_ns(candidate.driver_id);
157 return (preparation_ns > 0).then_some(preparation_ns);
158 }
159 #[cfg(not(feature = "gpu"))]
160 {
161 let _backend = backend;
162 None
163 }
164 }
165
166 #[cfg(feature = "simd")]
168 pub(crate) fn try_simd_prefilter(
169 &self,
170 ) -> std::result::Result<&crate::engine::SimdPhase1Prefilter, &str> {
171 if !self.simd_candidate_available {
172 return Err("the detector corpus produced no Hyperscan phase-one plan");
173 }
174 self.simd_prefilter
175 .get_or_init(|| {
176 let started = std::time::Instant::now();
177 let plan = self
178 .simd_compile_plan
179 .lock()
180 .map_err(|_| "Hyperscan compile-plan lock was poisoned".to_string())?
181 .take()
182 .ok_or_else(|| "Hyperscan compile plan was already consumed".to_string())?;
183 let result = plan.materialize();
184 self.simd_initialization_ns.store(
185 u64::try_from(started.elapsed().as_nanos())
186 .unwrap_or(u64::MAX)
188 .max(1),
189 std::sync::atomic::Ordering::Release,
190 );
191 result
192 })
193 .as_ref()
194 .map_err(String::as_str)
195 }
196
197 pub(crate) fn try_initialize_simd_backend(&self) -> std::result::Result<(), &str> {
198 #[cfg(feature = "simd")]
199 {
200 self.try_simd_prefilter().map(|_| ())
201 }
202 #[cfg(not(feature = "simd"))]
203 {
204 Err("this scanner build has no Hyperscan/SIMD backend")
205 }
206 }
207
208 #[must_use]
211 pub fn simd_backend_available(&self) -> bool {
212 #[cfg(feature = "simd")]
213 {
214 self.simd_candidate_available
215 }
216 #[cfg(not(feature = "simd"))]
217 {
218 false
219 }
220 }
221
222 #[must_use]
224 pub fn simd_backend_initialized(&self) -> bool {
225 #[cfg(feature = "simd")]
226 {
227 self.simd_prefilter
228 .get()
229 .is_some_and(std::result::Result::is_ok)
230 }
231 #[cfg(not(feature = "simd"))]
232 {
233 false
234 }
235 }
236
237 pub(crate) fn detector_count(&self) -> usize {
239 self.detector_plans.len()
240 }
241
242 pub fn try_resolve_matches(
246 &self,
247 matches: Vec<keyhog_core::RawMatch>,
248 ) -> std::result::Result<Vec<keyhog_core::RawMatch>, String> {
249 crate::resolution::try_resolve_matches_with_compiled_plan(matches, &self.detector_plans)
250 }
251
252 #[cfg(test)]
259 #[inline]
260 pub(crate) fn interned_detector_metadata(
261 &self,
262 detector_index: usize,
263 ) -> (Arc<str>, Arc<str>, Arc<str>) {
264 self.detector_plans.get(detector_index).cloned_metadata()
265 }
266
267 pub(crate) fn pattern_count(&self) -> usize {
269 self.ac_map.len() + self.phase2_patterns.len()
270 }
271
272 #[cfg(test)]
276 pub(crate) fn tuning(&self) -> &phase2::ScannerTuning {
277 &self.tuning
278 }
279
280 #[cfg(test)]
284 pub(crate) fn phase2_anchor_stats(&self) -> (usize, usize, usize) {
285 let total = self.phase2_patterns.len();
286 let always_active = self.phase2_always_active_indices.len();
287 let aae = self.phase2_anchor_index.as_ref().map_or(0, |idx| {
288 self.phase2_always_active_indices
289 .iter()
290 .filter(|&&i| idx.is_always_active_eligible(i))
291 .count()
292 });
293 (total, always_active, aae)
294 }
295
296 #[cfg(test)]
305 pub(crate) fn mark_matches_gate_ns_per_call(&self, text: &str, n_calls: u32) -> f64 {
306 let Some(prefilter) = &self.phase2_always_active_prefilter else {
307 return 0.0;
308 };
309 let tuning = self.tuning().resolve();
310 let mut scratch = phase2::ActivePatternsScratch::new();
312 scratch.begin(self.phase2_patterns.len());
313 prefilter.mark_matches(
314 &self.phase2_patterns,
315 text,
316 &mut scratch,
317 false,
318 false,
319 &tuning,
320 true,
321 );
322 let t0 = std::time::Instant::now();
324 for _ in 0..n_calls {
325 scratch.begin(self.phase2_patterns.len());
326 prefilter.mark_matches(
327 &self.phase2_patterns,
328 text,
329 &mut scratch,
330 false,
331 false,
332 &tuning,
333 true,
334 );
335 }
336 let elapsed_ns = t0.elapsed().as_nanos() as f64;
337 elapsed_ns / n_calls as f64
338 }
339
340 #[cfg(all(test, feature = "simd"))]
352 pub(crate) fn bench_hs_homoglyph_skip(
353 &self,
354 haystack: &str,
355 n_calls: u32,
356 ) -> (f64, f64, usize, usize) {
357 use super::phase2::ActivePatternsScratch;
358 use super::Phase2HsEngine;
359 let all: Vec<usize> = self.phase2_always_active_indices.clone();
360 let lean_n = all
361 .iter()
362 .filter(|&&i| !self.phase2_patterns[i].0.homoglyph_variant)
363 .count();
364 let engine = Phase2HsEngine::build(&self.phase2_patterns, &all).expect("HS engine");
368 let mut scratch = ActivePatternsScratch::new();
369 let mut time_one = |skip_homoglyph_ascii: bool| -> f64 {
370 scratch.begin(self.phase2_patterns.len());
371 if let Err(error) = engine.mark(haystack, &mut scratch, skip_homoglyph_ascii) {
372 panic!("HS benchmark warmup failed: {error}");
373 }
374 let t0 = std::time::Instant::now();
375 for _ in 0..n_calls {
376 scratch.begin(self.phase2_patterns.len());
377 if let Err(error) = engine.mark(haystack, &mut scratch, skip_homoglyph_ascii) {
378 panic!("HS benchmark trial failed: {error}");
379 }
380 }
381 t0.elapsed().as_nanos() as f64 / n_calls as f64
382 };
383 let full_ns = time_one(false);
384 let lean_ns = time_one(true);
385 (full_ns, lean_ns, all.len(), lean_n)
386 }
387
388 #[cfg(all(test, feature = "simd"))]
400 pub(crate) fn hs_mark_full_vs_lean_diff(
401 &self,
402 ascii_text: &str,
403 ) -> (usize, usize, Vec<usize>, Vec<usize>) {
404 use super::phase2::ActivePatternsScratch;
405 use super::Phase2HsEngine;
406 use std::collections::HashSet;
407 let all: Vec<usize> = self.phase2_always_active_indices.clone();
408 let engine = Phase2HsEngine::build(&self.phase2_patterns, &all).expect("HS engine");
409 let mut scratch = ActivePatternsScratch::new();
410 scratch.begin(self.phase2_patterns.len());
411 engine
412 .mark(ascii_text, &mut scratch, false)
413 .expect("full mark");
414 let full: HashSet<usize> = scratch.active.iter().copied().collect();
415 scratch.begin(self.phase2_patterns.len());
416 engine
417 .mark(ascii_text, &mut scratch, true)
418 .expect("lean mark");
419 let lean: HashSet<usize> = scratch.active.iter().copied().collect();
420 let non_homoglyph_dropped: Vec<usize> = full
421 .iter()
422 .copied()
423 .filter(|i| !lean.contains(i) && !self.phase2_patterns[*i].0.homoglyph_variant)
424 .collect();
425 let lean_extra: Vec<usize> = lean.iter().copied().filter(|i| !full.contains(i)).collect();
426 (full.len(), lean.len(), non_homoglyph_dropped, lean_extra)
427 }
428
429 #[cfg(test)]
436 pub(crate) fn phase2_pattern_diagnostics(&self) -> Vec<(String, Vec<String>)> {
437 self.phase2_patterns
438 .iter()
439 .map(|(p, kw)| (p.regex.as_str().to_string(), kw.clone()))
440 .collect()
441 }
442
443 #[cfg(test)]
453 pub(crate) fn phase2_always_active_family_breakdown(&self) -> Phase2PoolBreakdown {
454 let mut b = Phase2PoolBreakdown::default();
455 for &idx in &self.phase2_always_active_indices {
456 let pattern = &self.phase2_patterns[idx].0;
457 let id = self
458 .detector_plans
459 .get(pattern.detector_index)
460 .metadata
461 .0
462 .as_ref();
463 let generic_entropy = matches!(
464 self.detector_plans.resolution_class(id),
465 Some(
466 crate::detector_plan::DetectorResolutionClass::Generic
467 | crate::detector_plan::DetectorResolutionClass::Entropy
468 )
469 );
470 let homoglyph = pattern.homoglyph_variant;
471 match (generic_entropy, homoglyph) {
472 (true, false) => b.generic_entropy_real += 1,
473 (true, true) => b.generic_entropy_homoglyph += 1,
474 (false, false) => {
475 b.vendor_real += 1;
476 if !b.vendor_real_ids.iter().any(|existing| existing == id) {
477 b.vendor_real_ids.push(id.to_string());
478 }
479 }
480 (false, true) => b.vendor_homoglyph += 1,
481 }
482 }
483 b
484 }
485
486 pub fn warm(&self) {
501 use rayon::prelude::*;
502 const WARM_SAMPLE: &str = concat!(
505 "int main(void){ char *buf = malloc(4096); for(size_t i=0;i<len;i++){ ",
506 "config.timeout_ms = 30000; user_id=0x1f3b9c; const KEY = \"abcDEF0123456789\"; ",
507 "https://example.org/api/v2?payload=eyJhbGciOi&id=550e8400-e29b-41d4-a716; ",
508 "base64=QUtJQUlPU0ZPRE5ON0VYQU1QTEU= sha=da39a3ee5e6b4b0d3255bfef95601890; ",
509 "snake_case_name camelCaseName SCREAMING_CASE path/to/file.rs node_modules ",
510 "} /* comment */ // trailing\n\t<xml attr='v'>text</xml> {\"json\":true,\"n\":42}"
511 );
512 self.ac_map.par_iter().for_each(|p| {
513 let _ = p.regex.get().find(WARM_SAMPLE); });
515 self.phase2_patterns.par_iter().for_each(|(p, _)| {
516 let _ = p.regex.get().find(WARM_SAMPLE); });
518 crate::shared_regexes::warm_runtime_regexes();
519 if let Some(generic_assignment) = self.detector_plans.generic_assignment() {
520 let _ = generic_assignment.matcher().find(WARM_SAMPLE); }
522 crate::multiline::warm_runtime_regexes();
523 }
524
525 pub(crate) fn pattern_regex_strs(&self) -> Vec<&str> {
528 let mut out = Vec::with_capacity(self.ac_map.len() + self.phase2_patterns.len());
529 out.extend(self.ac_map.iter().map(|p| p.regex.as_str()));
530 out.extend(self.phase2_patterns.iter().map(|(p, _)| p.regex.as_str()));
531 out
532 }
533
534 pub fn runtime_status(&self) -> CompiledScannerRuntime {
539 CompiledScannerRuntime {
540 detector_count: self.detector_count(),
541 pattern_count: self.pattern_count(),
542 detector_digest: self.detector_digest(),
543 preferred_backend: self.preferred_backend_label(),
544 gpu_backends: self.gpu_backends.availability(),
545 gpu_degrade_count: self.gpu_degrade_count(),
546 }
547 }
548 #[must_use]
553 pub fn bigram_prefilter_status(&self) -> crate::bigram_bloom::BigramPrefilterStatus {
554 self.bigram_bloom.status()
555 }
556
557 #[must_use]
562 pub fn bigram_prefilter_corpus_status<'a, I>(
563 &self,
564 corpus_name: &'a str,
565 inputs: I,
566 ) -> crate::bigram_bloom::BigramPrefilterCorpusStatus<'a>
567 where
568 I: IntoIterator<Item = &'a [u8]>,
569 {
570 self.bigram_bloom.corpus_status(
571 corpus_name,
572 inputs,
573 crate::engine::BIGRAM_BLOOM_MIN_CHUNK_BYTES,
574 )
575 }
576
577 pub fn gpu_degrade_count(&self) -> u64 {
583 self.gpu_degrade_count
584 .load(std::sync::atomic::Ordering::Relaxed)
585 }
586
587 pub fn dump_profile_reports(&self, label: &str) {
592 if !profile::enabled() {
593 return;
594 }
595 profile::dump(label);
596 self.phase2_profile_dump(label);
597 self.confirmed_profile_dump(label);
598 }
599
600 pub fn reset_profile_reports(&self) {
601 profile::reset();
602 self.phase2_profile_reset();
603 self.confirmed_profile_reset();
604 }
605
606 pub(crate) fn detector_digest(&self) -> u64 {
607 self.detector_digest
608 }
609
610 #[must_use]
612 pub fn gpu_backend_candidates(&self) -> Vec<GpuBackendCandidateStatus> {
613 use crate::hw_probe::ScanBackend;
614 [
615 ScanBackend::GpuCuda,
616 ScanBackend::GpuMetal,
617 ScanBackend::GpuWgpu,
618 ]
619 .into_iter()
620 .map(|backend| {
621 let acquired = self.gpu_backends.initialized(backend);
622 let available = match backend {
623 ScanBackend::GpuCuda => self.gpu_backends.cuda_available,
624 ScanBackend::GpuMetal => self.gpu_backends.metal_available,
625 ScanBackend::GpuWgpu => self.gpu_backends.wgpu_available,
626 _ => false,
627 };
628 let acquisition_error = self
629 .gpu_backends
630 .initialization_error(backend)
631 .map(str::to_owned)
632 .or_else(|| {
633 self.gpu_acquisition_failures
634 .iter()
635 .find(|failure| failure.backend == backend_driver_name(backend))
636 .map(|failure| failure.diagnostic.clone())
637 });
638 GpuBackendCandidateStatus {
639 backend,
640 available,
641 acquired: acquired.is_some(),
642 driver_id: available.then(|| backend_driver_name(backend)),
643 driver_version: available.then(|| match backend {
644 ScanBackend::GpuCuda => env!("KEYHOG_VYRE_CUDA_VERSION"),
645 ScanBackend::GpuMetal => env!("KEYHOG_VYRE_METAL_VERSION"),
646 ScanBackend::GpuWgpu => env!("KEYHOG_VYRE_WGPU_VERSION"),
647 _ => unreachable!("candidate list contains only GPU backends"),
648 }),
649 device_identity: acquired
650 .and_then(|peer| peer.device_identity.clone())
651 .or_else(|| match backend {
652 ScanBackend::GpuCuda => self.gpu_backends.cuda_device_identity.clone(),
653 ScanBackend::GpuMetal => self.gpu_backends.metal_device_identity.clone(),
654 ScanBackend::GpuWgpu => self.gpu_backends.wgpu_device_identity.clone(),
655 _ => None,
656 }),
657 runtime_identity: match backend {
658 ScanBackend::GpuCuda => self.gpu_backends.cuda_runtime_identity.clone(),
659 ScanBackend::GpuMetal => self.gpu_backends.metal_runtime_identity.clone(),
660 ScanBackend::GpuWgpu => self.gpu_backends.wgpu_runtime_identity.clone(),
661 _ => None,
662 },
663 is_software: acquired.map_or_else(
664 || match backend {
665 ScanBackend::GpuCuda => false,
666 ScanBackend::GpuMetal => false,
667 ScanBackend::GpuWgpu => self.gpu_backends.wgpu_is_software,
668 _ => true,
669 },
670 |peer| peer.is_software,
671 ),
672 acquisition_error,
673 }
674 })
675 .collect()
676 }
677
678 pub fn acquired_gpu_peer_identity(
681 &self,
682 backend: crate::hw_probe::ScanBackend,
683 ) -> std::result::Result<String, String> {
684 if !backend.is_gpu() {
685 return Err(format!("{} is not a GPU backend", backend.label()));
686 }
687 if !self.warm_backend(backend) {
688 return Err(self.gpu_backend_unavailable_reason(backend));
689 }
690 let candidate = self
691 .gpu_backend_candidates()
692 .into_iter()
693 .find(|candidate| candidate.backend == backend)
694 .ok_or_else(|| format!("{} is not a compiled GPU peer", backend.label()))?;
695 if !candidate.acquired || !candidate.available || candidate.is_software {
696 return Err(self.gpu_backend_unavailable_reason(backend));
697 }
698 let (Some(driver_id), Some(driver_version), Some(device_identity), Some(runtime_identity)) = (
699 candidate
700 .driver_id
701 .as_deref()
702 .filter(|value| !value.trim().is_empty()),
703 candidate
704 .driver_version
705 .as_deref()
706 .filter(|value| !value.trim().is_empty()),
707 candidate
708 .device_identity
709 .as_deref()
710 .filter(|value| !value.trim().is_empty()),
711 candidate
712 .runtime_identity
713 .as_deref()
714 .filter(|value| !value.trim().is_empty()),
715 ) else {
716 let missing = [
717 (
718 "driver_id",
719 candidate
720 .driver_id
721 .as_deref()
722 .is_none_or(|value| value.trim().is_empty()),
723 ),
724 (
725 "driver_version",
726 candidate
727 .driver_version
728 .as_deref()
729 .is_none_or(|value| value.trim().is_empty()),
730 ),
731 (
732 "device_identity",
733 candidate
734 .device_identity
735 .as_deref()
736 .is_none_or(|value| value.trim().is_empty()),
737 ),
738 (
739 "runtime_identity",
740 candidate
741 .runtime_identity
742 .as_deref()
743 .is_none_or(|value| value.trim().is_empty()),
744 ),
745 ]
746 .into_iter()
747 .filter_map(|(field, absent)| absent.then_some(field))
748 .collect::<Vec<_>>()
749 .join(", ");
750 return Err(format!(
751 "{} reported acquired eligibility with missing identity fields: {missing}; reinitialize the GPU backend and recalibrate autoroute",
752 backend.label()
753 ));
754 };
755 let identity = (
756 candidate.backend.label(),
757 driver_id,
758 driver_version,
759 device_identity,
760 runtime_identity,
761 );
762 serde_json::to_string(&identity)
763 .map_err(|error| format!("GPU peer identity serialization failed: {error}"))
764 }
765
766 pub(crate) fn gpu_backend_unavailable_reason(
767 &self,
768 backend: crate::hw_probe::ScanBackend,
769 ) -> String {
770 let Some(candidate) = self
771 .gpu_backend_candidates()
772 .into_iter()
773 .find(|candidate| candidate.backend == backend)
774 else {
775 return format!("{} is not a compiled GPU peer", backend.label());
776 };
777 if let Some(error) = candidate.acquisition_error {
778 return format!(
779 "{} execution backend initialization failed: {error}",
780 backend.label()
781 );
782 }
783 if !candidate.available {
784 return format!(
785 "{} is absent from the current hardware peer census",
786 backend.label()
787 );
788 }
789 if !candidate.has_complete_identity() {
790 return format!(
791 "{} has incomplete driver, device, or runtime identity",
792 backend.label()
793 );
794 }
795 if candidate.acquired {
796 return format!("{} execution backend initialized", backend.label());
797 }
798 format!(
799 "{} did not publish an initialized execution handle",
800 backend.label()
801 )
802 }
803
804 #[cfg(feature = "gpu")]
808 pub(crate) fn last_gpu_degrade_reason(&self) -> Option<String> {
809 match self.gpu_last_degrade_reason.lock() {
810 Ok(guard) => guard.clone(),
811 Err(poisoned) => match poisoned.into_inner().clone() {
812 Some(reason) => Some(format!(
813 "GPU runtime diagnostic lock was poisoned after recording: {reason}"
814 )),
815 None => Some(
816 "GPU runtime degradation occurred, but its diagnostic lock was poisoned"
817 .to_owned(),
818 ),
819 },
820 }
821 }
822
823 #[must_use]
825 pub(crate) fn preferred_backend_label(&self) -> &'static str {
826 crate::hw_probe::ScanBackend::CpuFallback.label()
827 }
828
829 pub fn warm_backend(&self, backend: crate::hw_probe::ScanBackend) -> bool {
831 let ready = match backend {
835 crate::hw_probe::ScanBackend::GpuCuda
836 | crate::hw_probe::ScanBackend::GpuMetal
837 | crate::hw_probe::ScanBackend::GpuWgpu => self.gpu_stack_usable_for(backend),
838 crate::hw_probe::ScanBackend::SimdCpu => {
839 #[cfg(feature = "simd")]
840 {
841 match self.try_simd_prefilter() {
842 Ok(prefilter) => prefilter.scanner().warm().is_ok(),
843 Err(_) => false, }
845 }
846 #[cfg(not(feature = "simd"))]
847 {
848 false
849 }
850 }
851 crate::hw_probe::ScanBackend::CpuFallback => true,
852 };
853 ready
857 }
858
859 pub fn scan(&self, chunk: &Chunk) -> crate::error::Result<Vec<RawMatch>> {
863 self.scan_with_deadline(chunk, self.config.per_chunk_deadline())
864 }
865
866 pub fn scan_with_backend(
872 &self,
873 chunk: &Chunk,
874 backend: crate::hw_probe::ScanBackend,
875 ) -> crate::error::Result<Vec<RawMatch>> {
876 let results = self.scan_coalesced_with_backend_and_admission(
877 std::slice::from_ref(chunk),
878 backend,
879 None,
880 )?;
881 results.into_iter().next().ok_or_else(|| {
882 crate::error::ScanError::Config(
883 "single-chunk backend dispatch returned no result row".to_owned(),
884 )
885 })
886 }
887
888 pub fn scan_with_backend_and_admission_plan(
894 &self,
895 chunk: &Chunk,
896 backend: crate::hw_probe::ScanBackend,
897 plan: Option<&crate::engine::Phase1AdmissionPlan>,
898 ) -> crate::error::Result<crate::engine::CoalescedScanOutcome> {
899 self.scan_coalesced_with_backend_admission_route_and_recovery(
900 std::slice::from_ref(chunk),
901 backend,
902 plan,
903 self.execution_route_for_backend(backend),
904 false,
905 )
906 }
907
908 pub fn scan_chunks_with_backend(
913 &self,
914 chunks: &[Chunk],
915 backend: crate::hw_probe::ScanBackend,
916 ) -> crate::error::Result<Vec<Vec<RawMatch>>> {
917 self.scan_coalesced_with_backend_and_admission(chunks, backend, None)
918 }
919
920 pub fn scan_chunks_with_backend_bypassing_bigram_for_diagnostics(
927 &self,
928 chunks: &[Chunk],
929 backend: crate::hw_probe::ScanBackend,
930 ) -> crate::error::Result<Vec<Vec<RawMatch>>> {
931 let plan = self.phase1_admission_plan_bypassing_bigram_for_diagnostics(chunks);
932 self.scan_coalesced_with_backend_and_admission(chunks, backend, Some(&plan))
933 }
934
935 pub fn clear_fragment_cache(&self) {
937 self.fragment_cache.clear();
938 }
939
940 pub(crate) fn scan_with_deadline(
942 &self,
943 chunk: &Chunk,
944 deadline: Option<std::time::Instant>,
945 ) -> crate::error::Result<Vec<RawMatch>> {
946 self.scan_with_deadline_and_backend(
951 chunk,
952 deadline,
953 crate::hw_probe::ScanBackend::CpuFallback,
954 )
955 }
956
957 pub(crate) fn scan_with_deadline_and_backend(
958 &self,
959 chunk: &Chunk,
960 deadline: Option<std::time::Instant>,
961 selected_backend: crate::hw_probe::ScanBackend,
962 ) -> crate::error::Result<Vec<RawMatch>> {
963 self.scan_with_deadline_and_backend_and_admission(chunk, deadline, selected_backend, None)
964 }
965 pub(crate) fn scan_with_deadline_and_backend_and_admission(
966 &self,
967 chunk: &Chunk,
968 deadline: Option<std::time::Instant>,
969 selected_backend: crate::hw_probe::ScanBackend,
970 admission: Option<crate::engine::Phase1Admission>,
971 ) -> crate::error::Result<Vec<RawMatch>> {
972 self.scan_with_deadline_and_backend_admission_and_route(
973 chunk,
974 deadline,
975 selected_backend,
976 admission,
977 self.execution_route_for_backend(selected_backend),
978 )
979 }
980
981 pub(crate) fn scan_with_deadline_and_backend_admission_and_route(
982 &self,
983 chunk: &Chunk,
984 deadline: Option<std::time::Instant>,
985 selected_backend: crate::hw_probe::ScanBackend,
986 admission: Option<crate::engine::Phase1Admission>,
987 route: crate::ScanExecutionRoute,
988 ) -> crate::error::Result<Vec<RawMatch>> {
989 if scan_deadline_expired(deadline) {
990 return Ok(Vec::new());
991 }
992 let admission = admission.unwrap_or_else(|| self.phase1_admission(chunk.data.as_bytes()));
1004 if admission != Phase1Admission::Admitted {
1005 if self.should_scan_no_hit_chunk(chunk, route) {
1006 let prepared = self.prepare_chunk(chunk);
1007 let mut matches = self.scan_prepared_with_triggered(
1008 prepared,
1009 &[],
1010 deadline,
1011 None,
1012 None,
1013 None,
1014 None,
1015 route,
1016 )?;
1017 if scan_deadline_expired(deadline) {
1018 return Ok(matches);
1019 }
1020 self.post_process_matches(chunk, &mut matches, deadline, route)?;
1021 if scan_deadline_expired(deadline) {
1022 return Ok(matches);
1023 }
1024 return Ok(matches);
1025 }
1026
1027 if self.chunk_needs_decode_postprocess(chunk) {
1028 if scan_deadline_expired(deadline) {
1029 return Ok(Vec::new());
1030 }
1031 let mut matches = Vec::new();
1032 self.post_process_matches(chunk, &mut matches, deadline, route)?;
1033 if scan_deadline_expired(deadline) {
1034 return Ok(matches);
1035 }
1036 return Ok(matches);
1037 }
1038 crate::telemetry::record_file_skipped();
1039 return Ok(Vec::new());
1040 }
1041
1042 tracing::trace!(
1043 target: "keyhog::routing",
1044 backend = selected_backend.label(),
1045 chunk_bytes = chunk.data.len(),
1046 source_type = chunk.metadata.source_type.as_ref(),
1047 "scan dispatch"
1048 );
1049 let mut matches = if chunk.data.len() > MAX_SCAN_CHUNK_BYTES {
1050 self.scan_windowed(chunk, selected_backend, deadline, route)?
1051 } else {
1052 self.scan_inner(chunk, selected_backend, deadline, route)?
1053 };
1054
1055 if scan_deadline_expired(deadline) {
1056 return Ok(matches);
1057 }
1058 self.post_process_matches(chunk, &mut matches, deadline, route)?;
1059 if scan_deadline_expired(deadline) {
1060 return Ok(matches);
1061 }
1062
1063 Ok(matches)
1064 }
1065}