Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3866d10762 |
@@ -10,3 +10,4 @@ result
|
||||
.repro/
|
||||
.wrangler/
|
||||
*.so
|
||||
big-repo/
|
||||
|
||||
Generated
+225
@@ -26,6 +26,18 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
@@ -103,6 +115,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.27"
|
||||
@@ -141,6 +159,58 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
@@ -153,6 +223,42 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||
dependencies = [
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
@@ -196,6 +302,12 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.4.7"
|
||||
@@ -263,6 +375,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"chrono",
|
||||
"criterion",
|
||||
"ctrlc",
|
||||
"git2",
|
||||
"heed",
|
||||
@@ -276,6 +389,7 @@ dependencies = [
|
||||
"openssl",
|
||||
"pathdiff",
|
||||
"rayon",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
@@ -355,6 +469,17 @@ dependencies = [
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heed"
|
||||
version = "0.22.0"
|
||||
@@ -393,6 +518,12 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
@@ -560,6 +691,26 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
@@ -892,6 +1043,12 @@ version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.73"
|
||||
@@ -1051,6 +1208,34 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"plotters-backend",
|
||||
"plotters-svg",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plotters-backend"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
|
||||
|
||||
[[package]]
|
||||
name = "plotters-svg"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
|
||||
dependencies = [
|
||||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.2"
|
||||
@@ -1429,6 +1614,16 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
@@ -1620,6 +1815,16 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -1896,6 +2101,26 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.6"
|
||||
|
||||
+16
@@ -15,6 +15,14 @@ path = "src/bin/test_watcher.rs"
|
||||
name = "jemalloc_profile"
|
||||
path = "src/bin/jemalloc_profile.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "search_profiler"
|
||||
path = "src/bin/search_profiler.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "search_only"
|
||||
path = "src/bin/search_only.rs"
|
||||
|
||||
|
||||
[dependencies]
|
||||
blake3 = "1.8.2"
|
||||
@@ -37,3 +45,11 @@ thiserror = "2.0.10"
|
||||
tracing = "0.1"
|
||||
tracing-appender = "0.2"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
tempfile = "3.8"
|
||||
|
||||
[[bench]]
|
||||
name = "indexing_and_search"
|
||||
harness = false
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
# FFF.nvim Benchmarks
|
||||
|
||||
This directory contains Criterion benchmarks for measuring the performance of the FFF.nvim plugin.
|
||||
|
||||
## Setup
|
||||
|
||||
Place your test repository in `./big-repo` at the project root. The benchmarks will index and search this directory.
|
||||
|
||||
```bash
|
||||
# Example: clone a large repository for testing
|
||||
git clone https://github.com/torvalds/linux big-repo
|
||||
# or
|
||||
git clone https://github.com/rust-lang/rust big-repo
|
||||
```
|
||||
|
||||
## Running Benchmarks
|
||||
|
||||
```bash
|
||||
# Run all benchmarks
|
||||
cargo bench --bench indexing_and_search
|
||||
|
||||
# Run only indexing benchmark
|
||||
cargo bench --bench indexing_and_search -- indexing
|
||||
|
||||
# Run only search benchmarks
|
||||
cargo bench --bench indexing_and_search -- search
|
||||
|
||||
# Run only thread scaling tests
|
||||
cargo bench --bench indexing_and_search -- thread_scaling
|
||||
|
||||
# Run only result limit tests
|
||||
cargo bench --bench indexing_and_search -- result_limits
|
||||
```
|
||||
|
||||
## Benchmark Groups
|
||||
|
||||
### 1. Indexing (`bench_indexing`)
|
||||
Measures the complete indexing time for the `./big-repo` directory:
|
||||
- File system scanning
|
||||
- Git status detection
|
||||
- Frecency score calculation
|
||||
- Background watcher initialization
|
||||
|
||||
**What it measures:** Time from `FilePicker::new()` until all files are indexed and available.
|
||||
|
||||
### 2. Search Queries (`bench_search_queries`)
|
||||
Tests fuzzy search with different query patterns:
|
||||
- **short**: `"mod"` - common short query
|
||||
- **medium**: `"controller"` - medium length query
|
||||
- **long**: `"user_authentication"` - long specific query
|
||||
- **typo**: `"contrlr"` - query with typos (tests typo-resistance)
|
||||
- **partial**: `"src/lib"` - path-like query
|
||||
|
||||
**What it measures:** Pure search time (no indexing overhead).
|
||||
|
||||
### 3. Thread Scaling (`bench_search_thread_scaling`)
|
||||
Compares search performance with different thread counts (1, 2, 4, 8).
|
||||
|
||||
**What it measures:** How well the parallel search scales with CPU cores.
|
||||
|
||||
### 4. Result Limits (`bench_search_result_limits`)
|
||||
Tests search with different max result counts (10, 50, 100, 500).
|
||||
|
||||
**What it measures:** Impact of result limit on search performance.
|
||||
|
||||
## Output
|
||||
|
||||
### Console Output
|
||||
Real-time progress with file counts:
|
||||
```
|
||||
✓ Indexed 50000 files
|
||||
✓ Search benchmarks will use 50000 files
|
||||
```
|
||||
|
||||
### HTML Reports
|
||||
Detailed reports generated in `target/criterion/`:
|
||||
- View at `target/criterion/report/index.html`
|
||||
- Includes graphs, statistics, and historical comparisons
|
||||
- Automatically detects performance regressions
|
||||
|
||||
### Comparison
|
||||
Criterion automatically compares against previous runs:
|
||||
```
|
||||
indexing/index_big_repo
|
||||
time: [1.234 s 1.250 s 1.267 s]
|
||||
change: [-5.23% -3.45% -1.67%] (p = 0.001 < 0.05)
|
||||
Performance has improved.
|
||||
```
|
||||
|
||||
## Profiling Tips
|
||||
|
||||
### For detailed profiling, use:
|
||||
```bash
|
||||
# With flamegraph (install cargo-flamegraph first)
|
||||
cargo flamegraph --bench indexing_and_search -- --bench indexing
|
||||
|
||||
# With perf
|
||||
perf record --call-graph dwarf cargo bench --bench indexing_and_search -- indexing
|
||||
perf report
|
||||
```
|
||||
|
||||
### Enable debug output:
|
||||
```bash
|
||||
RUST_LOG=debug cargo bench --bench indexing_and_search
|
||||
```
|
||||
|
||||
## Customizing Benchmarks
|
||||
|
||||
Edit `benches/indexing_and_search.rs` to:
|
||||
- Change sample size: `group.sample_size(N)`
|
||||
- Change measurement time: `group.measurement_time(Duration::from_secs(N))`
|
||||
- Add custom queries or test scenarios
|
||||
- Adjust thread counts or result limits
|
||||
|
||||
## Performance Expectations
|
||||
|
||||
Typical results for a 50k file repository:
|
||||
- **Indexing**: 1-3 seconds (depends on disk speed and git status)
|
||||
- **Search**: 5-50ms (depends on query complexity and match count)
|
||||
- **Thread scaling**: ~2-4x speedup from 1→4 threads
|
||||
- **Result limits**: Minimal impact (unless extremely large)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Indexed 0 files"
|
||||
The benchmark now properly waits for async indexing. If you still see this:
|
||||
- Ensure `./big-repo` exists and contains files
|
||||
- Check that files aren't all gitignored
|
||||
- Increase timeout in `wait_for_scan_completion()`
|
||||
|
||||
### Slow indexing
|
||||
- Check if git repository is very large (git status can be slow)
|
||||
- Disable git integration temporarily by testing on non-git directory
|
||||
- Profile with `perf` or flamegraph to identify bottleneck
|
||||
|
||||
### Inconsistent results
|
||||
- Close other applications to reduce system noise
|
||||
- Increase sample size for more stable measurements
|
||||
- Run benchmarks multiple times to establish baseline
|
||||
@@ -0,0 +1,377 @@
|
||||
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
|
||||
use fff_nvim::FILE_PICKER;
|
||||
use fff_nvim::file_picker::FilePicker;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tracing_subscriber;
|
||||
|
||||
/// Initialize tracing to output to console
|
||||
fn init_tracing() {
|
||||
// use tracing_subscriber::EnvFilter;
|
||||
// use tracing_subscriber::fmt;
|
||||
// let _ = fmt()
|
||||
// .with_env_filter(
|
||||
// EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
|
||||
// )
|
||||
// .with_target(false)
|
||||
// .with_thread_ids(true)
|
||||
// .with_line_number(true)
|
||||
// .try_init();
|
||||
}
|
||||
|
||||
/// Initialize FilePicker and insert into global state
|
||||
fn init_file_picker_internal(path: &str) -> Result<(), String> {
|
||||
let picker = FilePicker::new(path.to_string())
|
||||
.map_err(|e| format!("Failed to create FilePicker: {:?}", e))?;
|
||||
|
||||
let mut picker_guard = FILE_PICKER
|
||||
.write()
|
||||
.map_err(|_| "Failed to acquire write lock")?;
|
||||
*picker_guard = Some(picker);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to wait for scanning to complete and get file count
|
||||
fn wait_for_scan_completion(timeout_secs: u64) -> Result<usize, String> {
|
||||
let start = std::time::Instant::now();
|
||||
let timeout = Duration::from_secs(timeout_secs);
|
||||
let mut last_log = std::time::Instant::now();
|
||||
let mut iteration = 0;
|
||||
|
||||
loop {
|
||||
iteration += 1;
|
||||
|
||||
{
|
||||
let picker_guard = FILE_PICKER
|
||||
.read()
|
||||
.map_err(|_| "Failed to acquire read lock")?;
|
||||
if let Some(ref picker) = *picker_guard {
|
||||
let is_scanning = picker.is_scan_active();
|
||||
let file_count = picker.get_files().len();
|
||||
|
||||
// Log progress every 2 seconds
|
||||
if last_log.elapsed() >= Duration::from_secs(2) {
|
||||
eprintln!(
|
||||
" [{:.1}s] Scanning: {}, Files: {}, Iterations: {}",
|
||||
start.elapsed().as_secs_f32(),
|
||||
is_scanning,
|
||||
file_count,
|
||||
iteration
|
||||
);
|
||||
last_log = std::time::Instant::now();
|
||||
}
|
||||
|
||||
if !is_scanning && file_count > 0 {
|
||||
eprintln!(
|
||||
" ✓ Scan complete after {:.2}s: {} files found",
|
||||
start.elapsed().as_secs_f32(),
|
||||
file_count
|
||||
);
|
||||
return Ok(file_count);
|
||||
}
|
||||
} else {
|
||||
if iteration % 100 == 0 {
|
||||
eprintln!(
|
||||
" [{:.1}s] FilePicker is None (iteration {})",
|
||||
start.elapsed().as_secs_f32(),
|
||||
iteration
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if start.elapsed() > timeout {
|
||||
return Err(format!(
|
||||
"Scan timed out after {} seconds (iteration {})",
|
||||
timeout_secs, iteration
|
||||
));
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
/// Get files from the global FILE_PICKER
|
||||
fn get_files_snapshot() -> Result<Vec<fff_nvim::types::FileItem>, String> {
|
||||
let picker_guard = FILE_PICKER
|
||||
.read()
|
||||
.map_err(|_| "Failed to acquire read lock")?;
|
||||
if let Some(ref picker) = *picker_guard {
|
||||
Ok(picker.get_files().to_vec())
|
||||
} else {
|
||||
Err("FilePicker not initialized".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean up global state
|
||||
fn cleanup_global_state() {
|
||||
if let Ok(mut picker_guard) = FILE_PICKER.write() {
|
||||
if let Some(mut picker) = picker_guard.take() {
|
||||
picker.stop_background_monitor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize FilePicker once and return files snapshot
|
||||
fn setup_once() -> Result<Vec<fff_nvim::types::FileItem>, String> {
|
||||
init_tracing();
|
||||
|
||||
let big_repo_path = PathBuf::from("./big-repo");
|
||||
if !big_repo_path.exists() {
|
||||
return Err("./big-repo directory does not exist".to_string());
|
||||
}
|
||||
|
||||
let canonical_path = big_repo_path
|
||||
.canonicalize()
|
||||
.map_err(|e| format!("Failed to canonicalize path: {}", e))?;
|
||||
eprintln!(" Path: {:?}", canonical_path);
|
||||
|
||||
{
|
||||
let picker_guard = FILE_PICKER
|
||||
.read()
|
||||
.map_err(|_| "Failed to acquire read lock")?;
|
||||
if let Some(ref picker) = *picker_guard {
|
||||
let files = picker.get_files();
|
||||
if !files.is_empty() {
|
||||
eprintln!(" ℹ Reusing existing index with {} files", files.len());
|
||||
return Ok(files.to_vec());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanup_global_state();
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
|
||||
init_file_picker_internal(&canonical_path.to_string_lossy())?;
|
||||
|
||||
eprintln!(" Waiting for background scan to complete...");
|
||||
let file_count = wait_for_scan_completion(120)?;
|
||||
eprintln!(
|
||||
" ✓ Indexed {} files (will be reused for all benchmarks)\n",
|
||||
file_count
|
||||
);
|
||||
|
||||
get_files_snapshot()
|
||||
}
|
||||
|
||||
/// Benchmark for indexing the big-repo directory
|
||||
fn bench_indexing(c: &mut Criterion) {
|
||||
init_tracing();
|
||||
|
||||
let big_repo_path = PathBuf::from("./big-repo");
|
||||
|
||||
if !big_repo_path.exists() {
|
||||
eprintln!("⚠ Skipping indexing benchmark: ./big-repo directory does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
let canonical_path = match big_repo_path.canonicalize() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("⚠ Failed to canonicalize path: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut group = c.benchmark_group("indexing");
|
||||
group.sample_size(10);
|
||||
group.measurement_time(Duration::from_secs(20));
|
||||
|
||||
group.bench_function("index_big_repo", |b| {
|
||||
b.iter(|| {
|
||||
cleanup_global_state();
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
init_file_picker_internal(black_box(&canonical_path.to_string_lossy()))
|
||||
.expect("Failed to init FilePicker");
|
||||
|
||||
match wait_for_scan_completion(120) {
|
||||
Ok(file_count) => {
|
||||
let elapsed = start.elapsed();
|
||||
eprintln!(" ✓ Indexed {} files in {:?}", file_count, elapsed);
|
||||
file_count
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" ✗ Error: {}", e);
|
||||
0
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark for searching with various query patterns
|
||||
fn bench_search_queries(c: &mut Criterion) {
|
||||
let files = match setup_once() {
|
||||
Ok(files) => files,
|
||||
Err(e) => {
|
||||
eprint!("Failed to setup picker {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut group = c.benchmark_group("search");
|
||||
group.sample_size(100);
|
||||
|
||||
let test_queries = vec![
|
||||
("short", "mod"),
|
||||
("medium", "controller"),
|
||||
("long", "user_authentication"),
|
||||
("typo", "contrlr"),
|
||||
("partial", "src/lib"),
|
||||
];
|
||||
|
||||
for (name, query) in test_queries {
|
||||
group.bench_with_input(BenchmarkId::new("query", name), &query, |b, &query| {
|
||||
b.iter(|| {
|
||||
let results = FilePicker::fuzzy_search(
|
||||
black_box(&files),
|
||||
black_box(query),
|
||||
black_box(100),
|
||||
black_box(4),
|
||||
black_box(None),
|
||||
black_box(false),
|
||||
);
|
||||
results.total_matched
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark search with different thread counts
|
||||
fn bench_search_thread_scaling(c: &mut Criterion) {
|
||||
let files = match setup_once() {
|
||||
Ok(files) => files,
|
||||
Err(e) => {
|
||||
eprintln!("⚠ Skipping thread scaling benchmarks: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut group = c.benchmark_group("thread_scaling");
|
||||
group.sample_size(100);
|
||||
|
||||
let query = "controller";
|
||||
let thread_counts = vec![1, 2, 4, 8];
|
||||
|
||||
for threads in thread_counts {
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(threads),
|
||||
&threads,
|
||||
|b, &threads| {
|
||||
b.iter(|| {
|
||||
let results = FilePicker::fuzzy_search(
|
||||
black_box(&files),
|
||||
black_box(query),
|
||||
black_box(100),
|
||||
black_box(threads),
|
||||
black_box(None),
|
||||
black_box(false),
|
||||
);
|
||||
results.total_matched
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark search with different result limits
|
||||
fn bench_search_result_limits(c: &mut Criterion) {
|
||||
let files = match setup_once() {
|
||||
Ok(files) => files,
|
||||
Err(e) => {
|
||||
eprintln!("⚠ Skipping result limit benchmarks: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut group = c.benchmark_group("result_limits");
|
||||
group.sample_size(100);
|
||||
|
||||
let query = "mod";
|
||||
let result_limits = vec![10, 50, 100, 500];
|
||||
|
||||
for limit in result_limits {
|
||||
group.bench_with_input(BenchmarkId::from_parameter(limit), &limit, |b, &limit| {
|
||||
b.iter(|| {
|
||||
let results = FilePicker::fuzzy_search(
|
||||
black_box(&files),
|
||||
black_box(query),
|
||||
black_box(limit),
|
||||
black_box(4),
|
||||
black_box(None),
|
||||
black_box(false),
|
||||
);
|
||||
results.total_matched
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// Benchmark search algorithm performance scaling with file count
|
||||
fn bench_search_scalability(c: &mut Criterion) {
|
||||
let all_files = match setup_once() {
|
||||
Ok(files) => files,
|
||||
Err(e) => {
|
||||
eprintln!("⚠ Skipping scalability benchmarks: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if all_files.len() < 1000 {
|
||||
eprintln!(
|
||||
"⚠ Skipping scalability benchmark: need at least 1000 files, got {}",
|
||||
all_files.len()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut group = c.benchmark_group("search_scalability");
|
||||
group.sample_size(50);
|
||||
|
||||
let query = "controller";
|
||||
let file_counts = vec![100, 1000, 5000, 10000, all_files.len().min(50000)];
|
||||
|
||||
for count in file_counts {
|
||||
if count > all_files.len() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let subset = &all_files[..count];
|
||||
group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, _| {
|
||||
b.iter(|| {
|
||||
let results = FilePicker::fuzzy_search(
|
||||
black_box(subset),
|
||||
black_box(query),
|
||||
black_box(100),
|
||||
black_box(4),
|
||||
black_box(None),
|
||||
black_box(false),
|
||||
);
|
||||
results.total_matched
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_indexing,
|
||||
bench_search_queries,
|
||||
bench_search_thread_scaling,
|
||||
bench_search_result_limits,
|
||||
bench_search_scalability,
|
||||
);
|
||||
|
||||
criterion_main!(benches);
|
||||
@@ -66,7 +66,9 @@ impl FileItem {
|
||||
|
||||
Self {
|
||||
path,
|
||||
relative_path_lower: relative_path.to_lowercase(),
|
||||
relative_path,
|
||||
file_name_lower: name.to_lowercase(),
|
||||
file_name: name,
|
||||
size,
|
||||
modified,
|
||||
@@ -592,7 +594,8 @@ fn scan_filesystem(
|
||||
files.len()
|
||||
);
|
||||
|
||||
files.par_sort_unstable_by(|a, b| a.path.cmp(&b.path));
|
||||
// Sort by OsStr instead of Path to avoid expensive component-by-component comparison
|
||||
files.par_sort_unstable_by(|a, b| a.path.as_os_str().cmp(b.path.as_os_str()));
|
||||
Ok(FileSync { files, git_workdir })
|
||||
})
|
||||
}
|
||||
|
||||
+243
-19
@@ -33,9 +33,9 @@ pub fn match_and_score_files<'a>(
|
||||
};
|
||||
|
||||
let query_contains_path_separator = context.query.contains(MAIN_SEPARATOR);
|
||||
let haystack: Vec<_> = files
|
||||
let haystack: Vec<&str> = files
|
||||
.iter()
|
||||
.map(|f| f.relative_path.to_lowercase())
|
||||
.map(|f| f.relative_path_lower.as_str())
|
||||
.collect();
|
||||
tracing::debug!(
|
||||
"Starting fuzzy search for query '{}' in {} files",
|
||||
@@ -52,27 +52,39 @@ pub fn match_and_score_files<'a>(
|
||||
// assume that filename should only match if the path matches
|
||||
// we should actually incorporate this bonus by getting this information from neo_frizbee directly
|
||||
// instead of spawning a separate matching process, but it's okay for the beta
|
||||
let haystack_of_filenames = path_matches
|
||||
.par_iter()
|
||||
// Use sequential iteration - this is a simple filtering operation that's faster without Rayon overhead
|
||||
let haystack_of_filenames: Vec<&str> = path_matches
|
||||
.iter()
|
||||
.filter_map(|m| {
|
||||
files
|
||||
.get(m.index as usize)
|
||||
.map(|f| f.file_name.to_lowercase())
|
||||
.map(|f| f.file_name_lower.as_str())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect();
|
||||
|
||||
// if there is a / in the query we don't even match filenames
|
||||
let filename_matches = if query_contains_path_separator {
|
||||
vec![]
|
||||
} else {
|
||||
let mut list = neo_frizbee::match_list_parallel(
|
||||
context.query,
|
||||
&haystack_of_filenames,
|
||||
&options,
|
||||
context.max_threads,
|
||||
);
|
||||
// Use parallel matching only if we have enough filenames to justify overhead
|
||||
// Sequential matching is faster for small result sets (< 1000 matches)
|
||||
let mut list = if haystack_of_filenames.len() > 1000 {
|
||||
neo_frizbee::match_list_parallel(
|
||||
context.query,
|
||||
&haystack_of_filenames,
|
||||
&options,
|
||||
context.max_threads,
|
||||
)
|
||||
} else {
|
||||
neo_frizbee::match_list(context.query, &haystack_of_filenames, &options)
|
||||
};
|
||||
|
||||
list.par_sort_unstable_by_key(|m| m.index);
|
||||
// Sequential sort is faster for small lists
|
||||
if list.len() > 1000 {
|
||||
list.par_sort_unstable_by_key(|m| m.index);
|
||||
} else {
|
||||
list.sort_unstable_by_key(|m| m.index);
|
||||
}
|
||||
|
||||
list
|
||||
};
|
||||
@@ -250,30 +262,242 @@ fn calculate_current_file_penalty(
|
||||
}
|
||||
|
||||
/// Dynamically sorts and returns the top results either in ascending or descending order
|
||||
/// Uses partial sorting for large result sets to improve performance
|
||||
fn sort_and_truncate<'a>(
|
||||
mut results: Vec<(&'a FileItem, Score)>,
|
||||
context: &ScoringContext,
|
||||
) -> (Vec<&'a FileItem>, Vec<Score>, usize) {
|
||||
let total_matched = results.len();
|
||||
|
||||
// For large result sets, use partial sort to avoid sorting everything
|
||||
// Only sort enough elements to get the top N we need
|
||||
let threshold = context.max_results * 2;
|
||||
|
||||
if context.reverse_order {
|
||||
if results.len() > threshold {
|
||||
// For reverse order (lowest scores first), partition at max_results position
|
||||
// This puts the lowest max_results elements at the front
|
||||
results.select_nth_unstable_by(context.max_results, |a, b| {
|
||||
a.1.total
|
||||
.cmp(&b.1.total)
|
||||
.then_with(|| a.0.modified.cmp(&b.0.modified))
|
||||
});
|
||||
// Truncate to keep only the lowest max_results
|
||||
results.truncate(context.max_results);
|
||||
}
|
||||
|
||||
// Now fully sort the remaining results in ascending order
|
||||
results.sort_by(|a, b| {
|
||||
a.1.total
|
||||
.cmp(&b.1.total)
|
||||
.then_with(|| a.0.modified.cmp(&b.0.modified))
|
||||
});
|
||||
|
||||
if results.len() > context.max_results {
|
||||
results.drain(0..(total_matched - context.max_results));
|
||||
}
|
||||
} else {
|
||||
if results.len() > threshold {
|
||||
// Partition so the top max_results are at the front
|
||||
results.select_nth_unstable_by(context.max_results, |a, b| {
|
||||
b.1.total
|
||||
.cmp(&a.1.total)
|
||||
.then_with(|| b.0.modified.cmp(&a.0.modified))
|
||||
});
|
||||
results.truncate(context.max_results);
|
||||
}
|
||||
|
||||
// Now fully sort just the top N results
|
||||
results.sort_by(|a, b| {
|
||||
b.1.total
|
||||
.cmp(&a.1.total)
|
||||
.then_with(|| b.0.modified.cmp(&a.0.modified))
|
||||
});
|
||||
|
||||
results.truncate(context.max_results);
|
||||
}
|
||||
|
||||
let (items, scores) = results.into_iter().unzip();
|
||||
(items, scores, total_matched)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn create_test_file(path: &str, score: i32, modified: u64) -> (FileItem, Score) {
|
||||
let file = FileItem {
|
||||
path: PathBuf::from(path),
|
||||
relative_path: path.to_string(),
|
||||
relative_path_lower: path.to_lowercase(),
|
||||
file_name: path.split('/').last().unwrap_or(path).to_string(),
|
||||
file_name_lower: path.split('/').last().unwrap_or(path).to_lowercase(),
|
||||
size: 0,
|
||||
modified,
|
||||
access_frecency_score: 0,
|
||||
modification_frecency_score: 0,
|
||||
total_frecency_score: 0,
|
||||
git_status: None,
|
||||
};
|
||||
let score_obj = Score {
|
||||
total: score,
|
||||
base_score: score,
|
||||
filename_bonus: 0,
|
||||
distance_penalty: 0,
|
||||
special_filename_bonus: 0,
|
||||
current_file_penalty: 0,
|
||||
frecency_boost: 0,
|
||||
exact_match: false,
|
||||
match_type: "test",
|
||||
};
|
||||
(file, score_obj)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partial_sort_descending() {
|
||||
// Create test data with known scores
|
||||
let test_data = vec![
|
||||
create_test_file("file1.rs", 100, 1000),
|
||||
create_test_file("file2.rs", 200, 2000),
|
||||
create_test_file("file3.rs", 50, 3000),
|
||||
create_test_file("file4.rs", 300, 4000),
|
||||
create_test_file("file5.rs", 150, 5000),
|
||||
create_test_file("file6.rs", 250, 6000),
|
||||
create_test_file("file7.rs", 80, 7000),
|
||||
create_test_file("file8.rs", 180, 8000),
|
||||
create_test_file("file9.rs", 120, 9000),
|
||||
create_test_file("file10.rs", 90, 10000),
|
||||
];
|
||||
|
||||
// Convert to references like the actual function uses
|
||||
let results: Vec<(&FileItem, Score)> = test_data
|
||||
.iter()
|
||||
.map(|(file, score)| (file, score.clone()))
|
||||
.collect();
|
||||
|
||||
let context = ScoringContext {
|
||||
query: "test",
|
||||
max_results: 3,
|
||||
max_threads: 1,
|
||||
max_typos: 2,
|
||||
current_file: None,
|
||||
reverse_order: false,
|
||||
};
|
||||
|
||||
// Test with partial sort (threshold = 3 * 2 = 6, our len is 10 > 6)
|
||||
let (items, scores, total) = sort_and_truncate(results.clone(), &context);
|
||||
|
||||
// Should return top 3: 300, 250, 200
|
||||
assert_eq!(total, 10);
|
||||
assert_eq!(scores.len(), 3);
|
||||
assert_eq!(scores[0].total, 300, "First should be highest score");
|
||||
assert_eq!(scores[1].total, 250, "Second should be second highest");
|
||||
assert_eq!(scores[2].total, 200, "Third should be third highest");
|
||||
|
||||
// Verify the files match
|
||||
assert_eq!(items[0].relative_path, "file4.rs");
|
||||
assert_eq!(items[1].relative_path, "file6.rs");
|
||||
assert_eq!(items[2].relative_path, "file2.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partial_sort_with_same_scores() {
|
||||
// Test tiebreaker with modified time
|
||||
let test_data = vec![
|
||||
create_test_file("file1.rs", 100, 5000), // Same score, older
|
||||
create_test_file("file2.rs", 100, 8000), // Same score, newer
|
||||
create_test_file("file3.rs", 100, 3000), // Same score, oldest
|
||||
create_test_file("file4.rs", 200, 1000),
|
||||
create_test_file("file5.rs", 200, 9000), // Higher score, newest
|
||||
];
|
||||
|
||||
let results: Vec<(&FileItem, Score)> = test_data
|
||||
.iter()
|
||||
.map(|(file, score)| (file, score.clone()))
|
||||
.collect();
|
||||
|
||||
let context = ScoringContext {
|
||||
query: "test",
|
||||
max_results: 3,
|
||||
max_threads: 1,
|
||||
max_typos: 2,
|
||||
current_file: None,
|
||||
reverse_order: false,
|
||||
};
|
||||
|
||||
let (items, scores, _) = sort_and_truncate(results, &context);
|
||||
|
||||
// Should return: 200(9000), 200(1000), 100(8000)
|
||||
assert_eq!(scores[0].total, 200);
|
||||
assert_eq!(items[0].modified, 9000, "First 200 should be newest");
|
||||
assert_eq!(scores[1].total, 200);
|
||||
assert_eq!(items[1].modified, 1000, "Second 200 should be older");
|
||||
assert_eq!(scores[2].total, 100);
|
||||
assert_eq!(items[2].modified, 8000, "Third should be newest of 100s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_partial_sort_for_small_results() {
|
||||
// When results.len() <= threshold, should use regular sort
|
||||
let test_data = vec![
|
||||
create_test_file("file1.rs", 100, 1000),
|
||||
create_test_file("file2.rs", 200, 2000),
|
||||
create_test_file("file3.rs", 50, 3000),
|
||||
];
|
||||
|
||||
let results: Vec<(&FileItem, Score)> = test_data
|
||||
.iter()
|
||||
.map(|(file, score)| (file, score.clone()))
|
||||
.collect();
|
||||
|
||||
let context = ScoringContext {
|
||||
query: "test",
|
||||
max_results: 2,
|
||||
max_threads: 1,
|
||||
max_typos: 2,
|
||||
current_file: None,
|
||||
reverse_order: false,
|
||||
};
|
||||
|
||||
// threshold = 2 * 2 = 4, len = 3 < 4, so regular sort
|
||||
let (items, scores, _) = sort_and_truncate(results, &context);
|
||||
|
||||
assert_eq!(scores.len(), 2);
|
||||
assert_eq!(scores[0].total, 200);
|
||||
assert_eq!(scores[1].total, 100);
|
||||
assert_eq!(items[0].relative_path, "file2.rs");
|
||||
assert_eq!(items[1].relative_path, "file1.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reverse_order_partial_sort() {
|
||||
let test_data = vec![
|
||||
create_test_file("file1.rs", 100, 1000),
|
||||
create_test_file("file2.rs", 200, 2000),
|
||||
create_test_file("file3.rs", 50, 3000),
|
||||
create_test_file("file4.rs", 300, 4000),
|
||||
create_test_file("file5.rs", 150, 5000),
|
||||
create_test_file("file6.rs", 250, 6000),
|
||||
];
|
||||
|
||||
let results: Vec<(&FileItem, Score)> = test_data
|
||||
.iter()
|
||||
.map(|(file, score)| (file, score.clone()))
|
||||
.collect();
|
||||
|
||||
let context = ScoringContext {
|
||||
query: "test",
|
||||
max_results: 3,
|
||||
max_threads: 1,
|
||||
max_typos: 2,
|
||||
current_file: None,
|
||||
reverse_order: true,
|
||||
};
|
||||
|
||||
let (items, scores, _) = sort_and_truncate(results, &context);
|
||||
|
||||
// Reverse order should return lowest 3: 50, 100, 150
|
||||
assert_eq!(scores.len(), 3);
|
||||
assert_eq!(scores[0].total, 50, "First should be lowest");
|
||||
assert_eq!(scores[1].total, 100, "Second should be second lowest");
|
||||
assert_eq!(scores[2].total, 150, "Third should be third lowest");
|
||||
assert_eq!(items[0].relative_path, "file3.rs");
|
||||
assert_eq!(items[1].relative_path, "file1.rs");
|
||||
assert_eq!(items[2].relative_path, "file5.rs");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ use crate::{git::format_git_status, location::Location};
|
||||
pub struct FileItem {
|
||||
pub path: PathBuf,
|
||||
pub relative_path: String,
|
||||
pub relative_path_lower: String,
|
||||
pub file_name: String,
|
||||
pub file_name_lower: String,
|
||||
pub size: u64,
|
||||
pub modified: u64,
|
||||
pub access_frecency_score: i64,
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/// Simple search profiler that directly uses scan_filesystem without background thread overhead
|
||||
use fff_nvim::file_picker::FilePicker;
|
||||
use std::time::Instant;
|
||||
|
||||
fn main() {
|
||||
let big_repo_path = std::path::PathBuf::from("./big-repo");
|
||||
|
||||
if !big_repo_path.exists() {
|
||||
eprintln!("Error: ./big-repo directory does not exist");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let canonical_path = big_repo_path
|
||||
.canonicalize()
|
||||
.expect("Failed to canonicalize path");
|
||||
|
||||
eprintln!("Loading files from: {:?}", canonical_path);
|
||||
|
||||
// Directly scan without background thread
|
||||
let start = Instant::now();
|
||||
let files = {
|
||||
use ignore::WalkBuilder;
|
||||
let mut files = Vec::new();
|
||||
|
||||
WalkBuilder::new(&canonical_path)
|
||||
.hidden(false)
|
||||
.build()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.file_type().is_some_and(|ft| ft.is_file()))
|
||||
.for_each(|entry| {
|
||||
let path = entry.path().to_path_buf();
|
||||
let relative =
|
||||
pathdiff::diff_paths(&path, &canonical_path).unwrap_or_else(|| path.clone());
|
||||
|
||||
let relative_path = relative.to_string_lossy().into_owned();
|
||||
let file_name = entry.file_name().to_string_lossy().into_owned();
|
||||
|
||||
files.push(fff_nvim::types::FileItem {
|
||||
path,
|
||||
relative_path_lower: relative_path.to_lowercase(),
|
||||
relative_path,
|
||||
file_name_lower: file_name.to_lowercase(),
|
||||
file_name,
|
||||
size: entry.metadata().ok().map_or(0, |m| m.len()),
|
||||
modified: 0,
|
||||
access_frecency_score: 0,
|
||||
modification_frecency_score: 0,
|
||||
total_frecency_score: 0,
|
||||
git_status: None,
|
||||
});
|
||||
});
|
||||
|
||||
files
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"✓ Loaded {} files in {:.2}s\n",
|
||||
files.len(),
|
||||
start.elapsed().as_secs_f64()
|
||||
);
|
||||
|
||||
// Test queries
|
||||
let test_queries = vec![
|
||||
("short_common", "mod", 500),
|
||||
("medium_specific", "controller", 200),
|
||||
("long_rare", "user_authentication", 100),
|
||||
("typo_resistant", "contrlr", 200),
|
||||
("path_like", "src/lib", 150),
|
||||
("single_char", "a", 300),
|
||||
("two_char", "st", 300),
|
||||
("partial_word", "test", 200),
|
||||
("deep_path", "drivers/net", 100),
|
||||
("extension", ".rs", 200),
|
||||
];
|
||||
|
||||
eprintln!("Running search profiler...");
|
||||
eprintln!("Query | Iterations | Total Time | Avg Time | Matches");
|
||||
eprintln!("----------------------|------------|------------|-----------|--------");
|
||||
|
||||
let global_start = Instant::now();
|
||||
let mut total_iterations = 0;
|
||||
|
||||
for (name, query, iterations) in test_queries {
|
||||
let start = Instant::now();
|
||||
let mut match_count = 0;
|
||||
|
||||
for _ in 0..iterations {
|
||||
let results = FilePicker::fuzzy_search(
|
||||
&files, query, 100, // max_results
|
||||
4, // max_threads
|
||||
None, // current_file
|
||||
false, // reverse_order
|
||||
);
|
||||
match_count += results.total_matched;
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
let avg_time = elapsed / iterations as u32;
|
||||
|
||||
eprintln!(
|
||||
"{:<21} | {:>10} | {:>9.2}s | {:>7}µs | {}",
|
||||
name,
|
||||
iterations,
|
||||
elapsed.as_secs_f64(),
|
||||
avg_time.as_micros(),
|
||||
match_count / iterations
|
||||
);
|
||||
|
||||
total_iterations += iterations;
|
||||
}
|
||||
|
||||
let total_time = global_start.elapsed();
|
||||
|
||||
eprintln!("\n=== Summary ===");
|
||||
eprintln!("Total searches: {}", total_iterations);
|
||||
eprintln!("Total time: {:.2}s", total_time.as_secs_f64());
|
||||
eprintln!(
|
||||
"Average per search: {}µs",
|
||||
(total_time.as_micros() as usize) / total_iterations
|
||||
);
|
||||
eprintln!(
|
||||
"Searches per sec: {:.0}",
|
||||
total_iterations as f64 / total_time.as_secs_f64()
|
||||
);
|
||||
eprintln!(
|
||||
"\nYou can now run: perf record -g --call-graph dwarf -F 999 ./target/release/search_only"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
use fff_nvim::FILE_PICKER;
|
||||
use fff_nvim::file_picker::FilePicker;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Wait for background scan to complete
|
||||
fn wait_for_scan(timeout_secs: u64) -> Result<usize, String> {
|
||||
let start = Instant::now();
|
||||
let timeout = Duration::from_secs(timeout_secs);
|
||||
let mut iteration = 0;
|
||||
|
||||
loop {
|
||||
iteration += 1;
|
||||
|
||||
let picker_guard = FILE_PICKER
|
||||
.read()
|
||||
.map_err(|_| "Failed to acquire read lock")?;
|
||||
if let Some(ref picker) = *picker_guard {
|
||||
let is_scanning = picker.is_scan_active();
|
||||
let file_count = picker.get_files().len();
|
||||
|
||||
if iteration % 20 == 0 {
|
||||
eprintln!(
|
||||
" [{:.1}s] Scanning: {}, Files: {}",
|
||||
start.elapsed().as_secs_f64(),
|
||||
is_scanning,
|
||||
file_count
|
||||
);
|
||||
}
|
||||
|
||||
if !is_scanning && file_count > 0 {
|
||||
return Ok(file_count);
|
||||
}
|
||||
} else if iteration % 20 == 0 {
|
||||
eprintln!(
|
||||
" [{:.1}s] FilePicker is None",
|
||||
start.elapsed().as_secs_f64()
|
||||
);
|
||||
}
|
||||
|
||||
if start.elapsed() > timeout {
|
||||
return Err(format!("Scan timed out after {} seconds", timeout_secs));
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize FilePicker and insert into global state
|
||||
fn init_file_picker(path: &str) -> Result<(), String> {
|
||||
let picker = FilePicker::new(path.to_string())
|
||||
.map_err(|e| format!("Failed to create FilePicker: {:?}", e))?;
|
||||
|
||||
let mut picker_guard = FILE_PICKER
|
||||
.write()
|
||||
.map_err(|_| "Failed to acquire write lock")?;
|
||||
*picker_guard = Some(picker);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get files snapshot from global state
|
||||
fn get_files() -> Result<Vec<fff_nvim::types::FileItem>, String> {
|
||||
let picker_guard = FILE_PICKER
|
||||
.read()
|
||||
.map_err(|_| "Failed to acquire read lock")?;
|
||||
if let Some(ref picker) = *picker_guard {
|
||||
Ok(picker.get_files().to_vec())
|
||||
} else {
|
||||
Err("FilePicker not initialized".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let big_repo_path = std::path::PathBuf::from("./big-repo");
|
||||
|
||||
if !big_repo_path.exists() {
|
||||
eprintln!("Error: ./big-repo directory does not exist");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let canonical_path = big_repo_path
|
||||
.canonicalize()
|
||||
.expect("Failed to canonicalize path");
|
||||
|
||||
eprintln!("Initializing FilePicker for: {:?}", canonical_path);
|
||||
init_file_picker(&canonical_path.to_string_lossy()).expect("Failed to init FilePicker");
|
||||
|
||||
// Give background thread time to start
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
|
||||
eprintln!("Waiting for scan to complete...");
|
||||
let file_count = wait_for_scan(120).expect("Failed to wait for scan");
|
||||
eprintln!("✓ Indexed {} files\n", file_count);
|
||||
|
||||
let files = get_files().expect("Failed to get files");
|
||||
|
||||
// Test queries representing different search patterns
|
||||
let test_queries = vec![
|
||||
("short_common", "mod", 5000),
|
||||
("medium_specific", "controller", 2000),
|
||||
("long_rare", "user_authentication", 1000),
|
||||
("typo_resistant", "contrlr", 2000),
|
||||
("path_like", "src/lib", 1500),
|
||||
("single_char", "a", 3000),
|
||||
("two_char", "st", 3000),
|
||||
("partial_word", "test", 2000),
|
||||
("deep_path", "drivers/net", 1000),
|
||||
("extension", ".rs", 2000),
|
||||
];
|
||||
|
||||
eprintln!("Running search profiler...");
|
||||
eprintln!("Query | Iterations | Total Time | Avg Time | Matches");
|
||||
eprintln!("----------------------|------------|------------|-----------|--------");
|
||||
|
||||
let global_start = Instant::now();
|
||||
let mut total_iterations = 0;
|
||||
|
||||
for (name, query, iterations) in test_queries {
|
||||
let start = Instant::now();
|
||||
let mut match_count = 0;
|
||||
|
||||
for _ in 0..iterations {
|
||||
let results = FilePicker::fuzzy_search(
|
||||
&files, query, 100, // max_results
|
||||
4, // max_threads
|
||||
None, // current_file
|
||||
false, // reverse_order
|
||||
);
|
||||
match_count += results.total_matched;
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
let avg_time = elapsed / iterations as u32;
|
||||
|
||||
eprintln!(
|
||||
"{:<21} | {:>10} | {:>9.2}s | {:>7}µs | {}",
|
||||
name,
|
||||
iterations,
|
||||
elapsed.as_secs_f64(),
|
||||
avg_time.as_micros(),
|
||||
match_count / iterations
|
||||
);
|
||||
|
||||
total_iterations += iterations;
|
||||
}
|
||||
|
||||
let total_time = global_start.elapsed();
|
||||
|
||||
eprintln!("\n=== Summary ===");
|
||||
eprintln!("Total searches: {}", total_iterations);
|
||||
eprintln!("Total time: {:.2}s", total_time.as_secs_f64());
|
||||
eprintln!(
|
||||
"Average per search: {}µs",
|
||||
(total_time.as_micros() as usize) / total_iterations
|
||||
);
|
||||
eprintln!(
|
||||
"Searches per sec: {:.0}",
|
||||
total_iterations as f64 / total_time.as_secs_f64()
|
||||
);
|
||||
|
||||
// Keep the program alive briefly so perf can capture everything
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Reference in New Issue
Block a user