use criterion::{Criterion, black_box, criterion_group, criterion_main}; use keydr::engine::key_stats::KeyStatsStore; use keydr::engine::ngram_stats::{BigramKey, BigramStatsStore, extract_ngram_events}; use keydr::session::result::KeyTime; fn make_keystrokes(count: usize) -> Vec { let chars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; (0..count) .map(|i| KeyTime { key: chars[i % chars.len()], time_ms: 200.0 + (i % 50) as f64, correct: i % 7 != 0, // ~14% error rate }) .collect() } fn bench_extraction(c: &mut Criterion) { let keystrokes = make_keystrokes(500); c.bench_function("extract_bigrams (500 keystrokes)", |b| { b.iter(|| extract_ngram_events(black_box(&keystrokes), 800.0)) }); } fn bench_update(c: &mut Criterion) { let keystrokes = make_keystrokes(500); let bigram_events = extract_ngram_events(&keystrokes, 800.0); c.bench_function("bigram_stats update (400 events)", |b| { b.iter(|| { let mut store = BigramStatsStore::default(); for ev in bigram_events.iter().take(400) { store.update( black_box(ev.key.clone()), black_box(ev.total_time_ms), black_box(ev.correct), black_box(ev.has_hesitation), 0, ); } store }) }); } fn bench_focus_selection(c: &mut Criterion) { // Use a-z + A-Z + 0-9 = 62 chars for up to 3844 unique bigrams let all_chars: Vec = ('a'..='z').chain('A'..='Z').chain('0'..='9').collect(); let mut bigram_stats = BigramStatsStore::default(); let mut char_stats = KeyStatsStore::default(); // Char baselines: 5% error rate, 430ms per char. Expected independent bigram // error rate is therefore 1 - 0.95^2 = 0.0975, so a bigram needs an error_rate_ema // above ~0.146 to clear the 1.5x error-anomaly ratio threshold, and a // filtered_time_ms above ~645 to clear the 50% speed-anomaly threshold. for &ch in &all_chars { let stat = char_stats.stats.entry(ch).or_default(); stat.confidence = 0.8; stat.filtered_time_ms = 430.0; stat.sample_count = 50; stat.total_count = 50; stat.error_count = 3; stat.error_rate_ema = 0.05; } let mut count: usize = 0; for &a in &all_chars { for &b in &all_chars { if bigram_stats.stats.len() >= 3000 { break; } let key = BigramKey([a, b]); let stat = bigram_stats.stats.entry(key).or_default(); // Spread values so roughly half the entries clear each anomaly threshold, // exercising both the error and speed candidate paths. stat.error_rate_ema = 0.05 + (count % 20) as f64 * 0.01; stat.filtered_time_ms = 500.0 + (count % 40) as f64 * 10.0; stat.sample_count = 25 + count % 30; stat.error_count = 5 + count % 10; // Streak >= 3 confirms an anomaly; mix confirmed and unconfirmed entries. stat.error_anomaly_streak = if count % 3 == 0 { 3 } else { 1 }; stat.speed_anomaly_streak = if count % 4 == 0 { 3 } else { 0 }; count += 1; } } assert_eq!(bigram_stats.stats.len(), 3000); let unlocked: Vec = all_chars; c.bench_function("worst_confirmed_anomaly (3K entries)", |b| { b.iter(|| { bigram_stats.worst_confirmed_anomaly(black_box(&char_stats), black_box(&unlocked)) }) }); } fn bench_history_replay(c: &mut Criterion) { // Build 500 drills of ~300 keystrokes each let drills: Vec> = (0..500).map(|_| make_keystrokes(300)).collect(); c.bench_function("history replay (500 drills x 300 keystrokes)", |b| { b.iter(|| { let mut bigram_stats = BigramStatsStore::default(); let mut key_stats = KeyStatsStore::default(); for (drill_idx, keystrokes) in drills.iter().enumerate() { let bigram_events = extract_ngram_events(keystrokes, 800.0); for kt in keystrokes { if kt.correct { let stat = key_stats.stats.entry(kt.key).or_default(); stat.total_count += 1; } else { key_stats.update_key_error(kt.key); } } for ev in &bigram_events { bigram_stats.update( ev.key.clone(), ev.total_time_ms, ev.correct, ev.has_hesitation, drill_idx as u32, ); } } (bigram_stats, key_stats) }) }); } criterion_group!( benches, bench_extraction, bench_update, bench_focus_selection, bench_history_replay, ); criterion_main!(benches);