use criterion::{Criterion, black_box, criterion_group, criterion_main}; use keydr::engine::key_stats::KeyStatsStore; use keydr::engine::ngram_stats::{ BigramKey, BigramStatsStore, TrigramStatsStore, 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_ngram_events (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(); 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; } 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(); stat.confidence = 0.5 + (count % 50) as f64 * 0.01; stat.sample_count = 25 + count % 30; stat.error_count = 5 + count % 10; stat.redundancy_streak = if count % 3 == 0 { 3 } else { 1 }; count += 1; } } assert_eq!(bigram_stats.stats.len(), 3000); let unlocked: Vec = all_chars; c.bench_function("weakest_bigram (3K entries)", |b| { b.iter(|| { bigram_stats.weakest_bigram(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 trigram_stats = TrigramStatsStore::default(); let mut key_stats = KeyStatsStore::default(); for (drill_idx, keystrokes) in drills.iter().enumerate() { let (bigram_events, trigram_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, ); } for ev in &trigram_events { trigram_stats.update( ev.key.clone(), ev.total_time_ms, ev.correct, ev.has_hesitation, drill_idx as u32, ); } } (bigram_stats, trigram_stats, key_stats) }) }); } criterion_group!( benches, bench_extraction, bench_update, bench_focus_selection, bench_history_replay, ); criterion_main!(benches);