mirror of
https://github.com/uutils/findutils.git
synced 2026-06-10 15:48:30 -07:00
33703b79d8
Introduce criterion benchmarks (via codspeed-criterion-compat) that drive find and xargs end-to-end through their real entry points, plus a CodSpeed GitHub Actions workflow to track performance on pushes and PRs. - benches/find_bench.rs: walks a generated directory tree exercising the matcher tree (-name/-iname/-regex/-size/-type, grouping, -prune, -printf). Output is sent to a sink so the directory walk and matching dominate. - benches/xargs_bench.rs: feeds a corpus via -a and runs 'true' so xargs' own argument reading/splitting/batching dominates (whitespace, NUL, -n, -s). - .github/workflows/codspeed.yml: mirrors the uutils/grep setup.
167 lines
5.3 KiB
Rust
167 lines
5.3 KiB
Rust
// Copyright 2024 the uutils developers
|
|
//
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file or at
|
|
// https://opensource.org/licenses/MIT.
|
|
|
|
//! End-to-end benchmarks for `find`, run through the real `find_main` entry
|
|
//! point so they exercise argument parsing, the matcher tree and the directory
|
|
//! walk together. Output is sent to a sink so the benchmarks measure the work
|
|
//! `find` does rather than terminal I/O.
|
|
|
|
use std::cell::RefCell;
|
|
use std::io::{self, Write};
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::SystemTime;
|
|
|
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
|
use findutils::find::{find_main, Dependencies};
|
|
|
|
/// `Dependencies` implementation that throws output away. A fixed `now` keeps
|
|
/// time-based matchers (`-newer`, `-mtime`, …) deterministic across runs.
|
|
struct SinkDependencies {
|
|
output: RefCell<io::Sink>,
|
|
now: SystemTime,
|
|
}
|
|
|
|
impl SinkDependencies {
|
|
fn new() -> Self {
|
|
Self {
|
|
output: RefCell::new(io::sink()),
|
|
now: SystemTime::now(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Dependencies for SinkDependencies {
|
|
fn get_output(&self) -> &RefCell<dyn Write> {
|
|
&self.output
|
|
}
|
|
|
|
fn now(&self) -> SystemTime {
|
|
self.now
|
|
}
|
|
|
|
fn confirm(&self, _prompt: &str) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Run `find` end-to-end. `args` are the arguments after the program name
|
|
/// (flags, paths, expression). The exit status is ignored — we only care about
|
|
/// the work performed.
|
|
fn run(args: &[&str]) {
|
|
let mut argv: Vec<&str> = Vec::with_capacity(args.len() + 1);
|
|
argv.push("find");
|
|
argv.extend_from_slice(args);
|
|
let deps = SinkDependencies::new();
|
|
let _ = find_main(&argv, &deps);
|
|
}
|
|
|
|
/// Build a moderately deep directory tree to walk. `depth` levels, each holding
|
|
/// `dirs` sub-directories and `files` files. File names vary so name/regex
|
|
/// matchers have something to discriminate on, and sizes vary so `-size` does
|
|
/// real work. Returns the root directory.
|
|
fn build_tree(depth: u32, dirs: u32, files: u32) -> PathBuf {
|
|
let root = std::env::temp_dir().join(format!("uu_find_bench_{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&root);
|
|
std::fs::create_dir_all(&root).unwrap();
|
|
populate(&root, depth, dirs, files, 0);
|
|
root
|
|
}
|
|
|
|
fn populate(dir: &Path, depth: u32, dirs: u32, files: u32, seed: u32) {
|
|
for f in 0..files {
|
|
let n = seed.wrapping_add(f);
|
|
// A mix of extensions and a sprinkling of a rare marker name.
|
|
let name = match n % 5 {
|
|
0 => format!("module_{n}.rs"),
|
|
1 => format!("data_{n}.txt"),
|
|
2 => format!("image_{n}.png"),
|
|
3 => format!("README_{n}.md"),
|
|
_ if n % 500 == 0 => format!("RAREHIT_{n}.log"),
|
|
_ => format!("file_{n}.log"),
|
|
};
|
|
// Sizes from a few bytes up to ~8 KiB so -size buckets differ.
|
|
let size = (n as usize % 8192) + 1;
|
|
let path = dir.join(name);
|
|
std::fs::write(&path, vec![b'x'; size]).unwrap();
|
|
}
|
|
|
|
if depth == 0 {
|
|
return;
|
|
}
|
|
|
|
for d in 0..dirs {
|
|
let sub = dir.join(format!("dir_{d}"));
|
|
std::fs::create_dir_all(&sub).unwrap();
|
|
populate(
|
|
&sub,
|
|
depth - 1,
|
|
dirs,
|
|
files,
|
|
seed.wrapping_add((d + 1) * 31),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn bench_e2e(c: &mut Criterion) {
|
|
// depth 4, 4 dirs/level, 25 files/dir → a few thousand entries.
|
|
let root = build_tree(4, 4, 25);
|
|
let dir = root.to_str().unwrap();
|
|
|
|
let mut group = c.benchmark_group("find");
|
|
|
|
// Plain full walk with the implicit -print.
|
|
group.bench_function("walk_all", |b| {
|
|
b.iter(|| run(black_box(&[dir])));
|
|
});
|
|
// Filter by file type only.
|
|
group.bench_function("type_f", |b| {
|
|
b.iter(|| run(black_box(&[dir, "-type", "f"])));
|
|
});
|
|
// Glob name match — a common invocation.
|
|
group.bench_function("name_glob", |b| {
|
|
b.iter(|| run(black_box(&[dir, "-name", "*.rs"])));
|
|
});
|
|
// Case-insensitive name match.
|
|
group.bench_function("iname_glob", |b| {
|
|
b.iter(|| run(black_box(&[dir, "-iname", "*.RS"])));
|
|
});
|
|
// Regex over the whole path.
|
|
group.bench_function("regex_path", |b| {
|
|
b.iter(|| run(black_box(&[dir, "-regex", r".*/module_[0-9]+\.rs"])));
|
|
});
|
|
// Size predicate forces a stat per entry.
|
|
group.bench_function("size_gt", |b| {
|
|
b.iter(|| run(black_box(&[dir, "-type", "f", "-size", "+4k"])));
|
|
});
|
|
// Combined predicate with AND/OR and grouping.
|
|
group.bench_function("combined_expr", |b| {
|
|
b.iter(|| {
|
|
run(black_box(&[
|
|
dir, "-type", "f", "(", "-name", "*.rs", "-o", "-name", "*.md", ")",
|
|
]));
|
|
});
|
|
});
|
|
// Prune whole subtrees, then print the rest.
|
|
group.bench_function("prune", |b| {
|
|
b.iter(|| {
|
|
run(black_box(&[
|
|
dir, "-name", "dir_0", "-prune", "-o", "-type", "f", "-print",
|
|
]));
|
|
});
|
|
});
|
|
// -printf with several directives exercises the formatter.
|
|
group.bench_function("printf", |b| {
|
|
b.iter(|| run(black_box(&[dir, "-type", "f", "-printf", "%p %s %y\\n"])));
|
|
});
|
|
|
|
group.finish();
|
|
|
|
let _ = std::fs::remove_dir_all(&root);
|
|
}
|
|
|
|
criterion_group!(benches, bench_e2e);
|
|
criterion_main!(benches);
|