use criterion::{black_box, criterion_group, criterion_main, Criterion}; use tagtree::{batch_rename, merge_tags, remove_subtree, rename_prefix_bulk, TagConfig, TagIndex}; const AF_CONFIG: TagConfig = TagConfig { max_depth: 5, max_length: 100, semantic_depth: 0, }; const GO_CONFIG: TagConfig = TagConfig { max_depth: 3, max_length: 60, semantic_depth: 0, }; fn sample_tags() -> Vec { vec![ "genre", "genre.electronic", "genre.electronic.house", "genre.electronic.techno", "genre.electronic.ambient", "genre.rock", "genre.rock.indie", "genre.rock.post-punk", "genre.jazz", "genre.jazz.bebop", "genre.jazz.fusion", "mood", "mood.energetic", "mood.calm", "mood.dark", "source", "source.vinyl", "source.digital", "source.tape", "instrument", "instrument.guitar", "instrument.synth", "instrument.drums", "instrument.bass", "era", "era.60s", "era.70s", "era.80s", "era.90s", "era.2000s", "bpm.slow", "bpm.medium", "bpm.fast", "key.c-major", "key.a-minor", "key.d-major", "quality.high", "quality.medium", "quality.low", "format.wav", "format.flac", "format.mp3", "format.aiff", ] .into_iter() .map(String::from) .collect() } fn bench_validate(c: &mut Criterion) { let mut group = c.benchmark_group("validate"); group.bench_function("simple_tag", |b| { b.iter(|| tagtree::validate_with(black_box("genre"), &AF_CONFIG)); }); group.bench_function("deep_tag", |b| { b.iter(|| { tagtree::validate_with(black_box("genre.electronic.house"), &AF_CONFIG) }); }); group.bench_function("max_depth_tag", |b| { b.iter(|| { tagtree::validate_with(black_box("a.b.c.d.e"), &AF_CONFIG) }); }); group.bench_function("invalid_tag", |b| { b.iter(|| tagtree::validate_with(black_box("UPPER.case"), &AF_CONFIG)); }); group.bench_function("long_tag", |b| { let tag = "abcdefghij.abcdefghij.abcdefghij.abcdefghij.abcdefghij"; b.iter(|| tagtree::validate_with(black_box(tag), &AF_CONFIG)); }); group.bench_function("go_config", |b| { b.iter(|| tagtree::validate_with(black_box("work.meeting"), &GO_CONFIG)); }); group.finish(); } fn bench_hierarchy(c: &mut Criterion) { let mut group = c.benchmark_group("hierarchy"); group.bench_function("parent", |b| { b.iter(|| tagtree::parent(black_box("genre.electronic.house"))); }); group.bench_function("leaf", |b| { b.iter(|| tagtree::leaf(black_box("genre.electronic.house"))); }); group.bench_function("depth", |b| { b.iter(|| tagtree::depth(black_box("genre.electronic.house"))); }); group.bench_function("ancestors", |b| { b.iter(|| tagtree::ancestors(black_box("genre.electronic.house"))); }); group.bench_function("is_ancestor_of", |b| { b.iter(|| { tagtree::is_ancestor_of( black_box("genre.electronic"), black_box("genre.electronic.house"), ) }); }); group.bench_function("common_ancestor", |b| { b.iter(|| { tagtree::common_ancestor( black_box("genre.electronic.house"), black_box("genre.electronic.techno"), ) }); }); group.finish(); } fn bench_sql(c: &mut Criterion) { let mut group = c.benchmark_group("sql"); group.bench_function("escape_like", |b| { b.iter(|| tagtree::escape_like(black_box("genre%_electronic"))); }); group.bench_function("like_descendant_pattern", |b| { b.iter(|| tagtree::like_descendant_pattern(black_box("genre.electronic"))); }); group.finish(); } fn bench_tree_ops(c: &mut Criterion) { let tags = sample_tags(); let mut group = c.benchmark_group("tree_ops"); group.bench_function("children_at_prefix", |b| { b.iter(|| tagtree::children_at_prefix(black_box("genre"), &tags)); }); group.bench_function("subtree", |b| { b.iter(|| tagtree::subtree(black_box("genre"), &tags)); }); group.bench_function("rename_prefix", |b| { b.iter(|| { tagtree::rename_prefix( black_box("genre.electronic"), black_box("style.electronic"), black_box("genre.electronic.house"), ) }); }); group.finish(); } fn bench_tag_index(c: &mut Criterion) { let tags = sample_tags(); let mut group = c.benchmark_group("tag_index"); group.bench_function("build_index", |b| { b.iter(|| TagIndex::new(black_box(tags.clone()))); }); let index = TagIndex::new(tags); group.bench_function("suggest_prefix", |b| { b.iter(|| index.suggest(black_box("gen"), 5)); }); group.bench_function("suggest_path", |b| { b.iter(|| index.suggest(black_box("genre.e"), 5)); }); group.bench_function("suggest_exact", |b| { b.iter(|| index.suggest(black_box("genre.electronic"), 5)); }); group.bench_function("suggest_no_match", |b| { b.iter(|| index.suggest(black_box("zzz"), 5)); }); group.bench_function("suggest_fuzzy_typo", |b| { b.iter(|| index.suggest_fuzzy(black_box("genr"), 5)); }); group.bench_function("suggest_fuzzy_no_match", |b| { b.iter(|| index.suggest_fuzzy(black_box("zzz"), 5)); }); group.finish(); } // --------------------------------------------------------------------------- // Large-scale benchmarks (1K, 10K, 50K tags) // --------------------------------------------------------------------------- /// Generate a hierarchical tag set of approximately `target` tags. /// /// Structure: `{root}.{mid}.{leaf}` with configurable fan-out. fn generate_tags(target: usize) -> Vec { let roots = [ "genre", "mood", "source", "instrument", "era", "bpm", "key", "quality", "format", "region", "label", "artist", "album", "technique", "effect", "tempo", "scale", "texture", "timbre", "dynamic", ]; let mut tags = Vec::with_capacity(target); // How many children per node needed to reach target // ~20 roots * mid * leaf ≈ target, so mid ≈ sqrt(target / 20) let fan = ((target as f64 / roots.len() as f64).sqrt().ceil() as usize).max(2); for root in &roots { tags.push(root.to_string()); for m in 0..fan { let mid = format!("{root}.sub-{m}"); tags.push(mid.clone()); for l in 0..fan { tags.push(format!("{mid}.leaf-{l}")); if tags.len() >= target { return tags; } } } } tags } fn bench_large_tree_ops(c: &mut Criterion) { let tags_1k = generate_tags(1_000); let tags_10k = generate_tags(10_000); let tags_50k = generate_tags(50_000); let mut group = c.benchmark_group("large_tree_ops"); // children_at_prefix — scans the full tag list group.bench_function("children_at_prefix/1k", |b| { b.iter(|| tagtree::children_at_prefix(black_box("genre"), &tags_1k)); }); group.bench_function("children_at_prefix/10k", |b| { b.iter(|| tagtree::children_at_prefix(black_box("genre"), &tags_10k)); }); group.bench_function("children_at_prefix/50k", |b| { b.iter(|| tagtree::children_at_prefix(black_box("genre"), &tags_50k)); }); // subtree — scans for prefix matches group.bench_function("subtree/1k", |b| { b.iter(|| tagtree::subtree(black_box("genre"), &tags_1k)); }); group.bench_function("subtree/10k", |b| { b.iter(|| tagtree::subtree(black_box("genre"), &tags_10k)); }); group.bench_function("subtree/50k", |b| { b.iter(|| tagtree::subtree(black_box("genre"), &tags_50k)); }); group.finish(); } fn bench_large_tag_index(c: &mut Criterion) { let tags_1k = generate_tags(1_000); let tags_10k = generate_tags(10_000); let tags_50k = generate_tags(50_000); let mut group = c.benchmark_group("large_tag_index"); // Build time group.bench_function("build/1k", |b| { b.iter(|| TagIndex::new(black_box(tags_1k.clone()))); }); group.bench_function("build/10k", |b| { b.iter(|| TagIndex::new(black_box(tags_10k.clone()))); }); group.bench_function("build/50k", |b| { b.iter(|| TagIndex::new(black_box(tags_50k.clone()))); }); // Suggest — build once, query many let idx_1k = TagIndex::new(tags_1k); let idx_10k = TagIndex::new(tags_10k); let idx_50k = TagIndex::new(tags_50k); group.bench_function("suggest/1k", |b| { b.iter(|| idx_1k.suggest(black_box("genre.sub"), 10)); }); group.bench_function("suggest/10k", |b| { b.iter(|| idx_10k.suggest(black_box("genre.sub"), 10)); }); group.bench_function("suggest/50k", |b| { b.iter(|| idx_50k.suggest(black_box("genre.sub"), 10)); }); // Fuzzy suggest at scale group.bench_function("suggest_fuzzy/1k", |b| { b.iter(|| idx_1k.suggest_fuzzy(black_box("genr"), 10)); }); group.bench_function("suggest_fuzzy/10k", |b| { b.iter(|| idx_10k.suggest_fuzzy(black_box("genr"), 10)); }); group.bench_function("suggest_fuzzy/50k", |b| { b.iter(|| idx_50k.suggest_fuzzy(black_box("genr"), 10)); }); // Fuzzy worst case: no match, must scan all segments group.bench_function("suggest_fuzzy_miss/10k", |b| { b.iter(|| idx_10k.suggest_fuzzy(black_box("zzz"), 10)); }); // Worst case: short prefix that matches many tags group.bench_function("suggest_broad/1k", |b| { b.iter(|| idx_1k.suggest(black_box("g"), 10)); }); group.bench_function("suggest_broad/10k", |b| { b.iter(|| idx_10k.suggest(black_box("g"), 10)); }); group.bench_function("suggest_broad/50k", |b| { b.iter(|| idx_50k.suggest(black_box("g"), 10)); }); group.finish(); } fn bench_large_validate(c: &mut Criterion) { let tags_10k = generate_tags(10_000); let mut group = c.benchmark_group("large_validate"); // Validate every tag in a 10K set group.bench_function("validate_all/10k", |b| { b.iter(|| { for tag in &tags_10k { let _ = tagtree::validate_with(black_box(tag), &AF_CONFIG); } }); }); group.finish(); } // --------------------------------------------------------------------------- // Deep tree benchmarks (max depth stress test) // --------------------------------------------------------------------------- /// Generate a single tag chain of depth `n`: "a.b.c.d.e.f..." fn deep_tag(depth: usize) -> String { (0..depth) .map(|i| format!("seg-{i}")) .collect::>() .join(".") } /// Generate a tree where every node has `fan` children, up to `depth` levels. fn generate_deep_tree(depth: usize, fan: usize) -> Vec { let mut tags = Vec::new(); let mut stack: Vec = vec!["root".to_string()]; while let Some(prefix) = stack.pop() { tags.push(prefix.clone()); let d = prefix.chars().filter(|&c| c == '.').count() + 1; if d < depth { for i in 0..fan { stack.push(format!("{prefix}.child-{i}")); } } } tags } fn bench_deep_tree(c: &mut Criterion) { // Deep validation config (allows up to 20 levels) const DEEP_CONFIG: TagConfig = TagConfig { max_depth: 20, max_length: 500, semantic_depth: 0, }; let tag_5 = deep_tag(5); let tag_10 = deep_tag(10); let tag_20 = deep_tag(20); let mut group = c.benchmark_group("deep_tree"); // Validate deep tags group.bench_function("validate/depth-5", |b| { b.iter(|| tagtree::validate_with(black_box(&tag_5), &DEEP_CONFIG)); }); group.bench_function("validate/depth-10", |b| { b.iter(|| tagtree::validate_with(black_box(&tag_10), &DEEP_CONFIG)); }); group.bench_function("validate/depth-20", |b| { b.iter(|| tagtree::validate_with(black_box(&tag_20), &DEEP_CONFIG)); }); // Ancestors at deep levels group.bench_function("ancestors/depth-5", |b| { b.iter(|| tagtree::ancestors(black_box(&tag_5))); }); group.bench_function("ancestors/depth-10", |b| { b.iter(|| tagtree::ancestors(black_box(&tag_10))); }); group.bench_function("ancestors/depth-20", |b| { b.iter(|| tagtree::ancestors(black_box(&tag_20))); }); // is_ancestor_of with deep tags group.bench_function("is_ancestor/depth-20", |b| { let ancestor = &deep_tag(3); b.iter(|| tagtree::is_ancestor_of(black_box(ancestor), black_box(&tag_20))); }); // Deep wide tree: depth 6, fan 5 = ~19K tags let deep_wide = generate_deep_tree(6, 5); let deep_wide_len = deep_wide.len(); group.bench_function(format!("children_at_prefix/{deep_wide_len}_tags"), |b| { b.iter(|| tagtree::children_at_prefix(black_box("root"), &deep_wide)); }); group.bench_function(format!("subtree/{deep_wide_len}_tags"), |b| { b.iter(|| tagtree::subtree(black_box("root"), &deep_wide)); }); // TagIndex with deep wide tree let deep_idx = TagIndex::new(deep_wide); group.bench_function("suggest_deep_path", |b| { b.iter(|| deep_idx.suggest(black_box("root.child-0.child-1"), 10)); }); group.bench_function("suggest_root", |b| { b.iter(|| deep_idx.suggest(black_box("root"), 10)); }); group.finish(); } fn bench_bulk_ops(c: &mut Criterion) { let tags_10k = generate_tags(10_000); let mut group = c.benchmark_group("bulk_ops"); group.bench_function("rename_prefix_bulk/10k", |b| { b.iter_batched( || TagIndex::new(tags_10k.clone()), |mut idx| rename_prefix_bulk(black_box("genre"), black_box("style"), &mut idx), criterion::BatchSize::SmallInput, ); }); group.bench_function("remove_subtree/10k", |b| { b.iter_batched( || TagIndex::new(tags_10k.clone()), |mut idx| remove_subtree(black_box("genre"), &mut idx), criterion::BatchSize::SmallInput, ); }); group.bench_function("merge_tags/10k", |b| { b.iter_batched( || TagIndex::new(tags_10k.clone()), |mut idx| merge_tags(black_box("genre.sub-0"), black_box("mood.sub-0"), &mut idx), criterion::BatchSize::SmallInput, ); }); group.bench_function("batch_rename_3ops/10k", |b| { let ops = [ ("genre.sub-0", "style.sub-0"), ("mood.sub-1", "vibe.sub-1"), ("source.sub-2", "origin.sub-2"), ]; b.iter_batched( || TagIndex::new(tags_10k.clone()), |mut idx| batch_rename(black_box(&ops), &mut idx), criterion::BatchSize::SmallInput, ); }); group.finish(); } criterion_group!( benches, bench_validate, bench_hierarchy, bench_sql, bench_tree_ops, bench_tag_index, bench_large_tree_ops, bench_large_tag_index, bench_large_validate, bench_deep_tree, bench_bulk_ops, ); criterion_main!(benches);