use std::time::Instant;
use regex_automata::Input;
use regex_automata::dfa::{Automaton, dense};
use regex_automata::nfa::thompson;
use regex_automata::util::syntax;
use sheng::Sieve;
mod common;
const PATTERNS: &[&str] = &[
r"(?-u)WalletService",
r"(?-u)foo[^\n]*bar",
r"(?-u)a[^\n]*b",
r"(?-u)a[^\n]*b[^\n]*c",
r"(?-u)<[^>]*>",
r#"(?-u)"[^"]*;"#,
r"(?-u)\{[^\n]*\}",
r"(?-u)(alpha|beta|gamma)",
r"(?-u)[0-9]{3}-[0-9]{4}",
r"(?-u)ab+c",
r"(?-u)[A-Z][a-z]+Service",
r"(?-u)#[0-9a-fA-F]{6}",
r"(?-u)panic!\(",
];
const ROUNDS: usize = 5;
const SAMPLES: usize = 5;
const JUDGEABLE: usize = 8 << 20;
fn matcher(pattern: &str) -> dense::DFA<Vec<u32>> {
dense::Builder::new()
.syntax(syntax::Config::new().utf8(false))
.thompson(thompson::Config::new().utf8(false))
.build(pattern)
.expect("pattern builds")
}
fn accel(dfa: &dense::DFA<Vec<u32>>) -> String {
let Ok(start) = dfa.start_state_forward(&Input::new(b"")) else {
return "?".to_string();
};
match dfa.accelerator(start) {
[] => "-".to_string(),
bytes => String::from_utf8_lossy(bytes).escape_debug().to_string(),
}
}
fn matches(dfa: &dense::DFA<Vec<u32>>, hay: &[u8]) -> bool {
dfa.try_search_fwd(&Input::new(hay))
.expect("no quit bytes")
.is_some()
}
fn main() {
let docs = common::corpus_files(3000);
let bytes: usize = docs.iter().map(Vec::len).sum();
println!(
"{} documents · {:.1} MiB · {SAMPLES} samples of min-of-{ROUNDS}\n",
docs.len(),
bytes as f64 / (1 << 20) as f64
);
println!(
"{:<28} {:>9} {:>3} {:>8} {:>10} {:>10} {:>11} {:>15}",
"pattern", "armed", "#q", "accel", "engine", "sieved", "end to end", "interval"
);
let policy = sheng::Policy {
skip: std::env::var_os("SHENG_NO_SKIP").is_none(),
..sheng::Policy::default()
};
println!("skip kernel: {}", if policy.skip { "on" } else { "off" });
let mut armed: Vec<Row> = Vec::new();
for pattern in PATTERNS {
let dfa = matcher(pattern);
let sieve = match Sieve::with(pattern, &policy) {
Ok(s) => s,
Err(why) => {
println!(
"{pattern:<28} {:>9} {:>3} {:>8} declined: {why}",
"-",
"-",
accel(&dfa)
);
continue;
},
};
let engine = time(|| docs.iter().filter(|d| matches(&dfa, d)).count());
let sieved = time(|| {
docs.iter()
.filter(|d| !sieve.refutes(d) && matches(&dfa, d))
.count()
});
let row = Row {
pattern,
engine: engine.best,
sieved: sieved.best,
ratio: engine.best / sieved.best,
kindest: engine.worst / sieved.best,
harshest: engine.best / sieved.worst,
};
println!(
"{pattern:<28} {:>9.3} {:>3} {:>8} {:>8.2}ms {:>8.2}ms {:>10.3}x {:>7.3}-{:.3}x",
sieve.fallthrough(),
sieve.conjuncts(),
accel(&dfa),
engine.best * 1e3,
sieved.best * 1e3,
row.ratio,
row.harshest,
row.kindest
);
armed.push(row);
}
assert!(
!armed.is_empty(),
"no pattern armed — the cost gate has closed entirely"
);
let geo = (armed.iter().map(|r| r.ratio.ln()).sum::<f64>() / armed.len() as f64).exp();
println!(
"\ngeomean end to end over {} armed patterns: {geo:.3}x",
armed.len()
);
if bytes < JUDGEABLE {
println!(
"no verdict: {:.1} MiB is under the {} MiB this needs to judge anything. A \
corpus this small is cache-resident, so the ratios above price the engine's \
accelerator against L2 bandwidth rather than the memory the calibration was \
minted over. Aim $SHENG_CORPUS at a real tree.",
bytes as f64 / (1 << 20) as f64,
JUDGEABLE >> 20
);
return;
}
let lost: Vec<&Row> = armed.iter().filter(|r| r.kindest < 1.0).collect();
assert!(
lost.is_empty(),
"these rows armed and then lost by more than the clock could account for, so a \
coefficient in `price::ACTIVE` is too generous:\n{}",
lost.iter()
.map(|r| format!(
" {:<28} {:.3}x (interval {:.3}-{:.3}x, arms {:.2}ms vs {:.2}ms)",
r.pattern,
r.ratio,
r.harshest,
r.kindest,
r.engine * 1e3,
r.sieved * 1e3
))
.collect::<Vec<_>>()
.join("\n")
);
let unsettled: Vec<&Row> = armed.iter().filter(|r| r.harshest < 1.0).collect();
if unsettled.is_empty() {
println!("every armed row cleared 1.000x — the gate's predictions held.");
return;
}
println!(
"{} of {} armed rows are undecided — their two arms overlap, so this run cannot \
say which is faster:",
unsettled.len(),
armed.len()
);
for row in &unsettled {
println!(
" {:<28} {:.3}-{:.3}x over arms of {:.2}ms and {:.2}ms",
row.pattern,
row.harshest,
row.kindest,
row.engine * 1e3,
row.sieved * 1e3
);
}
println!(
"Every other row above is decided. An undecided row is usually one the engine's \
own accelerator already finishes too fast to time against {:.1} MiB; give it \
more bytes with $SHENG_CORPUS, or an idler machine, before reading anything \
into its ratio.",
bytes as f64 / (1 << 20) as f64
);
}
#[derive(Debug)]
struct Row {
pattern: &'static str,
engine: f64,
sieved: f64,
ratio: f64,
kindest: f64,
harshest: f64,
}
struct Reading {
best: f64,
worst: f64,
}
fn time(mut run: impl FnMut() -> usize) -> Reading {
let want = run();
let mut samples = [f64::MAX; SAMPLES];
for sample in &mut samples {
for _ in 0..ROUNDS {
let t = Instant::now();
let got = run();
*sample = sample.min(t.elapsed().as_secs_f64());
assert_eq!(got, want, "the two arms disagree — not an optimization");
}
}
Reading {
best: samples.iter().copied().fold(f64::MAX, f64::min),
worst: samples.iter().copied().fold(0.0, f64::max),
}
}