diff --git a/Cargo.lock b/Cargo.lock index d2cf4d7af..4f1786416 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3930,6 +3930,7 @@ dependencies = [ "bigdecimal", "binary-heap-plus", "clap", + "codspeed-divan-compat", "compare", "ctrlc", "fluent", diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index 789f6fec5..816e42d20 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -40,6 +40,21 @@ fluent = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] nix = { workspace = true } +[dev-dependencies] +divan = { workspace = true } +tempfile = { workspace = true } +uucore = { workspace = true, features = [ + "benchmark", + "fs", + "parser", + "version-cmp", + "i18n-collator", +] } + [[bin]] name = "sort" path = "src/main.rs" + +[[bench]] +name = "sort_bench" +harness = false diff --git a/src/uu/sort/benches/sort_bench.rs b/src/uu/sort/benches/sort_bench.rs new file mode 100644 index 000000000..6270339a0 --- /dev/null +++ b/src/uu/sort/benches/sort_bench.rs @@ -0,0 +1,306 @@ +// This file is part of the uutils coreutils package. +// +// 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 std::path::PathBuf; +use uu_sort::uumain; +use uucore::benchmark::{create_test_file, run_util_function}; + +/// Helper function to generate test data from a list of words +fn generate_data_from_words(words: &[&str], num_lines: usize) -> Vec { + let mut data = Vec::new(); + for i in 0..num_lines { + let word = words[i % words.len()]; + let number = i % 1000; + data.extend_from_slice(format!("{word}_{number:03}\n").as_bytes()); + } + data +} + +/// Helper function to generate test data from a list of words without number suffix +fn generate_data_from_words_simple(words: &[&str], num_lines: usize) -> Vec { + let mut data = Vec::new(); + for i in 0..num_lines { + let word = words[i % words.len()]; + data.extend_from_slice(format!("{word}\n").as_bytes()); + } + data +} + +/// Generate test data with ASCII-only text +fn generate_ascii_data(num_lines: usize) -> Vec { + let words = [ + "apple", + "banana", + "cherry", + "date", + "elderberry", + "fig", + "grape", + "honeydew", + "kiwi", + "lemon", + "mango", + "nectarine", + "orange", + "papaya", + "quince", + "raspberry", + "strawberry", + "tangerine", + "ugli", + "vanilla", + "watermelon", + "xigua", + "yellow", + "zucchini", + "avocado", + ]; + + generate_data_from_words(&words, num_lines) +} + +/// Generate test data with accented characters that require locale-aware sorting +fn generate_accented_data(num_lines: usize) -> Vec { + let words = [ + // French words with accents + "café", + "naïve", + "résumé", + "fiancé", + "crème", + "déjà", + "façade", + "château", + "élève", + "côte", + // German words with umlauts + "über", + "Müller", + "schön", + "Köln", + "Düsseldorf", + "Österreich", + "Zürich", + "Mädchen", + "Bär", + "größer", + // Spanish words with tildes and accents + "niño", + "señor", + "año", + "mañana", + "español", + "corazón", + "María", + "José", + "más", + "también", + ]; + + generate_data_from_words(&words, num_lines) +} + +/// Generate test data with mixed ASCII and non-ASCII characters +fn generate_mixed_data(num_lines: usize) -> Vec { + let words = [ + // Mix of ASCII and accented words + "apple", + "café", + "banana", + "naïve", + "cherry", + "résumé", + "date", + "fiancé", + "elderberry", + "crème", + "über", + "grape", + "Müller", + "honeydew", + "schön", + "niño", + "kiwi", + "señor", + "lemon", + "año", + "mango", + "María", + "orange", + "José", + "papaya", + ]; + + generate_data_from_words(&words, num_lines) +} + +/// Generate test data with uppercase/lowercase variations +fn generate_case_sensitive_data(num_lines: usize) -> Vec { + let base_words = [ + "apple", "Apple", "APPLE", "banana", "Banana", "BANANA", "café", "Café", "CAFÉ", "über", + "Über", "ÜBER", + ]; + + generate_data_from_words_simple(&base_words, num_lines) +} + +fn setup_test_file(data: &[u8]) -> PathBuf { + let temp_dir = tempfile::tempdir().unwrap(); + let file_path = create_test_file(data, temp_dir.path()); + // Keep temp_dir alive by leaking it - the OS will clean it up + std::mem::forget(temp_dir); + file_path +} + +/// Benchmark sorting ASCII-only data +#[divan::bench(args = [100_000, 500_000])] +fn sort_ascii_only(bencher: Bencher, num_lines: usize) { + let data = generate_ascii_data(num_lines); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function(uumain, &[file_path.to_str().unwrap()])); + }); +} + +/// Benchmark sorting accented/non-ASCII data +#[divan::bench(args = [100_000, 500_000])] +fn sort_accented_data(bencher: Bencher, num_lines: usize) { + let data = generate_accented_data(num_lines); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function(uumain, &[file_path.to_str().unwrap()])); + }); +} + +/// Benchmark sorting mixed ASCII/non-ASCII data +#[divan::bench(args = [100_000, 500_000])] +fn sort_mixed_data(bencher: Bencher, num_lines: usize) { + let data = generate_mixed_data(num_lines); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function(uumain, &[file_path.to_str().unwrap()])); + }); +} + +/// Benchmark case-sensitive sorting with mixed case data +#[divan::bench(args = [100_000, 500_000])] +fn sort_case_sensitive(bencher: Bencher, num_lines: usize) { + let data = generate_case_sensitive_data(num_lines); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function(uumain, &[file_path.to_str().unwrap()])); + }); +} + +/// Benchmark case-insensitive sorting (fold case) +#[divan::bench(args = [100_000, 500_000])] +fn sort_case_insensitive(bencher: Bencher, num_lines: usize) { + let data = generate_case_sensitive_data(num_lines); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-f", file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark dictionary order sorting (only blanks and alphanumeric) +#[divan::bench(args = [100_000, 500_000])] +fn sort_dictionary_order(bencher: Bencher, num_lines: usize) { + let data = generate_mixed_data(num_lines); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-d", file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark numeric sorting with mixed data +#[divan::bench(args = [100_000, 500_000])] +fn sort_numeric(bencher: Bencher, num_lines: usize) { + let mut data = Vec::new(); + + // Generate numeric data with some text prefixes + for i in 0..num_lines { + let value = (i * 13) % 10000; // Pseudo-random numeric values + data.extend_from_slice(format!("value_{value}\n").as_bytes()); + } + + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-n", file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark reverse sorting with locale-aware data +#[divan::bench(args = [100_000, 500_000])] +fn sort_reverse_locale(bencher: Bencher, num_lines: usize) { + let data = generate_accented_data(num_lines); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-r", file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark sorting with specific key field +#[divan::bench(args = [100_000, 500_000])] +fn sort_key_field(bencher: Bencher, num_lines: usize) { + let mut data = Vec::new(); + + // Generate data with multiple fields + let words = ["café", "naïve", "apple", "über", "banana"]; + for i in 0..num_lines { + let word = words[i % words.len()]; + let num1 = i % 100; + let num2 = (i * 7) % 100; + data.extend_from_slice(format!("{num1}\t{word}\t{num2}\n").as_bytes()); + } + + let file_path = setup_test_file(&data); + + bencher.bench(|| { + // Sort by second field + black_box(run_util_function( + uumain, + &["-k", "2", file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark unique sorting with locale-aware data +#[divan::bench(args = [100_000, 500_000])] +fn sort_unique_locale(bencher: Bencher, num_lines: usize) { + let data = generate_accented_data(num_lines); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + black_box(run_util_function( + uumain, + &["-u", file_path.to_str().unwrap()], + )); + }); +} + +fn main() { + divan::main(); +} diff --git a/src/uu/sort/benches/sort_locale_bench.rs b/src/uu/sort/benches/sort_locale_bench.rs new file mode 100644 index 000000000..6b925ae78 --- /dev/null +++ b/src/uu/sort/benches/sort_locale_bench.rs @@ -0,0 +1,231 @@ +// This file is part of the uutils coreutils package. +// +// 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 std::env; +use std::path::PathBuf; +use uu_sort::uumain; +use uucore::benchmark::{create_test_file, run_util_function}; + +/// Helper function to generate test data from a list of words +fn generate_data_from_words_with_counter(words: &[&str], num_lines: usize) -> Vec { + let mut data = Vec::new(); + for i in 0..num_lines { + let word = words[i % words.len()]; + let line = format!("{word}{i:04}\n"); + data.extend_from_slice(line.as_bytes()); + } + data +} + +fn generate_ascii_data(num_lines: usize) -> Vec { + let mut data = Vec::new(); + for i in 0..num_lines { + let line = format!("line_{:06}\n", (num_lines - i - 1)); + data.extend_from_slice(line.as_bytes()); + } + data +} + +fn generate_mixed_locale_data(num_lines: usize) -> Vec { + let mixed_strings = [ + "zebra", "äpfel", "banana", "öl", "cat", "über", "dog", "zürich", "elephant", "café", + "fish", "naïve", "grape", "résumé", "house", "piñata", + ]; + generate_data_from_words_with_counter(&mixed_strings, num_lines) +} + +fn generate_german_locale_data(num_lines: usize) -> Vec { + let german_words = [ + "Ärger", "Öffnung", "Über", "Zucker", "Bär", "Föhn", "Größe", "Höhe", "Käse", "Löwe", + "Mädchen", "Nüsse", "Röntgen", "Schäfer", "Tür", "Würfel", "ä", "ö", "ü", "ß", "a", "o", + "u", "s", + ]; + generate_data_from_words_with_counter(&german_words, num_lines) +} + +fn generate_random_strings(num_lines: usize, length: usize) -> Vec { + let mut data = Vec::new(); + let charset = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789äöüÄÖÜßéèêëàâîïôûç"; + let charset_bytes = charset.as_bytes(); + + for i in 0..num_lines { + let mut line = String::new(); + for j in 0..length { + let idx = ((i * length + j) * 17 + 42) % charset_bytes.len(); + line.push(charset_bytes[idx] as char); + } + line.push('\n'); + data.extend_from_slice(line.as_bytes()); + } + data +} + +fn setup_test_file(data: &[u8]) -> PathBuf { + let temp_dir = tempfile::tempdir().unwrap(); + let file_path = create_test_file(data, temp_dir.path()); + // Keep temp_dir alive by leaking it - the OS will clean it up + std::mem::forget(temp_dir); + file_path +} + +/// Benchmark ASCII-only data sorting with C locale (byte comparison) +#[divan::bench] +fn sort_ascii_c_locale(bencher: Bencher) { + let temp_dir = tempfile::tempdir().unwrap(); + let data = generate_ascii_data(100_000); + let file_path = create_test_file(&data, temp_dir.path()); + + bencher.bench(|| { + unsafe { + env::set_var("LC_ALL", "C"); + } + black_box(run_util_function(uumain, &[file_path.to_str().unwrap()])); + }); +} + +/// Benchmark ASCII-only data sorting with UTF-8 locale +#[divan::bench] +fn sort_ascii_utf8_locale(bencher: Bencher) { + let data = generate_ascii_data(10_000); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + unsafe { + env::set_var("LC_ALL", "en_US.UTF-8"); + } + black_box(run_util_function(uumain, &[file_path.to_str().unwrap()])); + }); +} + +/// Benchmark mixed ASCII/Unicode data with C locale +#[divan::bench] +fn sort_mixed_c_locale(bencher: Bencher) { + let data = generate_mixed_locale_data(10_000); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + unsafe { + env::set_var("LC_ALL", "C"); + } + black_box(run_util_function(uumain, &[file_path.to_str().unwrap()])); + }); +} + +/// Benchmark mixed ASCII/Unicode data with UTF-8 locale +#[divan::bench] +fn sort_mixed_utf8_locale(bencher: Bencher) { + let data = generate_mixed_locale_data(10_000); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + unsafe { + env::set_var("LC_ALL", "en_US.UTF-8"); + } + black_box(run_util_function(uumain, &[file_path.to_str().unwrap()])); + }); +} + +/// Benchmark German locale-specific data with C locale +#[divan::bench] +fn sort_german_c_locale(bencher: Bencher) { + let data = generate_german_locale_data(10_000); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + unsafe { + env::set_var("LC_ALL", "C"); + } + black_box(run_util_function(uumain, &[file_path.to_str().unwrap()])); + }); +} + +/// Benchmark German locale-specific data with German locale +#[divan::bench] +fn sort_german_locale(bencher: Bencher) { + let data = generate_german_locale_data(10_000); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + unsafe { + env::set_var("LC_ALL", "de_DE.UTF-8"); + } + black_box(run_util_function(uumain, &[file_path.to_str().unwrap()])); + }); +} + +/// Benchmark random strings of different lengths +#[divan::bench] +fn sort_random_strings(bencher: Bencher) { + let data = generate_random_strings(10_000, 50); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + unsafe { + env::set_var("LC_ALL", "en_US.UTF-8"); + } + black_box(run_util_function(uumain, &[file_path.to_str().unwrap()])); + }); +} + +/// Benchmark numeric sorting performance +#[divan::bench] +fn sort_numeric(bencher: Bencher) { + let mut data = Vec::new(); + for i in 0..10_000 { + let line = format!("{}\n", 10_000 - i); + data.extend_from_slice(line.as_bytes()); + } + let file_path = setup_test_file(&data); + + bencher.bench(|| { + unsafe { + env::set_var("LC_ALL", "en_US.UTF-8"); + } + black_box(run_util_function( + uumain, + &["-n", file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark reverse sorting +#[divan::bench] +fn sort_reverse_mixed(bencher: Bencher) { + let data = generate_mixed_locale_data(10_000); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + unsafe { + env::set_var("LC_ALL", "en_US.UTF-8"); + } + black_box(run_util_function( + uumain, + &["-r", file_path.to_str().unwrap()], + )); + }); +} + +/// Benchmark unique sorting +#[divan::bench] +fn sort_unique_mixed(bencher: Bencher) { + let data = generate_mixed_locale_data(10_000); + let file_path = setup_test_file(&data); + + bencher.bench(|| { + unsafe { + env::set_var("LC_ALL", "en_US.UTF-8"); + } + black_box(run_util_function( + uumain, + &["-u", file_path.to_str().unwrap()], + )); + }); +} + +fn main() { + divan::main(); +}