1use std::cell::Cell;
23use std::sync::atomic::{AtomicBool, Ordering::Relaxed};
24
25#[derive(Copy, Clone)]
28#[repr(usize)]
29pub(crate) enum P {
30 Preprocess = 0,
31 Phase1Triggers,
32 BackendDispatch,
36 Hot,
37 Confirmed,
38 Phase2Prefilter,
41 Phase2KeywordAc,
43 Phase2SharedAc,
45 Phase2AnchoredVerify,
47 Phase2WholeChunk,
49 Generic,
50 Entropy,
51 Ml,
52 Decode,
56}
57
58const N: usize = 14;
59
60const NAMES: [&str; N] = [
61 "preprocess",
62 "phase1",
63 "backend-dispatch",
64 "hot",
65 "confirmed",
66 "phase2:prefilter",
67 "phase2:keyword-ac",
68 "phase2:shared-ac",
69 "phase2:verify",
70 "phase2:whole-chunk",
71 "generic",
72 "entropy",
73 "ml",
74 "decode",
75];
76
77static DETAILED_ENABLED: AtomicBool = AtomicBool::new(false);
78static PERF_TRACE_ENABLED: AtomicBool = AtomicBool::new(false);
79
80pub fn set_profile_enabled(enabled: bool) {
87 DETAILED_ENABLED.store(enabled, Relaxed);
88 keyhog_profile::set_enabled(enabled);
89}
90
91pub fn set_perf_trace_enabled(enabled: bool) {
96 PERF_TRACE_ENABLED.store(enabled, Relaxed);
97}
98
99pub(crate) fn enabled() -> bool {
100 DETAILED_ENABLED.load(Relaxed)
101}
102
103pub(crate) fn perf_trace_enabled() -> bool {
104 PERF_TRACE_ENABLED.load(Relaxed)
105}
106
107thread_local! {
108 static IN_DECODE: Cell<bool> = const { Cell::new(false) };
111}
112
113#[cfg(feature = "decode")]
116pub(crate) fn set_in_decode(on: bool) -> bool {
117 let previous = IN_DECODE.with(|cell| cell.replace(on));
118 keyhog_profile::set_attribution(if on {
119 keyhog_profile::Attribution::Decoded
120 } else {
121 keyhog_profile::Attribution::Root
122 });
123 previous
124}
125
126#[inline]
138pub(crate) fn in_decode() -> bool {
139 IN_DECODE.with(Cell::get)
140}
141
142pub(crate) type Guard = keyhog_profile::Span;
143
144fn stage(point: P) -> keyhog_profile::Stage {
145 use keyhog_profile::Stage;
146 match point {
147 P::Preprocess => Stage::Preprocess,
148 P::Phase1Triggers => Stage::Phase1Triggers,
149 P::BackendDispatch => Stage::BackendDispatch,
150 P::Hot => Stage::HotPatterns,
151 P::Confirmed => Stage::ConfirmedPatterns,
152 P::Phase2Prefilter => Stage::Phase2Prefilter,
153 P::Phase2KeywordAc => Stage::Phase2KeywordAc,
154 P::Phase2SharedAc => Stage::Phase2SharedAc,
155 P::Phase2AnchoredVerify => Stage::Phase2AnchoredVerify,
156 P::Phase2WholeChunk => Stage::Phase2WholeChunk,
157 P::Generic => Stage::GenericDetection,
158 P::Entropy => Stage::Entropy,
159 P::Ml => Stage::MachineLearning,
160 P::Decode => Stage::Decode,
161 }
162}
163
164fn point_index(stage: keyhog_profile::Stage) -> Option<usize> {
165 use keyhog_profile::Stage;
166 Some(match stage {
167 Stage::Preprocess => P::Preprocess as usize,
168 Stage::Phase1Triggers => P::Phase1Triggers as usize,
169 Stage::BackendDispatch => P::BackendDispatch as usize,
170 Stage::HotPatterns => P::Hot as usize,
171 Stage::ConfirmedPatterns => P::Confirmed as usize,
172 Stage::Phase2Prefilter => P::Phase2Prefilter as usize,
173 Stage::Phase2KeywordAc => P::Phase2KeywordAc as usize,
174 Stage::Phase2SharedAc => P::Phase2SharedAc as usize,
175 Stage::Phase2AnchoredVerify => P::Phase2AnchoredVerify as usize,
176 Stage::Phase2WholeChunk => P::Phase2WholeChunk as usize,
177 Stage::GenericDetection => P::Generic as usize,
178 Stage::Entropy => P::Entropy as usize,
179 Stage::MachineLearning => P::Ml as usize,
180 Stage::Decode => P::Decode as usize,
181 Stage::SourceAcquire
182 | Stage::SourceWalk
183 | Stage::SourceRead
184 | Stage::SourceQueueWait
185 | Stage::ScannerQueueWait
186 | Stage::IncrementalLookup
187 | Stage::BackendSelect
188 | Stage::ResultMerge
189 | Stage::Suppression
190 | Stage::LiveVerification
191 | Stage::Reporting => return None,
192 })
193}
194
195#[inline]
197#[must_use]
198pub(crate) fn span(point: P) -> Guard {
199 keyhog_profile::span(stage(point))
200}
201
202pub(crate) fn add_bytes(bytes: u64) {
204 keyhog_profile::add_input_bytes(bytes);
205}
206
207pub(crate) fn add_files(files: u64) {
209 keyhog_profile::add_input_units(files);
210}
211
212fn read_reset() -> ([u64; N], [u64; N], [u64; N], u64, u64) {
213 let mut ns = [0; N];
214 let mut calls = [0; N];
215 let mut ns_decode = [0; N];
216 for measurement in keyhog_profile::take_stage_measurements() {
217 let Some(index) = point_index(measurement.stage) else {
218 continue;
219 };
220 ns[index] = measurement.elapsed_ns;
221 calls[index] = measurement.calls;
222 ns_decode[index] = measurement.attributed_ns;
223 }
224 let (bytes, files) = keyhog_profile::take_input_totals();
225 (ns, calls, ns_decode, bytes, files)
226}
227
228pub fn reset() {
230 keyhog_profile::reset();
231 crate::engine::scan_inner_profile::scan_inner_profile_reset();
232 crate::engine::scan_postprocess::decode_profile_reset();
233 crate::decode::extract_profile_reset();
234 crate::decode::decoder_profile_reset();
235 crate::engine::phase2_generic::generic_profile_reset();
236 crate::engine::phase2::phase2_mark_stats_reset();
237 crate::engine::phase2::hs_mark_timing_reset();
238 crate::engine::scan_postprocess::ml_batch_profile_reset();
239 crate::gpu::ml_split_profile_reset();
240}
241
242const PHASE2_CAPTURE_LEAVES: [usize; 5] = [
243 P::Phase2Prefilter as usize,
244 P::Phase2KeywordAc as usize,
245 P::Phase2SharedAc as usize,
246 P::Phase2AnchoredVerify as usize,
247 P::Phase2WholeChunk as usize,
248];
249const PHASE2_LEAVES: [usize; 9] = [
250 P::Hot as usize,
251 P::Confirmed as usize,
252 P::Phase2Prefilter as usize,
253 P::Phase2KeywordAc as usize,
254 P::Phase2SharedAc as usize,
255 P::Phase2AnchoredVerify as usize,
256 P::Phase2WholeChunk as usize,
257 P::Generic as usize,
258 P::Entropy as usize,
259];
260pub fn dump(label: &str) {
265 if !enabled() {
266 eprintln!("[profile {label}] scanner profile switch is off; no data");
267 return;
268 }
269 let (ns, calls, ns_decode, bytes, files) = read_reset();
270 let ms = |i: usize| ns[i] as f64 / 1e6;
271 let sum = |ids: &[usize]| ids.iter().map(|&i| ns[i]).sum::<u64>();
272
273 let phase2_ns = sum(&PHASE2_LEAVES) + ns[P::Ml as usize];
274 let capture_ns = sum(&PHASE2_CAPTURE_LEAVES);
275 let scan_ns = ns[P::Preprocess as usize]
276 + ns[P::Phase1Triggers as usize]
277 + ns[P::BackendDispatch as usize]
278 + phase2_ns
279 + ns[P::Decode as usize];
280 let scan_ms = scan_ns as f64 / 1e6;
281 let pct = |part: u64, whole: u64| {
282 if whole > 0 {
283 100.0 * part as f64 / whole as f64
284 } else {
285 0.0
286 }
287 };
288
289 eprintln!("=== keyhog profile [{label}] ===");
290 let thru = if scan_ms > 0.0 {
291 (bytes as f64 / 1e6) / (scan_ms / 1000.0)
292 } else {
293 0.0
294 };
295 eprintln!(
296 "SCAN {scan_ms:>9.1} ms summed across workers · {} files · {:.2} MiB · {:.1} MB/s (pass-time sum)",
297 files,
298 bytes as f64 / (1024.0 * 1024.0),
299 thru
300 );
301
302 let leaf = |i: usize, parent_ns: u64, indent: &str| {
303 let c = calls[i];
304 let dec = ns_decode[i];
305 eprintln!(
306 "{indent}{:<16} {:>9.1} ms {:>5.1}% parent {:>6.1}% scan calls={:<8} {:>6.0} ns/call decode={:>4.1}%",
307 NAMES[i],
308 ms(i),
309 pct(ns[i], parent_ns),
310 pct(ns[i], scan_ns),
311 c,
312 if c > 0 { ns[i] as f64 / c as f64 } else { 0.0 },
313 pct(dec, ns[i].max(1)),
314 );
315 };
316 let parent = |name: &str, total: u64, indent: &str| {
317 eprintln!(
318 "{indent}{:<16} {:>9.1} ms {:>5.1}% scan",
319 name,
320 total as f64 / 1e6,
321 pct(total, scan_ns),
322 );
323 };
324
325 let mark: crate::engine::phase2::MarkSnapshot = crate::engine::phase2::phase2_mark_stats();
330 let hs_split: crate::engine::phase2::HsMarkSplit =
333 crate::engine::phase2::hs_mark_timing_snapshot();
334
335 leaf(P::Preprocess as usize, scan_ns, " ");
336 leaf(P::Phase1Triggers as usize, scan_ns, " ");
337 leaf(P::BackendDispatch as usize, scan_ns, " ");
338 parent("phase2", phase2_ns, " ");
339 leaf(P::Hot as usize, phase2_ns, " ");
340 leaf(P::Confirmed as usize, phase2_ns, " ");
341 parent("phase2-capture", capture_ns, " ");
342 for &i in &PHASE2_CAPTURE_LEAVES {
343 leaf(i, capture_ns, " ");
344 if i == P::Phase2Prefilter as usize && mark.calls > 0 {
347 let line = crate::engine::phase2::format_mark_decomposition(&mark);
348 if mark.is_consistent() {
349 eprintln!(" ↳ {line}");
350 } else {
351 eprintln!(
357 " ↳ {line} ⚠ INCONSISTENT: gate-skip + hs + regexset ({}) != calls ({}), prefilter call accounting bug",
358 mark.gate_skips + mark.served_total(),
359 mark.calls
360 );
361 }
362 if hs_split.any_recorded() {
365 eprintln!(
366 " ↳ {}",
367 crate::engine::phase2::format_hs_mark_split(&hs_split)
368 );
369 }
370 }
371 }
372 leaf(P::Generic as usize, phase2_ns, " ");
373 leaf(P::Entropy as usize, phase2_ns, " ");
374 leaf(P::Ml as usize, phase2_ns, " ");
375 leaf(P::Decode as usize, scan_ns, " ");
376
377 let decode_total: u64 = (0..N).map(|i| ns_decode[i]).sum();
378 eprintln!(
379 " (of all leaf time, {:.1}% was recorded inside decode sub-chunk rescans)",
380 pct(decode_total, scan_ns),
381 );
382
383 crate::engine::scan_inner_profile::scan_inner_profile_dump();
386 crate::engine::scan_postprocess::decode_profile_dump();
387 crate::decode::extract_profile_dump();
388 crate::decode::decoder_profile_dump();
389 crate::engine::phase2_generic::generic_profile_dump();
390 crate::engine::scan_postprocess::ml_batch_profile_dump();
391 crate::gpu::ml_split_profile_dump();
392
393 crate::engine::phase2::phase2_mark_stats_reset();
397 crate::engine::phase2::hs_mark_timing_reset();
398}