From fc446aba0b1e5e00c3c710ff0c223ae41aaa9c7b Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 6 Jun 2026 11:00:04 +0200 Subject: [PATCH] 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. --- Cargo.lock | 25 +++++++- Cargo.toml | 8 +++ benches/locate_bench.rs | 127 ++++++++++++++++++++++++++++++++++++++ benches/updatedb_bench.rs | 110 +++++++++++++++++++++++++++++++++ 4 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 benches/locate_bench.rs create mode 100644 benches/updatedb_bench.rs diff --git a/Cargo.lock b/Cargo.lock index 97955ef..9631e6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index cf783c6..3538b4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/benches/locate_bench.rs b/benches/locate_bench.rs new file mode 100644 index 0000000..2c4c473 --- /dev/null +++ b/benches/locate_bench.rs @@ -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() {} diff --git a/benches/updatedb_bench.rs b/benches/updatedb_bench.rs new file mode 100644 index 0000000..20ed69c --- /dev/null +++ b/benches/updatedb_bench.rs @@ -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);