mirror of
https://github.com/uutils/findutils.git
synced 2026-06-10 15:48:30 -07:00
ci: add CodSpeed benchmarks for find and xargs (#703)
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.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
name: CodSpeed
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
pull_request:
|
||||
# `workflow_dispatch` allows CodSpeed to trigger backtest
|
||||
# performance analysis in order to generate initial data.
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
codspeed:
|
||||
name: Run benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust toolchain, cache and cargo-codspeed binary
|
||||
uses: moonrepo/setup-rust@v0
|
||||
with:
|
||||
channel: stable
|
||||
cache-target: release
|
||||
bins: cargo-codspeed
|
||||
|
||||
- name: Build the benchmark target(s)
|
||||
run: cargo codspeed build
|
||||
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@v4
|
||||
with:
|
||||
mode: simulation
|
||||
run: cargo codspeed run
|
||||
Generated
+387
-11
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@ itertools = "0.14.0"
|
||||
thiserror = "2.0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "4.7.0", package = "codspeed-criterion-compat" }
|
||||
assert_cmd = "2"
|
||||
ctor = "1.0"
|
||||
filetime = "0.2"
|
||||
@@ -31,6 +32,14 @@ rstest = "0.25.0"
|
||||
tempfile = "3"
|
||||
uutests = "0.9.0"
|
||||
|
||||
[[bench]]
|
||||
name = "find_bench"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "xargs_bench"
|
||||
harness = false
|
||||
|
||||
[[bin]]
|
||||
name = "find"
|
||||
path = "src/find/main.rs"
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// 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);
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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 `xargs`, run through the real `xargs_main` entry
|
||||
//! point. Input is supplied via `-a <file>` (rather than stdin) so the harness
|
||||
//! can feed a fixed corpus without touching the process's stdin, and the
|
||||
//! command is `true` so the measurement is dominated by `xargs`'s own work
|
||||
//! (reading, splitting and batching arguments) rather than the child.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use findutils::xargs::xargs_main;
|
||||
|
||||
/// Run `xargs` end-to-end. `args` are the arguments after the program name.
|
||||
/// 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("xargs");
|
||||
argv.extend_from_slice(args);
|
||||
let _ = xargs_main(&argv);
|
||||
}
|
||||
|
||||
/// Write `count` short tokens to a temp file, joined by `sep`. Returns the path.
|
||||
fn build_input(count: u32, sep: &str, tag: &str) -> PathBuf {
|
||||
let mut content = String::new();
|
||||
for i in 0..count {
|
||||
if i > 0 {
|
||||
content.push_str(sep);
|
||||
}
|
||||
content.push_str(&format!("item_{i}"));
|
||||
}
|
||||
let path = std::env::temp_dir().join(format!("uu_xargs_bench_{}_{tag}", std::process::id()));
|
||||
std::fs::write(&path, content).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
fn bench_e2e(c: &mut Criterion) {
|
||||
// Whitespace- and newline-separated corpora, plus a NUL-separated one for
|
||||
// the `-0` path. A few thousand short tokens keeps parsing dominant while
|
||||
// `true` batches stay to a handful of (untraced) spawns.
|
||||
let ws = build_input(4000, " ", "ws");
|
||||
let nul = build_input(4000, "\0", "nul");
|
||||
let ws_path = ws.to_str().unwrap();
|
||||
let nul_path = nul.to_str().unwrap();
|
||||
|
||||
let mut group = c.benchmark_group("xargs");
|
||||
|
||||
// Default whitespace splitting, single batch (fits one command line).
|
||||
group.bench_function("split_whitespace", |b| {
|
||||
b.iter(|| run(black_box(&["-a", ws_path, "true"])));
|
||||
});
|
||||
// NUL-delimited input (`find -print0 | xargs -0` shape).
|
||||
group.bench_function("split_null", |b| {
|
||||
b.iter(|| run(black_box(&["-0", "-a", nul_path, "true"])));
|
||||
});
|
||||
// Cap arguments per command with -n, exercising the batching path across
|
||||
// several (untraced) invocations.
|
||||
group.bench_function("batched_n", |b| {
|
||||
b.iter(|| run(black_box(&["-a", ws_path, "-n", "1000", "true"])));
|
||||
});
|
||||
// Bound each command line by size with -s, another batching trigger.
|
||||
group.bench_function("batched_size", |b| {
|
||||
b.iter(|| run(black_box(&["-a", ws_path, "-s", "4096", "true"])));
|
||||
});
|
||||
|
||||
group.finish();
|
||||
|
||||
let _ = std::fs::remove_file(&ws);
|
||||
let _ = std::fs::remove_file(&nul);
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_e2e);
|
||||
criterion_main!(benches);
|
||||
Reference in New Issue
Block a user