use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}; use fuzzy::CharBag; use std::sync::atomic::AtomicBool; use util::{paths::PathStyle, rel_path::RelPath}; const DIRS: &[&str] = &[ "src", "crates/gpui/src", "crates/editor/src", "crates/fuzzy_nucleo/src", "crates/workspace/src", "crates/project/src", "crates/language/src", "crates/terminal/src", "crates/assistant/src", "crates/theme/src", "tests/integration", "tests/unit", "docs/architecture", "scripts", "assets/icons", "assets/fonts", "crates/git/src", "crates/rpc/src", "crates/settings/src", "crates/diagnostics/src", "crates/search/src", "crates/collab/src", "crates/db/src", "crates/lsp/src", ]; const FILENAMES: &[&str] = &[ "parser.rs", "main.rs", "executor.rs", "editor.rs", "strings.rs", "workspace.rs", "project.rs", "buffer.rs", "colors.rs", "panel.rs", "renderer.rs", "dispatcher.rs", "matcher.rs", "paths.rs", "context.rs", "toolbar.rs", "statusbar.rs", "keymap.rs", "config.rs", "settings.rs", "diagnostics.rs", "completion.rs", "hover.rs", "references.rs", "inlay_hints.rs", "git_blame.rs", "terminal.rs", "search.rs", "replace.rs", "outline.rs", "breadcrumbs.rs", "tab_bar.rs", "Cargo.toml", "README.md", "build.sh", "LICENSE", "overview.md", "string_helpers.rs", "test_helpers.rs", "fixtures.json", "schema.sql", ]; const QUERY_WORDS: &[&str] = &[ "par", "edi", "buf", "set", "mat", "con", "ren", "dis", "sea", "ter", "col", "hov", "out", "rep", "key", "too", "pan", "str", "dia", "com", "executor", "workspace", "settings", "terminal", "breadcrumbs", "git_blame", "fixtures", "schema", "config", "toolbar", ]; /// Deterministic query generation from QUERY_WORDS using a simple LCG. /// Returns `count` queries of each arity: 1, 2, and 4 space-separated words. fn generate_queries(count: usize) -> (Vec, Vec, Vec) { let mut state: u64 = 0xDEAD_BEEF; let mut next = || -> usize { // LCG: simple, fast, deterministic state = state.wrapping_mul(6364136223846793005).wrapping_add(1); (state >> 33) as usize }; let mut n_word = |n: usize| -> Vec { (0..count) .map(|_| { (0..n) .map(|_| QUERY_WORDS[next() % QUERY_WORDS.len()]) .collect::>() .join(" ") }) .collect() }; (n_word(1), n_word(2), n_word(4)) } fn generate_candidates(count: usize) -> Vec { (0..count) .map(|id| { let dir = DIRS[id % DIRS.len()]; let file = FILENAMES[id / DIRS.len() % FILENAMES.len()]; fuzzy_nucleo::StringMatchCandidate::new(id, &format!("{dir}/{file}")) }) .collect() } fn to_fuzzy_candidates( candidates: &[fuzzy_nucleo::StringMatchCandidate], ) -> Vec { candidates .iter() .map(|c| fuzzy::StringMatchCandidate::new(c.id, c.string.as_ref())) .collect() } fn bench_string_matching(criterion: &mut Criterion) { let cancel = AtomicBool::new(false); let dispatcher = std::sync::Arc::new(gpui::TestDispatcher::new(0)); let background_executor = gpui::BackgroundExecutor::new(dispatcher.clone()); let foreground_executor = gpui::ForegroundExecutor::new(dispatcher); let sizes = [100, 1000, 10_000]; let query_count = 200; let (q1, q2, q4) = generate_queries(query_count); for (label, queries) in [("1-word", &q1), ("2-word", &q2), ("4-word", &q4)] { let mut group = criterion.benchmark_group(label); for size in sizes { let candidates = generate_candidates(size); let fuzzy_candidates = to_fuzzy_candidates(&candidates); let mut query_idx = 0usize; group.bench_function(BenchmarkId::new("nucleo", size), |b| { b.iter_batched( || { let query = queries[query_idx % queries.len()].as_str(); query_idx += 1; query }, |query| { foreground_executor.block_on(fuzzy_nucleo::match_strings_async( &candidates, query, fuzzy_nucleo::Case::Ignore, fuzzy_nucleo::LengthPenalty::On, size, &cancel, background_executor.clone(), )) }, BatchSize::SmallInput, ) }); let mut query_idx = 0usize; group.bench_function(BenchmarkId::new("fuzzy", size), |b| { b.iter_batched( || { let query = queries[query_idx % queries.len()].as_str(); query_idx += 1; query }, |query| { foreground_executor.block_on(fuzzy::match_strings( &fuzzy_candidates, query, false, true, size, &cancel, background_executor.clone(), )) }, BatchSize::SmallInput, ) }); } group.finish(); } } fn generate_path_strings(count: usize) -> &'static [String] { let paths: Box<[String]> = (0..count) .map(|id| { let dir = DIRS[id % DIRS.len()]; let file = FILENAMES[id / DIRS.len() % FILENAMES.len()]; format!("{dir}/{file}") }) .collect(); Box::leak(paths) } fn generate_nucleo_path_candidates( paths: &'static [String], ) -> Vec> { paths .iter() .map(|path| { fuzzy_nucleo::PathMatchCandidate::new(RelPath::unix(path).unwrap(), false, None) }) .collect() } fn generate_fuzzy_path_candidates( paths: &'static [String], ) -> Vec> { paths .iter() .map(|path| fuzzy::PathMatchCandidate { is_dir: false, path: RelPath::unix(path).unwrap(), char_bag: CharBag::from(path.as_str()), }) .collect() } fn capitalize_each_word(query: &str) -> String { query .split_whitespace() .map(|w| { let mut chars = w.chars(); match chars.next() { Some(c) => c.to_ascii_uppercase().to_string() + chars.as_str(), None => String::new(), } }) .collect::>() .join(" ") } fn bench_path_matching(criterion: &mut Criterion) { let sizes = [100, 1000, 10_000]; let all_path_strings = sizes.map(generate_path_strings); let query_count = 200; let (q1, q2, q4) = generate_queries(query_count); let q1_upper: Vec = q1.iter().map(|q| capitalize_each_word(q)).collect(); let q2_upper: Vec = q2.iter().map(|q| capitalize_each_word(q)).collect(); let q4_upper: Vec = q4.iter().map(|q| capitalize_each_word(q)).collect(); for (label, queries, case) in [ ("path/1-word", &q1, fuzzy_nucleo::Case::Ignore), ("path/2-word", &q2, fuzzy_nucleo::Case::Ignore), ("path/4-word", &q4, fuzzy_nucleo::Case::Ignore), ("path_smart/1-word", &q1_upper, fuzzy_nucleo::Case::Smart), ("path_smart/2-word", &q2_upper, fuzzy_nucleo::Case::Smart), ("path_smart/4-word", &q4_upper, fuzzy_nucleo::Case::Smart), ] { let mut group = criterion.benchmark_group(label); for (size_index, &size) in sizes.iter().enumerate() { let path_strings = all_path_strings[size_index]; let mut query_idx = 0usize; group.bench_function(BenchmarkId::new("nucleo", size), |b| { b.iter_batched( || { let query = queries[query_idx % queries.len()].as_str(); query_idx += 1; (generate_nucleo_path_candidates(path_strings), query) }, |(candidates, query)| { fuzzy_nucleo::match_fixed_path_set( candidates, 0, None, query, case, size, PathStyle::Posix, ) }, BatchSize::SmallInput, ) }); let mut query_idx = 0usize; group.bench_function(BenchmarkId::new("fuzzy", size), |b| { b.iter_batched( || { let query = queries[query_idx % queries.len()].as_str(); query_idx += 1; (generate_fuzzy_path_candidates(path_strings), query) }, |(candidates, query)| { fuzzy::match_fixed_path_set( candidates, 0, None, query, false, size, PathStyle::Posix, ) }, BatchSize::SmallInput, ) }); } group.finish(); } } criterion_group!(benches, bench_string_matching, bench_path_matching); criterion_main!(benches);