| 679 |
679 |
|
}
|
| 680 |
680 |
|
println!();
|
| 681 |
681 |
|
|
|
682 |
+ |
// ─────────────────────────────────────────────────────────────
|
|
683 |
+ |
// Section 7: Library Scaling (similarity index build + query)
|
|
684 |
+ |
// ─────────────────────────────────────────────────────────────
|
|
685 |
+ |
println!("━━━ 7. LIBRARY SCALING ━━━");
|
|
686 |
+ |
println!();
|
|
687 |
+ |
println!(" Similarity index (VP-tree) build + query vs library size.");
|
|
688 |
+ |
println!(" This is the operation that previously froze the GUI on large");
|
|
689 |
+ |
println!(" libraries and motivated the off-thread search worker; tracking it");
|
|
690 |
+ |
println!(" here catches build/query regressions before users feel a stall.");
|
|
691 |
+ |
println!();
|
|
692 |
+ |
bench_similarity_scaling();
|
|
693 |
+ |
println!();
|
|
694 |
+ |
|
| 682 |
695 |
|
println!("═══════════════════════════════════════════════════════════════");
|
| 683 |
696 |
|
println!(" Benchmark complete. {} files analyzed.", n + total_classified);
|
| 684 |
697 |
|
println!("═══════════════════════════════════════════════════════════════");
|
| 685 |
698 |
|
}
|
|
699 |
+ |
|
|
700 |
+ |
// ── Library Scaling ──
|
|
701 |
+ |
|
|
702 |
+ |
/// Deterministic synthetic feature vector generator. Uses a small LCG so the
|
|
703 |
+ |
/// bench needs no RNG dependency and is reproducible across runs.
|
|
704 |
+ |
fn synth_features(count: usize) -> Vec<(String, audiofiles_core::similarity::FeatureVector)> {
|
|
705 |
+ |
use audiofiles_core::similarity::FeatureVector;
|
|
706 |
+ |
let mut state: u64 = 0x2545F4914F6CDD1D;
|
|
707 |
+ |
let mut next = || {
|
|
708 |
+ |
// xorshift64 → unit f64
|
|
709 |
+ |
state ^= state << 13;
|
|
710 |
+ |
state ^= state >> 7;
|
|
711 |
+ |
state ^= state << 17;
|
|
712 |
+ |
(state >> 11) as f64 / (1u64 << 53) as f64
|
|
713 |
+ |
};
|
|
714 |
+ |
(0..count)
|
|
715 |
+ |
.map(|i| {
|
|
716 |
+ |
let fv = FeatureVector {
|
|
717 |
+ |
bpm: Some(60.0 + next() * 140.0),
|
|
718 |
+ |
duration: Some(next() * 10.0),
|
|
719 |
+ |
lufs: Some(-40.0 + next() * 40.0),
|
|
720 |
+ |
spectral_centroid: Some(next() * 8000.0),
|
|
721 |
+ |
spectral_flatness: Some(next()),
|
|
722 |
+ |
spectral_rolloff: Some(next() * 12000.0),
|
|
723 |
+ |
zero_crossing_rate: Some(next()),
|
|
724 |
+ |
onset_strength: Some(next()),
|
|
725 |
+ |
spectral_bandwidth: Some(next() * 4000.0),
|
|
726 |
+ |
centroid_variance: Some(next()),
|
|
727 |
+ |
crest_factor: Some(next() * 20.0),
|
|
728 |
+ |
attack_time: Some(next() * 0.5),
|
|
729 |
+ |
};
|
|
730 |
+ |
(format!("{i:064x}"), fv)
|
|
731 |
+ |
})
|
|
732 |
+ |
.collect()
|
|
733 |
+ |
}
|
|
734 |
+ |
|
|
735 |
+ |
fn bench_similarity_scaling() {
|
|
736 |
+ |
use audiofiles_core::similarity::SimilarityIndex;
|
|
737 |
+ |
|
|
738 |
+ |
println!(
|
|
739 |
+ |
" {:>9} {:>12} {:>14} {:>14}",
|
|
740 |
+ |
"Library", "Build(ms)", "Indexed q(µs)", "Linear q(µs)"
|
|
741 |
+ |
);
|
|
742 |
+ |
println!(" {}", "─".repeat(53));
|
|
743 |
+ |
|
|
744 |
+ |
for &size in &[1_000usize, 10_000, 50_000, 100_000] {
|
|
745 |
+ |
let data = synth_features(size);
|
|
746 |
+ |
|
|
747 |
+ |
let t = Instant::now();
|
|
748 |
+ |
let index = SimilarityIndex::build_from_data(data.clone());
|
|
749 |
+ |
let build_ms = t.elapsed().as_secs_f64() * 1000.0;
|
|
750 |
+ |
|
|
751 |
+ |
// Query the index with the first sample's features, averaged over runs.
|
|
752 |
+ |
let (probe_hash, probe_fv) = &data[0];
|
|
753 |
+ |
let runs = 200;
|
|
754 |
+ |
let t = Instant::now();
|
|
755 |
+ |
for _ in 0..runs {
|
|
756 |
+ |
let _ = index.find_similar(probe_hash, probe_fv, 20);
|
|
757 |
+ |
}
|
|
758 |
+ |
let indexed_us = t.elapsed().as_secs_f64() * 1e6 / runs as f64;
|
|
759 |
+ |
|
|
760 |
+ |
// Linear scan baseline over the same data for the same query count.
|
|
761 |
+ |
let weights = audiofiles_core::similarity::FeatureWeights::default();
|
|
762 |
+ |
let t = Instant::now();
|
|
763 |
+ |
for _ in 0..runs {
|
|
764 |
+ |
let mut best: Vec<f64> = data
|
|
765 |
+ |
.iter()
|
|
766 |
+ |
.map(|(_, fv)| audiofiles_core::similarity::feature_distance(probe_fv, fv, &weights))
|
|
767 |
+ |
.collect();
|
|
768 |
+ |
best.sort_by(|a, b| a.total_cmp(b));
|
|
769 |
+ |
let _ = &best[..best.len().min(20)];
|
|
770 |
+ |
}
|
|
771 |
+ |
let linear_us = t.elapsed().as_secs_f64() * 1e6 / runs as f64;
|
|
772 |
+ |
|
|
773 |
+ |
println!(
|
|
774 |
+ |
" {:>9} {:>12.1} {:>14.1} {:>14.1}",
|
|
775 |
+ |
size, build_ms, indexed_us, linear_us
|
|
776 |
+ |
);
|
|
777 |
+ |
}
|
|
778 |
+ |
}
|