use keyhog_profile::Stage;
use std::cell::Cell;
const LEAVES: [Stage; 14] = [
Stage::Preprocess,
Stage::Phase1Triggers,
Stage::BackendDispatch,
Stage::HotPatterns,
Stage::ConfirmedPatterns,
Stage::Phase2Prefilter,
Stage::Phase2KeywordAc,
Stage::Phase2SharedAc,
Stage::Phase2AnchoredVerify,
Stage::Phase2WholeChunk,
Stage::GenericDetection,
Stage::Entropy,
Stage::MachineLearning,
Stage::Decode,
];
const N: usize = LEAVES.len();
fn leaf_index(stage: Stage) -> Option<usize> {
LEAVES.iter().position(|&leaf| leaf == stage)
}
pub(crate) fn enabled() -> bool {
keyhog_profile::detail().records_stages()
}
pub(crate) fn diagnostic() -> bool {
keyhog_profile::detail().is_diagnostic()
}
thread_local! {
static IN_DECODE: Cell<bool> = const { Cell::new(false) };
}
#[cfg(feature = "decode")]
pub(crate) fn set_in_decode(on: bool) -> bool {
let previous = IN_DECODE.with(|cell| cell.replace(on));
keyhog_profile::set_attribution(if on {
keyhog_profile::Attribution::Decoded
} else {
keyhog_profile::Attribution::Root
});
previous
}
#[inline]
pub(crate) fn in_decode() -> bool {
IN_DECODE.with(Cell::get)
}
pub(crate) type Guard = keyhog_profile::Span;
#[inline]
#[must_use]
pub(crate) fn span(stage: Stage) -> Guard {
keyhog_profile::span(stage)
}
pub(crate) fn add_bytes(bytes: u64) {
keyhog_profile::add_backend_dispatched_bytes(bytes);
}
pub(crate) fn add_derived_decoder_bytes(bytes: u64) {
keyhog_profile::add_derived_decoder_bytes(bytes);
}
const INCLUSIVE: [Stage; 2] = [Stage::BoundaryScan, Stage::AutorouteCalibration];
struct Drained {
ns: [u64; N],
calls: [u64; N],
ns_decode: [u64; N],
inclusive_ns: [u64; INCLUSIVE.len()],
inclusive_calls: [u64; INCLUSIVE.len()],
bytes: u64,
files: u64,
}
fn read_reset() -> Drained {
let mut drained = Drained {
ns: [0; N],
calls: [0; N],
ns_decode: [0; N],
inclusive_ns: [0; INCLUSIVE.len()],
inclusive_calls: [0; INCLUSIVE.len()],
bytes: 0,
files: 0,
};
for measurement in keyhog_profile::take_stage_measurements() {
if let Some(index) = leaf_index(measurement.stage) {
drained.ns[index] = measurement.elapsed_ns;
drained.calls[index] = measurement.calls;
drained.ns_decode[index] = measurement.attributed_ns;
} else if let Some(index) = INCLUSIVE.iter().position(|&s| s == measurement.stage) {
drained.inclusive_ns[index] = measurement.elapsed_ns;
drained.inclusive_calls[index] = measurement.calls;
}
}
let (bytes, files) = keyhog_profile::take_input_totals();
drained.bytes = bytes;
drained.files = files;
drained
}
pub fn reset() {
keyhog_profile::reset();
}
const fn row(stage: Stage) -> usize {
let mut index = 0;
while index < N {
if LEAVES[index] as usize == stage as usize {
return index;
}
index += 1;
}
panic!("dump grouping names a stage that is not a rendered leaf");
}
const PHASE2_CAPTURE_LEAVES: [usize; 5] = [
row(Stage::Phase2Prefilter),
row(Stage::Phase2KeywordAc),
row(Stage::Phase2SharedAc),
row(Stage::Phase2AnchoredVerify),
row(Stage::Phase2WholeChunk),
];
const PHASE2_LEAVES: [usize; 9] = [
row(Stage::HotPatterns),
row(Stage::ConfirmedPatterns),
row(Stage::Phase2Prefilter),
row(Stage::Phase2KeywordAc),
row(Stage::Phase2SharedAc),
row(Stage::Phase2AnchoredVerify),
row(Stage::Phase2WholeChunk),
row(Stage::GenericDetection),
row(Stage::Entropy),
];
pub fn dump(label: &str) {
if !enabled() {
eprintln!("[profile {label}] scanner profile switch is off; no data");
return;
}
let Drained {
ns,
calls,
ns_decode,
inclusive_ns,
inclusive_calls,
bytes,
files,
} = read_reset();
let ms = |i: usize| ns[i] as f64 / 1e6;
let sum = |ids: &[usize]| ids.iter().map(|&i| ns[i]).sum::<u64>();
let phase2_ns = sum(&PHASE2_LEAVES) + ns[row(Stage::MachineLearning)];
let capture_ns = sum(&PHASE2_CAPTURE_LEAVES);
let scan_ns = ns[row(Stage::Preprocess)]
+ ns[row(Stage::Phase1Triggers)]
+ ns[row(Stage::BackendDispatch)]
+ phase2_ns
+ ns[row(Stage::Decode)];
let scan_ms = scan_ns as f64 / 1e6;
let pct = |part: u64, whole: u64| {
if whole > 0 {
100.0 * part as f64 / whole as f64
} else {
0.0
}
};
eprintln!("=== keyhog profile [{label}] ===");
let thru = if scan_ms > 0.0 {
(bytes as f64 / 1e6) / (scan_ms / 1000.0)
} else {
0.0
};
eprintln!(
"SCAN {scan_ms:>9.1} ms summed across workers · {} files · {:.2} MiB · {:.1} MB/s (pass-time sum)",
files,
bytes as f64 / (1024.0 * 1024.0),
thru
);
let leaf = |i: usize, parent_ns: u64, indent: &str| {
let c = calls[i];
let dec = ns_decode[i];
eprintln!(
"{indent}{:<24} {:>9.1} ms {:>5.1}% parent {:>6.1}% scan calls={:<8} {:>6.0} ns/call decode={:>4.1}%",
LEAVES[i].as_str(),
ms(i),
pct(ns[i], parent_ns),
pct(ns[i], scan_ns),
c,
if c > 0 { ns[i] as f64 / c as f64 } else { 0.0 },
pct(dec, ns[i].max(1)),
);
};
let parent = |name: &str, total: u64, indent: &str| {
eprintln!(
"{indent}{:<24} {:>9.1} ms {:>5.1}% scan",
name,
total as f64 / 1e6,
pct(total, scan_ns),
);
};
let typed = keyhog_profile::take_typed_metrics();
#[cfg(feature = "ml")]
let distributions = keyhog_profile::take_metric_distributions();
let mark: crate::engine::phase2::MarkSnapshot =
crate::engine::phase2::mark_snapshot_from_typed(&typed);
let hs_split: crate::engine::phase2::HsMarkSplit =
crate::engine::phase2::hs_mark_split_from_typed(&typed);
leaf(row(Stage::Preprocess), scan_ns, " ");
leaf(row(Stage::Phase1Triggers), scan_ns, " ");
leaf(row(Stage::BackendDispatch), scan_ns, " ");
parent("phase2", phase2_ns, " ");
leaf(row(Stage::HotPatterns), phase2_ns, " ");
leaf(row(Stage::ConfirmedPatterns), phase2_ns, " ");
parent("phase2-capture", capture_ns, " ");
for &i in &PHASE2_CAPTURE_LEAVES {
leaf(i, capture_ns, " ");
if i == row(Stage::Phase2Prefilter) && mark.calls > 0 {
let line = crate::engine::phase2::format_mark_decomposition(&mark);
if mark.is_consistent() {
eprintln!(" ↳ {line}");
} else {
eprintln!(
" ↳ {line} ⚠ INCONSISTENT: gate-skip + hs + regexset ({}) != calls ({}), prefilter call accounting bug",
mark.gate_skips + mark.served_total(),
mark.calls
);
}
if hs_split.any_recorded() {
eprintln!(
" ↳ {}",
crate::engine::phase2::format_hs_mark_split(&hs_split)
);
}
}
}
leaf(row(Stage::GenericDetection), phase2_ns, " ");
leaf(row(Stage::Entropy), phase2_ns, " ");
leaf(row(Stage::MachineLearning), phase2_ns, " ");
leaf(row(Stage::Decode), scan_ns, " ");
let decode_total: u64 = (0..N).map(|i| ns_decode[i]).sum();
eprintln!(
" (of all leaf time, {:.1}% was recorded inside decode sub-chunk rescans)",
pct(decode_total, scan_ns),
);
for (index, stage) in INCLUSIVE.iter().enumerate() {
let stage_ns = inclusive_ns[index];
if stage_ns == 0 {
continue;
}
eprintln!(
" of which {:<22} {:>9.1} ms {:>5.1}% scan calls={} (inclusive of the leaves inside it)",
stage.as_str(),
stage_ns as f64 / 1e6,
pct(stage_ns, scan_ns),
inclusive_calls[index],
);
}
#[cfg(feature = "decode")]
{
let (parents, subchunks, derived_bytes) =
crate::engine::scan_postprocess::decode_recursion_from_typed(&typed);
let gen_ms = ns[row(Stage::Decode)] as f64 / 1e6;
let scan_ms = decode_total as f64 / 1e6;
if parents != 0 || subchunks != 0 || derived_bytes != 0 || gen_ms != 0.0 || scan_ms != 0.0 {
eprintln!(
"{}",
crate::engine::scan_postprocess::format_decode_recursion(
parents,
subchunks,
derived_bytes,
gen_ms,
scan_ms,
)
);
}
let (extract_calls, extract_bytes, extract_ns) =
crate::decode::extract_profile_from_typed(&typed);
if extract_calls != 0 || extract_bytes != 0 || extract_ns != 0 {
eprintln!(
"{}",
crate::decode::format_extract_profile(extract_calls, extract_bytes, extract_ns)
);
}
}
crate::decode::decoder_profile_dump();
let generic = crate::engine::phase2_generic::generic_profile_from_typed(&typed);
if generic.any_recorded() {
eprintln!(
"{}",
crate::engine::phase2_generic::format_generic_profile(&generic)
);
}
#[cfg(feature = "ml")]
{
let batch =
crate::engine::scan_postprocess::ml_batch_profile_from_parts(&typed, &distributions);
if batch.calls != 0 {
eprintln!(
"{}",
crate::engine::scan_postprocess::format_ml_batch_profile(&batch)
);
}
}
let (feature_ns, score_ns) = crate::gpu::ml_split_from_typed(&typed);
if feature_ns != 0 || score_ns != 0 {
eprintln!("{}", crate::gpu::format_ml_split(feature_ns, score_ns));
}
}