Add grep benchmarks and CodSpeed CI workflow

Add a divan-based benchmark suite (benches/grep_bench.rs) modeled on the
sed benchmarks, covering literal/fixed-string/regex search, case-insensitive
matching, counting, inversion, line numbers, word/whole-line matching, and
quiet mode. Wire up the dev-dependencies (codspeed-divan-compat, tempfile,
uucore benchmark feature) and a [[bench]] entry.

Add a Benchmarks GitHub Actions workflow running the suite through CodSpeed,
adapted from the sed setup.
This commit is contained in:
Sylvestre Ledru
2026-05-31 09:43:07 +02:00
parent 2a6a3aba7e
commit 3dde7c23c5
4 changed files with 614 additions and 11 deletions
+255
View File
@@ -0,0 +1,255 @@
// Benchmarks for the grep utility
//
// SPDX-License-Identifier: MIT
//
// This file is part of the uutils grep package.
// It is licensed under the MIT License.
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use divan::{Bencher, black_box};
use uu_grep::uumain;
use uucore::benchmark::{create_test_file, run_util_function};
/// Build an access-log-like data set with `n` lines.
///
/// Roughly a quarter of the lines use a non-default HTTP method / status /
/// user-agent so that selective patterns match a realistic subset rather than
/// every line or no line at all.
fn access_log(n: usize) -> Vec<u8> {
let mut data = Vec::new();
for i in 0..n {
let method = if i % 4 == 0 { "POST" } else { "GET" };
let status = if i % 7 == 0 { 404 } else { 200 };
let agent = if i % 3 == 0 {
"Mozilla/5.0 (X11; Linux x86_64) Chrome/120.0"
} else {
"curl/8.5.0"
};
let line = format!(
"192.168.{}.{} - - [01/Jan/2024:00:00:00 +0000] \"{} /index.html HTTP/1.1\" {} 1234 \"-\" \"{}\"\n",
(i / 256) % 256,
i % 256,
method,
status,
agent,
);
data.extend_from_slice(line.as_bytes());
}
data
}
/// Benchmark a literal search that matches nothing.
///
/// This is the purest measure of raw scan throughput: the whole file is read
/// and searched but no output is produced.
#[divan::bench]
fn literal_no_match(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(2_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(
uumain,
&["ZZZ_NONEXISTENT_PATTERN_ZZZ", file_path_str],
));
});
}
/// Benchmark a literal search that matches a subset of lines.
#[divan::bench]
fn literal_match_some(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(2_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(uumain, &["POST", file_path_str]));
});
}
/// Benchmark a literal search that matches every line (counting only).
///
/// `-c` keeps the output bounded so the benchmark measures matching rather than
/// terminal I/O.
#[divan::bench]
fn literal_match_all_count(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(2_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(uumain, &["-c", "HTTP", file_path_str]));
});
}
/// Benchmark a fixed-string search (`-F`).
#[divan::bench]
fn fixed_string(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(2_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(
uumain,
&["-F", "Chrome/120.0", file_path_str],
));
});
}
/// Benchmark a case-insensitive search (`-i`).
#[divan::bench]
fn case_insensitive(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(2_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(uumain, &["-i", "mozilla", file_path_str]));
});
}
/// Benchmark counting matches (`-c`).
#[divan::bench]
fn count(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(2_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(uumain, &["-c", "POST", file_path_str]));
});
}
/// Benchmark an inverted match (`-v`).
///
/// Most lines do not contain "POST", so this selects the majority of lines;
/// `-c` bounds the output.
#[divan::bench]
fn invert_match_count(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(2_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(uumain, &["-vc", "POST", file_path_str]));
});
}
/// Benchmark printing line numbers (`-n`).
#[divan::bench]
fn line_number(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(1_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(uumain, &["-nc", "POST", file_path_str]));
});
}
/// Benchmark word-boundary matching (`-w`).
#[divan::bench]
fn word_match(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(2_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(uumain, &["-wc", "GET", file_path_str]));
});
}
/// Benchmark an extended regular expression with alternation (`-E`).
#[divan::bench]
fn extended_regex(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(2_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(
uumain,
&["-Ec", "(POST|DELETE|PUT)", file_path_str],
));
});
}
/// Benchmark a basic regular expression with an anchor and character class.
#[divan::bench]
fn basic_regex(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(2_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(
uumain,
&["-c", "^192\\.168\\.[0-9]*\\.0 ", file_path_str],
));
});
}
/// Benchmark a Perl-compatible regular expression (`-P`).
#[divan::bench]
fn perl_regex(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(2_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(
uumain,
&["-Pc", "\"\\d{3}\" \\d+", file_path_str],
));
});
}
/// Benchmark `--only-matching` (`-o`) extracting a substring from each line.
#[divan::bench]
fn only_matching(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(1_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(
uumain,
&["-Eoc", "[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+", file_path_str],
));
});
}
/// Benchmark quiet mode (`-q`), which can stop at the first match.
#[divan::bench]
fn quiet_first_match(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = create_test_file(&access_log(2_000_000), temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(uumain, &["-q", "POST", file_path_str]));
});
}
/// Benchmark searching short numeric lines (many small lines).
#[divan::bench]
fn short_lines(bencher: Bencher) {
let temp_dir = tempfile::tempdir().unwrap();
let mut data = Vec::new();
for i in 0..10_000_000 {
data.extend_from_slice(format!("{i}\n").as_bytes());
}
let file_path = create_test_file(&data, temp_dir.path());
let file_path_str = file_path.to_str().unwrap();
bencher.bench(|| {
black_box(run_util_function(uumain, &["-c", "999", file_path_str]));
});
}
fn main() {
divan::main();
}