Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/uu/du/benches/du_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
}

/// Benchmark du -a (all files) on balanced tree
/// MEASURES BUG #9146: stdout buffering inefficiency
///
/// CURRENT: Each of ~81 entries triggers 3 stdout writes = ~243 syscalls.
/// With BufWriter, this becomes ~3-5 buffered flushes total.
/// Performance should improve significantly after fix.
#[divan::bench(args = [(4, 3, 10)])]
fn du_all_balanced_tree(
bencher: Bencher,
Expand Down Expand Up @@ -61,6 +66,11 @@
}

/// Benchmark du -a on wide directory structures
/// MEASURES BUG #9146: stdout buffering inefficiency
///
/// CURRENT: Each of ~5,500 entries (5,000 files + 500 dirs) triggers 3 stdout writes
/// = ~16,500 syscalls. With BufWriter, this becomes ~3-5 buffered flushes total.
/// Performance should improve dramatically after fix.
#[divan::bench(args = [(5000, 500)])]
fn du_all_wide_tree(bencher: Bencher, (total_files, total_dirs): (usize, usize)) {
let temp_dir = TempDir::new().unwrap();
Expand Down Expand Up @@ -98,6 +108,36 @@
bench_du_with_args(bencher, &temp_dir, &["--max-depth=2"]);
}

/// STRESS TEST: Benchmark du -a on large directory structure
/// MEASURES BUFWRITER SCALING: Tests if 64KB buffer benefits scale linearly
///
/// EXPECTED: ~161 entries (dirs+files) with ~483 potential stdout writes
/// With BufWriter: Should complete with ~3-5 syscalls regardless of entry count
/// This test validates that the optimization scales to larger real-world directories
/// and that memory usage remains bounded (64KB buffer).
#[divan::bench(args = [(3, 5, 6)])]
fn du_all_stress_balanced_tree(
bencher: Bencher,
(depth, dirs_per_level, files_per_dir): (usize, usize, usize),
) {
let temp_dir = TempDir::new().unwrap();
fs_tree::create_balanced_tree(temp_dir.path(), depth, dirs_per_level, files_per_dir);
bench_du_with_args(bencher, &temp_dir, &["-a"]);
}

/// STRESS TEST: Benchmark du -a on extremely wide directory structure

Check failure on line 128 in src/uu/du/benches/du_bench.rs

View workflow job for this annotation

GitHub Actions / Style and Lint (ubuntu-24.04, unix)

ERROR: `cargo clippy`: doc comment uses two spaces for a hard line break (file:'src/uu/du/benches/du_bench.rs', line:128)

Check failure on line 128 in src/uu/du/benches/du_bench.rs

View workflow job for this annotation

GitHub Actions / Style and Lint (ubuntu-24.04, unix)

ERROR: `cargo clippy`: doc comment uses two spaces for a hard line break (file:'src/uu/du/benches/du_bench.rs', line:128)
/// MEASURES BUFWRIDER UNDER EXTREME LOAD: Tests worst-case for stdout frequency
///
/// EXPECTED: ~2,500 entries = ~7,500 potential stdout writes without buffering

Check failure on line 131 in src/uu/du/benches/du_bench.rs

View workflow job for this annotation

GitHub Actions / Style and Lint (ubuntu-24.04, unix)

ERROR: `cargo clippy`: using tabs in doc comments is not recommended (file:'src/uu/du/benches/du_bench.rs', line:131)

Check failure on line 131 in src/uu/du/benches/du_bench.rs

View workflow job for this annotation

GitHub Actions / Style and Lint (ubuntu-24.04, unix)

ERROR: `cargo clippy`: using tabs in doc comments is not recommended (file:'src/uu/du/benches/du_bench.rs', line:131)
/// This is the scenario that most directly exposes the issue #9146 performance bottleneck.
/// Success is measured not just by time, but by consistent performance regardless of entry count.
#[divan::bench(args = [(2000, 500)])]
fn du_all_extreme_wide_tree(bencher: Bencher, (total_files, total_dirs): (usize, usize)) {
let temp_dir = TempDir::new().unwrap();
fs_tree::create_wide_tree(temp_dir.path(), total_files, total_dirs);
bench_du_with_args(bencher, &temp_dir, &["-a"]);
}

fn main() {
divan::main();
}
31 changes: 20 additions & 11 deletions src/uu/du/src/du.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::ffi::OsStr;
use std::ffi::OsString;
use std::fs::Metadata;
use std::fs::{self, DirEntry, File};
use std::io::{BufRead, BufReader, stdout};
use std::io::{BufRead, BufReader, BufWriter, Write, stdout};
#[cfg(not(windows))]
use std::os::unix::fs::MetadataExt;
#[cfg(windows)]
Expand All @@ -22,7 +22,7 @@ use std::str::FromStr;
use std::sync::mpsc;
use std::thread;
use thiserror::Error;
use uucore::display::{Quotable, print_verbatim};
use uucore::display::Quotable;
use uucore::error::{FromIo, UError, UResult, USimpleError, set_exit_code};
use uucore::fsext::{MetadataTimeField, metadata_get_time};
use uucore::line_ending::LineEnding;
Expand Down Expand Up @@ -96,6 +96,7 @@ struct StatPrinter {
line_ending: LineEnding,
summarize: bool,
total_text: String,
writer: BufWriter<std::io::Stdout>,
}

#[derive(PartialEq, Clone)]
Expand Down Expand Up @@ -820,7 +821,7 @@ impl StatPrinter {
}
}

fn print_stats(&self, rx: &mpsc::Receiver<UResult<StatPrintInfo>>) -> UResult<()> {
fn print_stats(&mut self, rx: &mpsc::Receiver<UResult<StatPrintInfo>>) -> UResult<()> {
let mut grand_total = 0;
loop {
let received = rx.recv();
Expand Down Expand Up @@ -880,30 +881,37 @@ impl StatPrinter {
}
}

fn print_stat(&self, stat: &Stat, size: u64) -> UResult<()> {
print!("{}\t", self.convert_size(size));
fn print_stat(&mut self, stat: &Stat, size: u64) -> UResult<()> {
write!(self.writer, "{}\t", self.convert_size(size))?;

if let Some(md_time) = &self.time {
if let Some(time) = metadata_get_time(&stat.metadata, *md_time) {
format_system_time(
&mut stdout(),
&mut self.writer,
time,
&self.time_format,
FormatSystemTimeFallback::IntegerError,
)?;
print!("\t");
write!(self.writer, "\t")?;
} else {
print!("???\t");
write!(self.writer, "???\t")?;
}
}

print_verbatim(&stat.path).unwrap();
print!("{}", self.line_ending);
self.writer
.write_all(stat.path.as_os_str().as_encoded_bytes())?;
write!(self.writer, "{}", self.line_ending)?;

Ok(())
}
}

impl Drop for StatPrinter {
fn drop(&mut self) {
let _ = self.writer.flush();
}
}

/// Read file paths from the specified file, separated by null characters
fn read_files_from(file_name: &OsStr) -> Result<Vec<PathBuf>, std::io::Error> {
let reader: Box<dyn BufRead> = if file_name == "-" {
Expand Down Expand Up @@ -1048,7 +1056,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
format::LONG_ISO.to_string()
};

let stat_printer = StatPrinter {
let mut stat_printer = StatPrinter {
max_depth,
size_format,
summarize,
Expand All @@ -1067,6 +1075,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
time_format,
line_ending: LineEnding::from_zero_flag(matches.get_flag(options::NULL)),
total_text: translate!("du-total"),
writer: BufWriter::with_capacity(65536, stdout()),
};

if stat_printer.inodes
Expand Down
Loading