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::GpuWgpu => "wgpu",
17 _ => "",
18 }
19}
20
21#[cfg(test)]
31#[derive(Default)]
32pub(crate) struct Phase2PoolBreakdown {
33 pub(crate) generic_entropy_real: usize,
34 pub(crate) generic_entropy_homoglyph: usize,
35 pub(crate) vendor_real: usize,
36 pub(crate) vendor_homoglyph: usize,
37 pub(crate) vendor_real_ids: Vec<String>,
38}
39
40impl CompiledScanner {
41 #[must_use]
44 pub fn default_execution_route(&self) -> crate::ScanExecutionRoute {
45 self.execution_route_for_backend(ScanBackend::CpuFallback)
46 }
47
48 #[must_use]
49 pub fn execution_route_for_backend(&self, backend: ScanBackend) -> crate::ScanExecutionRoute {
50 crate::ScanExecutionRoute {
51 decode_backend: if backend.is_gpu() {
52 ScanBackend::CpuFallback
53 } else {
54 backend
55 },
56 phase2_plain_localizer: self.tuning.phase2_plain_localizer_enabled(),
57 phase2_keyword_localizer: true,
58 }
59 }
60
61 pub fn prepare_autoroute_calibration_gpu_artifact(&self) -> std::result::Result<(), String> {
66 let eligible_gpu = self
67 .gpu_backend_candidates()
68 .into_iter()
69 .filter(|candidate| candidate.is_eligible())
70 .collect::<Vec<_>>();
71 if eligible_gpu.is_empty() {
72 self.autoroute_gpu_shared_cold_ns
73 .store(0, std::sync::atomic::Ordering::Relaxed);
74 return Ok(());
75 }
76 if self.gpu_matcher().is_none() {
77 return Err(
78 "eligible GPU peers exist but the shared literal program could not be prepared"
79 .to_string(),
80 );
81 }
82 if self
83 .autoroute_gpu_shared_cold_ns
84 .load(std::sync::atomic::Ordering::Acquire)
85 == 0
86 {
87 return Err(
88 "the shared GPU literal program initialized without recording its preparation duration"
89 .to_string(),
90 );
91 }
92 #[cfg(feature = "gpu")]
93 for candidate in eligible_gpu {
94 let backend_id = candidate.driver_id.ok_or_else(|| {
95 "eligible GPU peer has no driver identity during phase-2 preparation".to_string()
96 })?;
97 let _catalog = self.phase2_gpu_dfa_catalog(Some(backend_id));
98 if self.phase2_gpu_dfa.preparation_ns(Some(backend_id)) == 0 {
99 return Err(format!(
100 "the {backend_id} phase-2 GPU program initialized without recording its preparation duration"
101 ));
102 }
103 }
104 Ok(())
105 }
106
107 pub fn initialize_simd_backend(&self) -> std::result::Result<(), String> {
109 self.try_initialize_simd_backend().map_err(str::to_owned)
110 }
111
112 #[must_use]
114 pub fn simd_initialization_ns(&self) -> Option<u128> {
115 #[cfg(feature = "simd")]
116 {
117 let ns = self
118 .simd_initialization_ns
119 .load(std::sync::atomic::Ordering::Acquire);
120 return (self.simd_backend_initialized() && ns > 0).then_some(ns as u128);
121 }
122 #[cfg(not(feature = "simd"))]
123 {
124 None
125 }
126 }
127
128 pub fn reset_autoroute_calibration_gpu_workload(&self) -> std::result::Result<(), String> {
132 #[cfg(feature = "gpu")]
133 {
134 self.reset_gpu_resident_literal_for_calibration()?;
135 }
136 Ok(())
137 }
138
139 #[must_use]
140 pub fn autoroute_calibration_gpu_shared_cold_ns(&self) -> u128 {
141 self.autoroute_gpu_shared_cold_ns
142 .load(std::sync::atomic::Ordering::Acquire) as u128
143 }
144
145 #[must_use]
148 pub fn autoroute_calibration_gpu_backend_cold_ns(&self, backend: ScanBackend) -> Option<u128> {
149 #[cfg(feature = "gpu")]
150 {
151 let candidate = self
152 .gpu_backend_candidates()
153 .into_iter()
154 .find(|candidate| candidate.backend == backend && candidate.is_eligible())?;
155 let preparation_ns = self.phase2_gpu_dfa.preparation_ns(candidate.driver_id);
156 return (preparation_ns > 0).then_some(preparation_ns);
157 }
158 #[cfg(not(feature = "gpu"))]
159 {
160 let _backend = backend;
161 None
162 }
163 }
164
165 #[cfg(feature = "simd")]
167 pub(crate) fn try_simd_prefilter(
168 &self,
169 ) -> std::result::Result<&crate::engine::SimdPhase1Prefilter, &str> {
170 if !self.simd_candidate_available {
171 return Err("the detector corpus produced no Hyperscan phase-one plan");
172 }
173 self.simd_prefilter
174 .get_or_init(|| {
175 let started = std::time::Instant::now();
176 let plan = self
177 .simd_compile_plan
178 .lock()
179 .map_err(|_| "Hyperscan compile-plan lock was poisoned".to_string())?
180 .take()
181 .ok_or_else(|| "Hyperscan compile plan was already consumed".to_string())?;
182 let result = plan.materialize();
183 self.simd_initialization_ns.store(
184 u64::try_from(started.elapsed().as_nanos())
185 .unwrap_or(u64::MAX)
187 .max(1),
188 std::sync::atomic::Ordering::Release,
189 );
190 result
191 })
192 .as_ref()
193 .map_err(String::as_str)
194 }
195
196 pub(crate) fn try_initialize_simd_backend(&self) -> std::result::Result<(), &str> {
197 #[cfg(feature = "simd")]
198 {
199 self.try_simd_prefilter().map(|_| ())
200 }
201 #[cfg(not(feature = "simd"))]
202 {
203 Err("this scanner build has no Hyperscan/SIMD backend")
204 }
205 }
206
207 #[must_use]
210 pub fn simd_backend_available(&self) -> bool {
211 #[cfg(feature = "simd")]
212 {
213 self.simd_candidate_available
214 }
215 #[cfg(not(feature = "simd"))]
216 {
217 false
218 }
219 }
220
221 #[must_use]
223 pub fn simd_backend_initialized(&self) -> bool {
224 #[cfg(feature = "simd")]
225 {
226 self.simd_prefilter
227 .get()
228 .is_some_and(std::result::Result::is_ok)
229 }
230 #[cfg(not(feature = "simd"))]
231 {
232 false
233 }
234 }
235
236 pub(crate) fn require_selected_backend_stack(&self, backend: ScanBackend) {
238 if backend == ScanBackend::SimdCpu {
239 if let Err(error) = self.try_initialize_simd_backend() {
240 crate::process_exit::backend_unavailable(format!(
241 "simd-regex selected but Hyperscan initialization failed: {error}; silent \
242 cpu-fallback execution is forbidden. Run `keyhog backend --self-test` or \
243 choose `--backend cpu-fallback` explicitly."
244 ));
245 }
246 }
247 require_selected_gpu_stack(self, backend);
248 }
249
250 pub(crate) fn detector_count(&self) -> usize {
252 self.detector_plans.len()
253 }
254
255 pub fn try_resolve_matches(
259 &self,
260 matches: Vec<keyhog_core::RawMatch>,
261 ) -> std::result::Result<Vec<keyhog_core::RawMatch>, String> {
262 crate::resolution::try_resolve_matches_with_compiled_plan(matches, &self.detector_plans)
263 }
264
265 #[cfg(test)]
272 #[inline]
273 pub(crate) fn interned_detector_metadata(
274 &self,
275 detector_index: usize,
276 ) -> (Arc<str>, Arc<str>, Arc<str>) {
277 self.detector_plans.get(detector_index).cloned_metadata()
278 }
279
280 pub(crate) fn pattern_count(&self) -> usize {
282 self.ac_map.len() + self.phase2_patterns.len()
283 }
284
285 #[cfg(test)]
289 pub(crate) fn tuning(&self) -> &phase2::ScannerTuning {
290 &self.tuning
291 }
292
293 #[cfg(test)]
297 pub(crate) fn phase2_anchor_stats(&self) -> (usize, usize, usize) {
298 let total = self.phase2_patterns.len();
299 let always_active = self.phase2_always_active_indices.len();
300 let aae = self.phase2_anchor_index.as_ref().map_or(0, |idx| {
301 self.phase2_always_active_indices
302 .iter()
303 .filter(|&&i| idx.is_always_active_eligible(i))
304 .count()
305 });
306 (total, always_active, aae)
307 }
308
309 #[cfg(test)]
318 pub(crate) fn mark_matches_gate_ns_per_call(&self, text: &str, n_calls: u32) -> f64 {
319 let Some(prefilter) = &self.phase2_always_active_prefilter else {
320 return 0.0;
321 };
322 let tuning = self.tuning().resolve();
323 let mut scratch = phase2::ActivePatternsScratch::new();
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 let t0 = std::time::Instant::now();
337 for _ in 0..n_calls {
338 scratch.begin(self.phase2_patterns.len());
339 prefilter.mark_matches(
340 &self.phase2_patterns,
341 text,
342 &mut scratch,
343 false,
344 false,
345 &tuning,
346 true,
347 );
348 }
349 let elapsed_ns = t0.elapsed().as_nanos() as f64;
350 elapsed_ns / n_calls as f64
351 }
352
353 #[cfg(all(test, feature = "simd"))]
365 pub(crate) fn bench_hs_homoglyph_skip(
366 &self,
367 haystack: &str,
368 n_calls: u32,
369 ) -> (f64, f64, usize, usize) {
370 use super::phase2::ActivePatternsScratch;
371 use super::Phase2HsEngine;
372 let all: Vec<usize> = self.phase2_always_active_indices.clone();
373 let lean_n = all
374 .iter()
375 .filter(|&&i| !self.phase2_patterns[i].0.homoglyph_variant)
376 .count();
377 let engine = Phase2HsEngine::build(&self.phase2_patterns, &all).expect("HS engine");
381 let mut scratch = ActivePatternsScratch::new();
382 let mut time_one = |skip_homoglyph_ascii: bool| -> f64 {
383 scratch.begin(self.phase2_patterns.len());
384 if let Err(error) = engine.mark(haystack, &mut scratch, skip_homoglyph_ascii) {
385 panic!("HS benchmark warmup failed: {error}");
386 }
387 let t0 = std::time::Instant::now();
388 for _ in 0..n_calls {
389 scratch.begin(self.phase2_patterns.len());
390 if let Err(error) = engine.mark(haystack, &mut scratch, skip_homoglyph_ascii) {
391 panic!("HS benchmark trial failed: {error}");
392 }
393 }
394 t0.elapsed().as_nanos() as f64 / n_calls as f64
395 };
396 let full_ns = time_one(false);
397 let lean_ns = time_one(true);
398 (full_ns, lean_ns, all.len(), lean_n)
399 }
400
401 #[cfg(all(test, feature = "simd"))]
413 pub(crate) fn hs_mark_full_vs_lean_diff(
414 &self,
415 ascii_text: &str,
416 ) -> (usize, usize, Vec<usize>, Vec<usize>) {
417 use super::phase2::ActivePatternsScratch;
418 use super::Phase2HsEngine;
419 use std::collections::HashSet;
420 let all: Vec<usize> = self.phase2_always_active_indices.clone();
421 let engine = Phase2HsEngine::build(&self.phase2_patterns, &all).expect("HS engine");
422 let mut scratch = ActivePatternsScratch::new();
423 scratch.begin(self.phase2_patterns.len());
424 engine
425 .mark(ascii_text, &mut scratch, false)
426 .expect("full mark");
427 let full: HashSet<usize> = scratch.active.iter().copied().collect();
428 scratch.begin(self.phase2_patterns.len());
429 engine
430 .mark(ascii_text, &mut scratch, true)
431 .expect("lean mark");
432 let lean: HashSet<usize> = scratch.active.iter().copied().collect();
433 let non_homoglyph_dropped: Vec<usize> = full
434 .iter()
435 .copied()
436 .filter(|i| !lean.contains(i) && !self.phase2_patterns[*i].0.homoglyph_variant)
437 .collect();
438 let lean_extra: Vec<usize> = lean.iter().copied().filter(|i| !full.contains(i)).collect();
439 (full.len(), lean.len(), non_homoglyph_dropped, lean_extra)
440 }
441
442 #[cfg(test)]
449 pub(crate) fn phase2_pattern_diagnostics(&self) -> Vec<(String, Vec<String>)> {
450 self.phase2_patterns
451 .iter()
452 .map(|(p, kw)| (p.regex.as_str().to_string(), kw.clone()))
453 .collect()
454 }
455
456 #[cfg(test)]
466 pub(crate) fn phase2_always_active_family_breakdown(&self) -> Phase2PoolBreakdown {
467 let mut b = Phase2PoolBreakdown::default();
468 for &idx in &self.phase2_always_active_indices {
469 let pattern = &self.phase2_patterns[idx].0;
470 let id = self
471 .detector_plans
472 .get(pattern.detector_index)
473 .metadata
474 .0
475 .as_ref();
476 let generic_entropy = matches!(
477 self.detector_plans.resolution_class(id),
478 Some(
479 crate::detector_plan::DetectorResolutionClass::Generic
480 | crate::detector_plan::DetectorResolutionClass::Entropy
481 )
482 );
483 let homoglyph = pattern.homoglyph_variant;
484 match (generic_entropy, homoglyph) {
485 (true, false) => b.generic_entropy_real += 1,
486 (true, true) => b.generic_entropy_homoglyph += 1,
487 (false, false) => {
488 b.vendor_real += 1;
489 if !b.vendor_real_ids.iter().any(|existing| existing == id) {
490 b.vendor_real_ids.push(id.to_string());
491 }
492 }
493 (false, true) => b.vendor_homoglyph += 1,
494 }
495 }
496 b
497 }
498
499 pub fn warm(&self) {
514 use rayon::prelude::*;
515 const WARM_SAMPLE: &str = concat!(
518 "int main(void){ char *buf = malloc(4096); for(size_t i=0;i<len;i++){ ",
519 "config.timeout_ms = 30000; user_id=0x1f3b9c; const KEY = \"abcDEF0123456789\"; ",
520 "https://example.org/api/v2?token=eyJhbGciOi&id=550e8400-e29b-41d4-a716; ",
521 "base64=QUtJQUlPU0ZPRE5ON0VYQU1QTEU= sha=da39a3ee5e6b4b0d3255bfef95601890; ",
522 "snake_case_name camelCaseName SCREAMING_CASE path/to/file.rs node_modules ",
523 "} /* comment */ // trailing\n\t<xml attr='v'>text</xml> {\"json\":true,\"n\":42}"
524 );
525 self.ac_map.par_iter().for_each(|p| {
526 let _ = p.regex.get().find(WARM_SAMPLE); });
528 self.phase2_patterns.par_iter().for_each(|(p, _)| {
529 let _ = p.regex.get().find(WARM_SAMPLE); });
531 crate::shared_regexes::warm_runtime_regexes();
532 if let Some(generic_assignment) = self.detector_plans.generic_assignment() {
533 let _ = generic_assignment.matcher().find(WARM_SAMPLE); }
535 crate::multiline::warm_runtime_regexes();
536 }
537
538 pub(crate) fn pattern_regex_strs(&self) -> Vec<&str> {
541 let mut out = Vec::with_capacity(self.ac_map.len() + self.phase2_patterns.len());
542 out.extend(self.ac_map.iter().map(|p| p.regex.as_str()));
543 out.extend(self.phase2_patterns.iter().map(|(p, _)| p.regex.as_str()));
544 out
545 }
546
547 pub fn runtime_status(&self) -> CompiledScannerRuntime {
552 CompiledScannerRuntime {
553 detector_count: self.detector_count(),
554 pattern_count: self.pattern_count(),
555 detector_digest: self.detector_digest(),
556 preferred_backend: self.preferred_backend_label(),
557 gpu_backends: self.gpu_backends.availability(),
558 gpu_degrade_count: self
559 .gpu_degrade_count
560 .load(std::sync::atomic::Ordering::Relaxed),
561 }
562 }
563
564 pub fn gpu_degrade_count(&self) -> u64 {
569 self.gpu_degrade_count
570 .load(std::sync::atomic::Ordering::Relaxed)
571 }
572
573 pub fn dump_profile_reports(&self, label: &str) {
578 if !profile::enabled() {
579 return;
580 }
581 profile::dump(label);
582 self.phase2_profile_dump(label);
583 self.confirmed_profile_dump(label);
584 }
585
586 pub fn reset_profile_reports(&self) {
587 profile::reset();
588 self.phase2_profile_reset();
589 self.confirmed_profile_reset();
590 }
591
592 pub(crate) fn detector_digest(&self) -> u64 {
593 self.detector_digest
594 }
595
596 #[must_use]
598 pub fn gpu_backend_candidates(&self) -> Vec<GpuBackendCandidateStatus> {
599 use crate::hw_probe::ScanBackend;
600 [ScanBackend::GpuCuda, ScanBackend::GpuWgpu]
601 .into_iter()
602 .map(|backend| {
603 let acquired = self.gpu_backends.initialized(backend);
604 let available = match backend {
605 ScanBackend::GpuCuda => self.gpu_backends.cuda_available,
606 ScanBackend::GpuWgpu => self.gpu_backends.wgpu_available,
607 _ => false,
608 };
609 let acquisition_error = self
610 .gpu_backends
611 .initialization_error(backend)
612 .map(str::to_owned)
613 .or_else(|| {
614 self.gpu_acquisition_failures
615 .iter()
616 .find(|failure| failure.backend == backend_driver_name(backend))
617 .map(|failure| failure.diagnostic.clone())
618 });
619 GpuBackendCandidateStatus {
620 backend,
621 available,
622 acquired: acquired.is_some(),
623 driver_id: available.then(|| backend_driver_name(backend)),
624 driver_version: available.then(|| match backend {
625 ScanBackend::GpuCuda => env!("KEYHOG_VYRE_CUDA_VERSION"),
626 ScanBackend::GpuWgpu => env!("KEYHOG_VYRE_WGPU_VERSION"),
627 _ => unreachable!("candidate list contains only GPU backends"),
628 }),
629 device_identity: acquired
630 .and_then(|peer| peer.device_identity.clone())
631 .or_else(|| match backend {
632 ScanBackend::GpuCuda => self.gpu_backends.cuda_device_identity.clone(),
633 ScanBackend::GpuWgpu => self.gpu_backends.wgpu_device_identity.clone(),
634 _ => None,
635 }),
636 runtime_identity: match backend {
637 ScanBackend::GpuCuda => self.gpu_backends.cuda_runtime_identity.clone(),
638 ScanBackend::GpuWgpu => self.gpu_backends.wgpu_runtime_identity.clone(),
639 _ => None,
640 },
641 is_software: acquired.map_or_else(
642 || match backend {
643 ScanBackend::GpuCuda => false,
644 ScanBackend::GpuWgpu => self.gpu_backends.wgpu_is_software,
645 _ => true,
646 },
647 |peer| peer.is_software,
648 ),
649 acquisition_error,
650 }
651 })
652 .collect()
653 }
654
655 pub fn acquired_gpu_peer_identity(
658 &self,
659 backend: crate::hw_probe::ScanBackend,
660 ) -> std::result::Result<String, String> {
661 if !backend.is_gpu() {
662 return Err(format!("{} is not a GPU backend", backend.label()));
663 }
664 if !self.warm_backend(backend) {
665 return Err(self.gpu_backend_unavailable_reason(backend));
666 }
667 let candidate = self
668 .gpu_backend_candidates()
669 .into_iter()
670 .find(|candidate| candidate.backend == backend)
671 .ok_or_else(|| format!("{} is not a compiled GPU peer", backend.label()))?;
672 if !candidate.is_acquired_eligible() {
673 return Err(self.gpu_backend_unavailable_reason(backend));
674 }
675 let identity = (
676 candidate.backend.label(),
677 candidate.driver_id.expect("acquired eligible driver id"),
678 candidate
679 .driver_version
680 .expect("acquired eligible driver version"),
681 candidate
682 .device_identity
683 .expect("acquired eligible device identity"),
684 candidate
685 .runtime_identity
686 .expect("acquired eligible runtime identity"),
687 );
688 serde_json::to_string(&identity)
689 .map_err(|error| format!("GPU peer identity serialization failed: {error}"))
690 }
691
692 pub(crate) fn gpu_backend_unavailable_reason(
693 &self,
694 backend: crate::hw_probe::ScanBackend,
695 ) -> String {
696 let Some(candidate) = self
697 .gpu_backend_candidates()
698 .into_iter()
699 .find(|candidate| candidate.backend == backend)
700 else {
701 return format!("{} is not a compiled GPU peer", backend.label());
702 };
703 if let Some(error) = candidate.acquisition_error {
704 return format!(
705 "{} execution backend initialization failed: {error}",
706 backend.label()
707 );
708 }
709 if !candidate.available {
710 return format!(
711 "{} is absent from the current hardware peer census",
712 backend.label()
713 );
714 }
715 if !candidate.has_complete_identity() {
716 return format!(
717 "{} has incomplete driver, device, or runtime identity",
718 backend.label()
719 );
720 }
721 if candidate.acquired {
722 return format!("{} execution backend initialized", backend.label());
723 }
724 format!(
725 "{} did not publish an initialized execution handle",
726 backend.label()
727 )
728 }
729
730 #[cfg(feature = "gpu")]
734 pub(crate) fn last_gpu_degrade_reason(&self) -> Option<String> {
735 match self.gpu_last_degrade_reason.lock() {
736 Ok(guard) => guard.clone(),
737 Err(poisoned) => match poisoned.into_inner().clone() {
738 Some(reason) => Some(format!(
739 "GPU runtime diagnostic lock was poisoned after recording: {reason}"
740 )),
741 None => Some(
742 "GPU runtime degradation occurred, but its diagnostic lock was poisoned"
743 .to_owned(),
744 ),
745 },
746 }
747 }
748
749 #[must_use]
751 pub(crate) fn preferred_backend_label(&self) -> &'static str {
752 crate::hw_probe::ScanBackend::CpuFallback.label()
753 }
754
755 pub fn warm_backend(&self, backend: crate::hw_probe::ScanBackend) -> bool {
757 let ready = match backend {
761 crate::hw_probe::ScanBackend::GpuCuda | crate::hw_probe::ScanBackend::GpuWgpu => {
762 self.gpu_stack_usable_for(backend)
763 }
764 crate::hw_probe::ScanBackend::SimdCpu => self.try_initialize_simd_backend().is_ok(),
765 crate::hw_probe::ScanBackend::CpuFallback => true,
766 };
767 ready
779 }
780
781 pub fn scan(&self, chunk: &Chunk) -> Vec<RawMatch> {
783 self.scan_with_deadline(chunk, self.config.per_chunk_deadline())
784 }
785
786 pub fn scan_with_backend(
793 &self,
794 chunk: &Chunk,
795 backend: crate::hw_probe::ScanBackend,
796 ) -> Vec<RawMatch> {
797 self.scan_with_deadline_and_backend(chunk, self.config.per_chunk_deadline(), backend)
798 }
799
800 pub fn scan_with_backend_and_admission_plan(
804 &self,
805 chunk: &Chunk,
806 backend: crate::hw_probe::ScanBackend,
807 plan: Option<&crate::engine::Phase1AdmissionPlan>,
808 ) -> Vec<RawMatch> {
809 let admission = match plan {
810 Some(p) if p.matches_chunks(std::slice::from_ref(chunk)) => p.admission_for(0),
811 Some(_) => {
812 tracing::warn!(
814 "Phase1AdmissionPlan does not match live chunk identity; recomputing admission (recall preserved)"
815 );
816 None
817 }
818 None => None,
819 };
820 self.scan_with_deadline_and_backend_and_admission(
821 chunk,
822 self.config.per_chunk_deadline(),
823 backend,
824 admission,
825 )
826 }
827
828 pub fn scan_chunks_with_backend(
834 &self,
835 chunks: &[Chunk],
836 backend: crate::hw_probe::ScanBackend,
837 ) -> Vec<Vec<RawMatch>> {
838 self.require_selected_backend_stack(backend);
839 profile::add_bytes(chunks.iter().map(|c| c.data.len() as u64).sum());
840 profile::add_files(chunks.len() as u64);
841 self.scan_chunks_with_backend_internal(chunks, backend)
842 }
843
844 pub fn clear_fragment_cache(&self) {
846 self.fragment_cache.clear();
847 }
848
849 pub(crate) fn scan_with_deadline(
851 &self,
852 chunk: &Chunk,
853 deadline: Option<std::time::Instant>,
854 ) -> Vec<RawMatch> {
855 self.scan_with_deadline_and_backend(
860 chunk,
861 deadline,
862 crate::hw_probe::ScanBackend::CpuFallback,
863 )
864 }
865
866 pub(crate) fn scan_with_deadline_and_backend(
867 &self,
868 chunk: &Chunk,
869 deadline: Option<std::time::Instant>,
870 selected_backend: crate::hw_probe::ScanBackend,
871 ) -> Vec<RawMatch> {
872 self.scan_with_deadline_and_backend_and_admission(chunk, deadline, selected_backend, None)
873 }
874
875 pub(crate) fn scan_with_deadline_and_backend_and_admission(
876 &self,
877 chunk: &Chunk,
878 deadline: Option<std::time::Instant>,
879 selected_backend: crate::hw_probe::ScanBackend,
880 admission: Option<crate::engine::Phase1Admission>,
881 ) -> Vec<RawMatch> {
882 self.scan_with_deadline_and_backend_admission_and_route(
883 chunk,
884 deadline,
885 selected_backend,
886 admission,
887 self.execution_route_for_backend(selected_backend),
888 )
889 }
890
891 pub(crate) fn scan_with_deadline_and_backend_admission_and_route(
892 &self,
893 chunk: &Chunk,
894 deadline: Option<std::time::Instant>,
895 selected_backend: crate::hw_probe::ScanBackend,
896 admission: Option<crate::engine::Phase1Admission>,
897 route: crate::ScanExecutionRoute,
898 ) -> Vec<RawMatch> {
899 if scan_deadline_expired(deadline) {
900 return Vec::new();
901 }
902 self.require_selected_backend_stack(selected_backend);
903 let admission = admission.unwrap_or_else(|| self.phase1_admission(chunk.data.as_bytes()));
915 if admission != Phase1Admission::Admitted {
916 if self.should_scan_no_hit_chunk(chunk, route) {
917 let prepared = self.prepare_chunk(chunk);
918 let mut matches = self.scan_prepared_with_triggered(
919 prepared,
920 &[],
921 deadline,
922 None,
923 None,
924 None,
925 None,
926 route,
927 );
928 if scan_deadline_expired(deadline) {
929 return matches;
930 }
931 self.post_process_matches(chunk, &mut matches, deadline, route);
932 if scan_deadline_expired(deadline) {
933 return matches;
934 }
935 return matches;
936 }
937
938 if self.chunk_needs_decode_postprocess(chunk) {
939 if scan_deadline_expired(deadline) {
940 return Vec::new();
941 }
942 let mut matches = Vec::new();
943 self.post_process_matches(chunk, &mut matches, deadline, route);
944 if scan_deadline_expired(deadline) {
945 return matches;
946 }
947 return matches;
948 }
949 crate::telemetry::record_file_skipped();
950 return Vec::new();
951 }
952
953 tracing::trace!(
954 target: "keyhog::routing",
955 backend = selected_backend.label(),
956 chunk_bytes = chunk.data.len(),
957 source_type = chunk.metadata.source_type.as_ref(),
958 "scan dispatch"
959 );
960 let mut matches = if chunk.data.len() > MAX_SCAN_CHUNK_BYTES {
961 self.scan_windowed(chunk, selected_backend, deadline, route)
962 } else {
963 self.scan_inner(chunk, selected_backend, deadline, route)
964 };
965
966 if scan_deadline_expired(deadline) {
967 return matches;
968 }
969 self.post_process_matches(chunk, &mut matches, deadline, route);
970 if scan_deadline_expired(deadline) {
971 return matches;
972 }
973
974 matches
975 }
976}