use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering::Relaxed;
use std::sync::OnceLock;
use std::time::Duration;
pub(crate) fn confirmed_prof_enabled() -> bool {
super::profile::enabled()
}
static CONFIRMED_PAT_NS: OnceLock<Vec<AtomicU64>> = OnceLock::new();
static CONFIRMED_PAT_RUNS: OnceLock<Vec<AtomicU64>> = OnceLock::new();
static CONFIRMED_STAGE_NS: [AtomicU64; 4] = [
AtomicU64::new(0),
AtomicU64::new(0),
AtomicU64::new(0),
AtomicU64::new(0),
];
static CONFIRMED_STAGE_RUNS: [AtomicU64; 4] = [
AtomicU64::new(0),
AtomicU64::new(0),
AtomicU64::new(0),
AtomicU64::new(0),
];
#[derive(Clone, Copy)]
pub(crate) enum ConfirmedStage {
SuffixGate = 0,
AnchorCollect = 1,
Extract = 2,
CompanionGate = 3,
}
pub(crate) fn confirmed_prof_record(stage: ConfirmedStage, elapsed: Duration) {
let idx = stage as usize;
CONFIRMED_STAGE_NS[idx].fetch_add(elapsed.as_nanos() as u64, Relaxed);
CONFIRMED_STAGE_RUNS[idx].fetch_add(1, Relaxed);
}
pub(crate) fn confirmed_prof_stage_take() -> [(u64, u64); 4] {
std::array::from_fn(|idx| {
(
CONFIRMED_STAGE_NS[idx].swap(0, Relaxed),
CONFIRMED_STAGE_RUNS[idx].swap(0, Relaxed),
)
})
}
pub(crate) fn confirmed_prof_vecs(len: usize) -> (&'static [AtomicU64], &'static [AtomicU64]) {
let ns = CONFIRMED_PAT_NS.get_or_init(|| (0..len).map(|_| AtomicU64::new(0)).collect());
let runs = CONFIRMED_PAT_RUNS.get_or_init(|| (0..len).map(|_| AtomicU64::new(0)).collect());
(ns.as_slice(), runs.as_slice())
}
pub(crate) fn confirmed_prof_reset(len: usize) {
let (ns, runs) = confirmed_prof_vecs(len);
for n in ns {
n.store(0, Relaxed);
}
for r in runs {
r.store(0, Relaxed);
}
for n in &CONFIRMED_STAGE_NS {
n.store(0, Relaxed);
}
for r in &CONFIRMED_STAGE_RUNS {
r.store(0, Relaxed);
}
}
impl super::CompiledScanner {
pub(crate) fn confirmed_profile_dump(&self, label: &str) {
let total = self.ac_map.len() + self.phase2_patterns.len();
let (ns, runs) = confirmed_prof_vecs(total);
let mut rows: Vec<(usize, u64, u64)> = (0..total.min(ns.len()).min(runs.len()))
.map(|i| (i, ns[i].swap(0, Relaxed), runs[i].swap(0, Relaxed)))
.filter(|&(_, n, _)| n > 0)
.collect();
rows.sort_unstable_by(|a, b| b.1.cmp(&a.1));
let grand: u64 = rows.iter().map(|r| r.1).sum();
eprintln!(
"=== CONFIRMED per-pattern [{label}] total={:.1} ms over {} triggered patterns ===",
grand as f64 / 1e6,
rows.len()
);
let stages = confirmed_prof_stage_take();
let stage_total: u64 = stages.iter().map(|(ns, _)| *ns).sum();
if stage_total > 0 {
let labels = ["suffix-gate", "anchor-collect", "extract", "companion-gate"];
eprintln!(
"=== CONFIRMED stages [{label}] total={:.1} ms ===",
stage_total as f64 / 1e6
);
for (idx, name) in labels.iter().enumerate() {
let (ns, runs) = stages[idx];
if ns == 0 {
continue;
}
let per = if runs > 0 { ns / runs } else { 0 };
eprintln!(
" {:<15} {:>6.1}ms {:>5.1}% runs={:<6} {:>7}ns/run",
name,
ns as f64 / 1e6,
100.0 * ns as f64 / stage_total.max(1) as f64,
runs,
per
);
}
}
for (i, n, r) in rows.iter().take(30) {
let src = if *i < self.ac_map.len() {
self.ac_map[*i].regex.as_str()
} else {
self.phase2_patterns[*i - self.ac_map.len()]
.0
.regex
.as_str()
};
let per = if *r > 0 { *n / *r } else { 0 };
let s: String = src.chars().take(60).collect();
eprintln!(
" {:>6.1}ms {:>5.1}% runs={:<6} {:>7}ns/run {}",
*n as f64 / 1e6,
100.0 * *n as f64 / grand.max(1) as f64,
r,
per,
s
);
}
}
pub(crate) fn confirmed_profile_reset(&self) {
confirmed_prof_reset(self.ac_map.len() + self.phase2_patterns.len());
}
}
#[cfg(feature = "ml")]
use keyhog_profile::{CounterId, MetricId};
#[cfg(feature = "ml")]
pub(crate) struct MlBatchProfile {
pub calls: u64,
pub candidates: u64,
pub calls_ge64: u64,
pub candidates_ge64: u64,
pub buckets: Vec<(u64, u64, u64)>,
}
#[cfg(feature = "ml")]
pub(crate) fn ml_batch_record(n: usize) {
keyhog_profile::add_counter(CounterId::MlBatchCalls, 1);
keyhog_profile::add_counter(CounterId::MlBatchCandidates, n as u64);
if n >= 64 {
keyhog_profile::add_counter(CounterId::MlBatchCallsGe64, 1);
keyhog_profile::add_counter(CounterId::MlBatchCandidatesGe64, n as u64);
}
keyhog_profile::record_distribution(MetricId::MlBatchSize, n as u64);
}
#[cfg(feature = "ml")]
pub(crate) fn ml_batch_profile_from_parts(
metrics: &[keyhog_profile::TypedMetricRecordV2],
distributions: &[keyhog_profile::MetricDistributionV2],
) -> MlBatchProfile {
let value = |counter: CounterId| {
metrics
.iter()
.find(|record| record.metric_id == counter.metric_id())
.map_or(0, |record| record.value)
};
let buckets = distributions
.iter()
.find(|distribution| distribution.metric_id == MetricId::MlBatchSize)
.map(|distribution| {
distribution
.buckets
.iter()
.map(|bucket| (bucket.lower_bound, bucket.upper_bound, bucket.count))
.collect()
})
.unwrap_or_default();
MlBatchProfile {
calls: value(CounterId::MlBatchCalls),
candidates: value(CounterId::MlBatchCandidates),
calls_ge64: value(CounterId::MlBatchCallsGe64),
candidates_ge64: value(CounterId::MlBatchCandidatesGe64),
buckets,
}
}
#[cfg(feature = "ml")]
pub(crate) fn format_ml_batch_profile(p: &MlBatchProfile) -> String {
let mut out = format!(
"=== ML batch-size histogram: calls={} candidates={} (avg {:.1}/call) | \
CPU-parallel (>=64): {} calls ({:.1}%), {} candidates ({:.1}% of all ML work) ===",
p.calls,
p.candidates,
p.candidates as f64 / p.calls.max(1) as f64,
p.calls_ge64,
100.0 * p.calls_ge64 as f64 / p.calls.max(1) as f64,
p.candidates_ge64,
100.0 * p.candidates_ge64 as f64 / p.candidates.max(1) as f64,
);
for (lower, upper, count) in &p.buckets {
let label = if lower == upper {
format!("{lower}")
} else {
format!("{lower}-{upper}")
};
out.push_str(&format!("\n {label:>9}: {count}"));
}
out
}
#[cfg(feature = "decode")]
pub(crate) fn decode_recursion_from_typed(
metrics: &[keyhog_profile::TypedMetricRecordV2],
) -> (u64, u64, u64) {
let value = |counter: keyhog_profile::CounterId| {
metrics
.iter()
.find(|record| record.metric_id == counter.metric_id())
.map_or(0, |record| record.value)
};
(
value(keyhog_profile::CounterId::DecodeParentChunks),
value(keyhog_profile::CounterId::DecodeDerivedChunks),
value(keyhog_profile::CounterId::DecodeDerivedBytes),
)
}
#[cfg(feature = "decode")]
pub(crate) fn format_decode_recursion(
parents: u64,
subchunks: u64,
bytes: u64,
gen_ms: f64,
scan_ms: f64,
) -> String {
format!(
"decode-recursion: parents={parents} subchunks={subchunks} \
({:.1} sub/parent) bytes={bytes} gen={gen_ms:.1}ms scan={scan_ms:.1}ms \
({:.2} MB/s rescan)",
if parents > 0 {
subchunks as f64 / parents as f64
} else {
0.0
},
if scan_ms > 0.0 {
(bytes as f64 / 1e6) / (scan_ms / 1e3)
} else {
0.0
},
)
}