ci: add CodSpeed benchmarks for updatedb and locate (#705)

Drive both utilities end-to-end through their real entry points.

- benches/updatedb_bench.rs: walks a generated tree (~8.5k paths) and
  writes a LOCATE02 database, measuring the walk + front-coding + write
  (full build and a -regex/-prune variant). Empty prune options keep the
  run deterministic regardless of temp_dir location.
- benches/locate_bench.rs: builds the database once via updatedb, then
  scans it in -c/count mode so database decoding and pattern matching
  dominate (no-match, substring, -b basename, -i ignore-case, -r regex).
  Unix-only, matching the locate module's cfg.

The existing CodSpeed workflow picks these up via cargo codspeed build/run.
This commit is contained in:
Sylvestre Ledru
2026-06-06 11:00:04 +02:00
committed by GitHub
parent 33703b79d8
commit fc446aba0b
4 changed files with 267 additions and 3 deletions
Generated
+22 -3
View File
@@ -303,7 +303,7 @@ dependencies = [
"codspeed",
"criterion-plot",
"is-terminal",
"itertools",
"itertools 0.10.5",
"num-traits",
"once_cell",
"oorandom",
@@ -355,7 +355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
"itertools 0.10.5",
]
[[package]]
@@ -516,7 +516,7 @@ dependencies = [
"ctor",
"faccess",
"filetime",
"itertools",
"itertools 0.14.0",
"nix 0.31.3",
"onig",
"regex",
@@ -768,6 +768,15 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
@@ -1477,6 +1486,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 = "toml_datetime"
version = "1.1.1+spec-1.1.0"
+8
View File
@@ -40,6 +40,14 @@ harness = false
name = "xargs_bench"
harness = false
[[bench]]
name = "updatedb_bench"
harness = false
[[bench]]
name = "locate_bench"
harness = false
[[bin]]
name = "find"
path = "src/find/main.rs"
+127
View File
@@ -0,0 +1,127 @@
// 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 `locate`, run through the real `locate_main` entry
//! point. A LOCATE02 database is built once (via `updatedb`) from a generated
//! tree, then each case scans it in `-c`/count mode so the database decoding
//! and pattern matching dominate rather than terminal output.
//!
//! `locate` is Unix-only (it relies on `OsStr`/`CStr` byte APIs), so the
//! benchmark body is compiled only there.
#[cfg(unix)]
mod bench {
use std::path::{Path, PathBuf};
use criterion::{black_box, criterion_group, Criterion};
use findutils::locate::locate_main;
use findutils::updatedb::updatedb_main;
/// Run `locate` end-to-end. `args` are the arguments after the program name.
fn run(args: &[&str]) {
let mut argv: Vec<&str> = Vec::with_capacity(args.len() + 1);
argv.push("locate");
argv.extend_from_slice(args);
let _ = locate_main(&argv);
}
fn build_tree(depth: u32, dirs: u32, files: u32) -> PathBuf {
let root = std::env::temp_dir().join(format!("uu_locate_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);
let name = match n % 4 {
0 => format!("module_{n}.rs"),
1 => format!("data_{n}.txt"),
2 => format!("image_{n}.png"),
_ => format!("file_{n}.log"),
};
std::fs::write(dir.join(name), b"x").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),
);
}
}
pub fn bench_e2e(c: &mut Criterion) {
// ~8.5k paths, then front-coded into a LOCATE02 database once.
let root = build_tree(4, 4, 25);
let dir = root.to_str().unwrap();
let db = std::env::temp_dir().join(format!("uu_locate_bench_{}.db", std::process::id()));
let db_path = db.to_str().unwrap();
let _ = updatedb_main(&[
"updatedb",
&format!("--localpaths={dir}"),
&format!("--output={db_path}"),
"--prunepaths=",
"--prunefs=",
]);
let mut group = c.benchmark_group("locate");
// Pattern matching nothing: forces a full decode + match of every entry.
group.bench_function("count_no_match", |b| {
b.iter(|| run(black_box(&["-d", db_path, "-c", "NONEXISTENT_TOKEN_XYZ"])));
});
// Substring matching many entries (count mode → no per-match output).
group.bench_function("count_many", |b| {
b.iter(|| run(black_box(&["-d", db_path, "-c", ".log"])));
});
// Basename matching (-b) restricts the comparison to the file name.
group.bench_function("basename", |b| {
b.iter(|| run(black_box(&["-d", db_path, "-c", "-b", "module"])));
});
// Case-insensitive matching (-i).
group.bench_function("ignore_case", |b| {
b.iter(|| run(black_box(&["-d", db_path, "-c", "-i", "MODULE"])));
});
// Regex matching (-r) over the whole path.
group.bench_function("regex", |b| {
b.iter(|| {
run(black_box(&[
"-d",
db_path,
"-c",
"-r",
r"module_[0-9]+\.rs",
]));
});
});
group.finish();
let _ = std::fs::remove_dir_all(&root);
let _ = std::fs::remove_file(&db);
}
criterion_group!(benches, bench_e2e);
}
#[cfg(unix)]
criterion::criterion_main!(bench::benches);
#[cfg(not(unix))]
fn main() {}
+110
View File
@@ -0,0 +1,110 @@
// 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 `updatedb`, run through the real `updatedb_main`
//! entry point. Each case walks a generated directory tree (via the internal
//! `find`), encodes it into the LOCATE02 database format and writes the result
//! to a temp file, so the walk + front-coding + write dominate the measurement.
use std::path::{Path, PathBuf};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use findutils::updatedb::updatedb_main;
/// Run `updatedb` end-to-end. `args` are the arguments after the program name.
fn run(args: &[&str]) {
let mut argv: Vec<&str> = Vec::with_capacity(args.len() + 1);
argv.push("updatedb");
argv.extend_from_slice(args);
let _ = updatedb_main(&argv);
}
/// Build a directory tree to index. `depth` levels, each holding `dirs`
/// sub-directories and `files` files with varied names. Returns the root.
fn build_tree(depth: u32, dirs: u32, files: u32) -> PathBuf {
let root = std::env::temp_dir().join(format!("uu_updatedb_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);
let name = match n % 4 {
0 => format!("module_{n}.rs"),
1 => format!("data_{n}.txt"),
2 => format!("image_{n}.png"),
_ => format!("file_{n}.log"),
};
// Tiny files: updatedb only records names, so content size is irrelevant.
std::fs::write(dir.join(name), b"x").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 → ~8.5k indexed paths.
let root = build_tree(4, 4, 25);
let dir = root.to_str().unwrap();
let db = std::env::temp_dir().join(format!("uu_updatedb_bench_{}.db", std::process::id()));
let db_path = db.to_str().unwrap();
let localpaths = format!("--localpaths={dir}");
let output = format!("--output={db_path}");
let mut group = c.benchmark_group("updatedb");
// Full build: walk, front-code and write the whole tree. Empty prune
// options keep the run deterministic regardless of where temp_dir lives.
group.bench_function("build_full", |b| {
b.iter(|| {
run(black_box(&[
&localpaths,
&output,
"--prunepaths=",
"--prunefs=",
]));
});
});
// Build with a prune clause that drops every `dir_0` subtree, exercising the
// -regex/-prune path updatedb hands to find.
group.bench_function("build_pruned", |b| {
b.iter(|| {
run(black_box(&[
&localpaths,
&output,
"--prunepaths=.*/dir_0",
"--prunefs=",
]));
});
});
group.finish();
let _ = std::fs::remove_dir_all(&root);
let _ = std::fs::remove_file(&db);
}
criterion_group!(benches, bench_e2e);
criterion_main!(benches);