lotabout.skim/benches/partial.rs
2026-05-21 18:19:40 +02:00

292 lines
11 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![allow(missing_docs, clippy::pedantic)]
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Cursor, Stderr};
use std::time::Duration;
use clap::Parser as _;
use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main};
use ratatui::backend::TestBackend;
use ratatui::prelude::CrosstermBackend;
use skim::prelude::*;
/// Small inline fixture — fast to load, good for latency benchmarks.
const SMALL_ITEMS: &[&str] = &[
"src/main.rs",
"src/lib.rs",
"src/options.rs",
"src/skim.rs",
"benches/partial.rs",
"tests/common/insta.rs",
"Cargo.toml",
"README.md",
];
/// Path to the medium fixture shipped with the repo (≈664 lines).
const FIXTURE_DEFAULT: &str = "benches/fixtures/default.txt";
/// Path to the large fixture (100 000 lines). Skip in low-time CI runs if absent.
const FIXTURE_100K: &str = "benches/fixtures/100K.txt";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Read a fixture file into memory and return each non-empty line as a `String`.
fn load_fixture(path: &str) -> Vec<String> {
let f = File::open(path).unwrap_or_else(|e| panic!("cannot open fixture {path}: {e}"));
BufReader::new(f)
.lines()
.map_while(Result::ok)
.filter(|l| !l.is_empty())
.collect()
}
/// Build a newline-separated `Vec<u8>` from a slice of items.
fn items_to_bytes(items: &[impl AsRef<str>]) -> Vec<u8> {
let mut buf = Vec::new();
for item in items {
buf.extend_from_slice(item.as_ref().as_bytes());
buf.push(b'\n');
}
buf
}
/// Create a `SkimItemReceiver` that will produce `items` via the full
/// `SkimItemReader` pipeline (the same path used when skim reads stdin).
fn make_receiver(items: &[impl AsRef<str>], options: &SkimOptions) -> SkimItemReceiver {
let reader_opts = SkimItemReaderOption::from_options(options);
let item_reader = SkimItemReader::new(reader_opts);
item_reader.of_bufread(Cursor::new(items_to_bytes(items)))
}
/// Spin-wait until the reader is done and the matcher has stopped (mirrors the
/// logic in `tests/common/insta.rs`). Panics after `timeout`.
fn wait_until_done(skim: &mut Skim<TestBackend>, timeout: Duration) {
let start = std::time::Instant::now();
while !skim.reader_done() {
assert!(start.elapsed() < timeout, "timeout waiting for reader");
skim.check_reader();
std::thread::sleep(Duration::from_millis(1));
}
skim.check_reader();
while !skim.app().matcher_control.stopped() {
assert!(start.elapsed() < timeout, "timeout waiting for matcher");
std::thread::sleep(Duration::from_millis(1));
}
}
// ---------------------------------------------------------------------------
// Benchmark group
// ---------------------------------------------------------------------------
fn criterion_benchmark(c: &mut Criterion) {
// -----------------------------------------------------------------------
// Phase 0 — Options
// -----------------------------------------------------------------------
c.bench_function("parse_options", |b| {
b.iter(|| SkimOptions::parse_from(Vec::<&str>::new()));
});
// `from_env` merges SKIM_DEFAULT_OPTIONS, SKIM_OPTIONS_FILE, and argv.
// In a clean test environment it should behave identically to parse_from,
// but the code path is different and may regress independently.
c.bench_function("options_from_env", |b| {
b.iter(SkimOptions::from_env);
});
// `build()` post-processes raw options: expands keymaps, normalises
// tiebreak / layout, reads history files, etc.
c.bench_function("options_build", |b| {
b.iter_batched(
|| SkimOptions::parse_from(Vec::<&str>::new()),
|opts| opts.build(),
BatchSize::SmallInput,
);
});
// -----------------------------------------------------------------------
// Phase 1 — SkimItemReader pipeline construction
// -----------------------------------------------------------------------
// Building the reader option struct from a SkimOptions (called in sk_main
// before Skim::init).
c.bench_function("item_reader_option_from_options", |b| {
let opts = SkimOptions::default().build();
b.iter(|| SkimItemReaderOption::from_options(&opts));
});
// Constructing a new SkimItemReader (spawns a thread-pool).
c.bench_function("item_reader_new", |b| {
b.iter_batched(
|| {
let opts = SkimOptions::default().build();
SkimItemReaderOption::from_options(&opts)
},
SkimItemReader::new,
BatchSize::SmallInput,
);
});
// `of_bufread` starts the I/O dispatcher and pool threads but does NOT
// wait for all items to be processed; it just returns the receiver.
// Benchmark with the default fixture to get a realistic buffer.
c.bench_function("of_bufread_setup", |b| {
let opts = SkimOptions::default().build();
let data = items_to_bytes(&load_fixture(FIXTURE_DEFAULT));
b.iter_batched(
|| {
let reader_opts = SkimItemReaderOption::from_options(&opts);
(SkimItemReader::new(reader_opts), data.clone())
},
|(reader, bytes)| {
// Calling of_bufread starts the background threads; we
// deliberately drop the receiver immediately so they clean up.
let _rx = reader.of_bufread(Cursor::new(bytes));
},
BatchSize::SmallInput,
);
});
// -----------------------------------------------------------------------
// Phase 2 — Skim::init (unchanged from before, kept for comparison)
// -----------------------------------------------------------------------
c.bench_function("init", |b| {
b.iter_batched(
|| SkimOptions::default().build(),
|options: SkimOptions| Skim::<CrosstermBackend<BufWriter<Stderr>>>::init(options, None),
BatchSize::SmallInput,
);
});
c.bench_function("init_with_source", |b| {
b.iter_batched(
|| {
let (_tx, rx) = bounded(8);
(SkimOptions::default().build(), rx)
},
|input: (SkimOptions, SkimItemReceiver)| {
Skim::<CrosstermBackend<BufWriter<Stderr>>>::init(input.0, Some(input.1))
},
BatchSize::SmallInput,
);
});
// -----------------------------------------------------------------------
// Phase 2+3 — Skim::init + start
// -----------------------------------------------------------------------
c.bench_function("start", |b| {
b.iter_batched(
|| Skim::<CrosstermBackend<BufWriter<Stderr>>>::init(SkimOptions::default().build(), None).unwrap(),
|mut skim: Skim| skim.start(),
BatchSize::SmallInput,
);
});
// -----------------------------------------------------------------------
// Phase 3+4 — ingest + match with realistic data
// -----------------------------------------------------------------------
// These benchmarks measure end-to-end reader+matcher throughput: time from
// start() until all items are ingested and the matcher has finished its
// first pass. They are the closest in-process analog to the CLI startup
// time measured by `cargo bench --bench cli`.
// Small inline fixture (8 items) — baseline latency floor.
c.bench_function("ingest_and_match_small", |b| {
b.iter_batched(
|| {
let opts = SkimOptions::default().build();
let rx = make_receiver(SMALL_ITEMS, &opts);
Skim::<TestBackend>::init(opts, Some(rx)).unwrap()
},
|mut skim: Skim<TestBackend>| {
skim.start();
wait_until_done(&mut skim, Duration::from_secs(5));
},
BatchSize::SmallInput,
);
});
// Medium fixture (≈664 lines from FIXTURE_DEFAULT).
{
let items = load_fixture(FIXTURE_DEFAULT);
let n = items.len() as u64;
let mut group = c.benchmark_group("ingest_and_match_default_fixture");
group.throughput(Throughput::Elements(n));
group.bench_function("ingest_and_match", |b| {
b.iter_batched(
|| {
let opts = SkimOptions::default().build();
let rx = make_receiver(&items, &opts);
Skim::<TestBackend>::init(opts, Some(rx)).unwrap()
},
|mut skim: Skim<TestBackend>| {
skim.start();
wait_until_done(&mut skim, Duration::from_secs(10));
},
BatchSize::SmallInput,
);
});
group.finish();
}
// Large fixture (100 000 lines). Only run when the file exists so the
// bench suite does not fail in environments without the fixture.
if std::path::Path::new(FIXTURE_100K).exists() {
let items = load_fixture(FIXTURE_100K);
let n = items.len() as u64;
let mut group = c.benchmark_group("ingest_and_match_100k_fixture");
group.throughput(Throughput::Elements(n));
// Fewer samples: each iteration loads 100 k items.
group.sample_size(10);
group.bench_function("ingest_and_match", |b| {
b.iter_batched(
|| {
let opts = SkimOptions::default().build();
let rx = make_receiver(&items, &opts);
Skim::<TestBackend>::init(opts, Some(rx)).unwrap()
},
|mut skim: Skim<TestBackend>| {
skim.start();
wait_until_done(&mut skim, Duration::from_secs(30));
},
BatchSize::SmallInput,
);
});
group.finish();
}
// -----------------------------------------------------------------------
// Phase 25 — full_setup (unchanged from before, kept for comparison)
// -----------------------------------------------------------------------
c.bench_function("full_setup", |b| {
b.iter(|| {
let mut options = SkimOptions::default().build();
if let Some(ref filter_query) = options.filter
&& options.query.is_none()
{
options.query = Some(filter_query.clone());
}
let mut skim = Skim::init(options, None).unwrap();
skim.start();
if skim.should_enter() {
skim.init_tui().unwrap();
}
});
});
}
criterion_group!(
name = benches;
config = Criterion::default().sample_size(100);
targets = criterion_benchmark
);
criterion_main!(benches);