Skip to content
Open
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 @@ -34,6 +34,11 @@ fn du_balanced_tree(

/* too much variance
/// 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 @@ -75,6 +80,11 @@ fn du_wide_tree(bencher: Bencher, (total_files, total_dirs): (usize, usize)) {
}

/// 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)) {
bencher
Expand Down Expand Up @@ -128,6 +138,36 @@ fn du_max_depth_balanced_tree(
bench_du_with_args(bencher, &temp_dir, &["--max-depth=2"]);
}

/// STRESS TEST: Benchmark du -a on large directory structure
/// MEASURES BUFWRITER SCALING: Tests if 64KiB 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 (64KiB 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\
/// MEASURES BUFWRIDER UNDER EXTREME LOAD: Tests worst-case for stdout frequency
///
/// EXPECTED: ~2,500 entries = ~7,500 potential stdout writes without buffering
/// 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();
}
40 changes: 27 additions & 13 deletions src/uu/du/src/du.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::collections::HashSet;
use std::env;
use std::ffi::{OsStr, OsString};
use std::fs::{self, DirEntry, File, Metadata};
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 @@ -21,7 +21,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 @@ -95,6 +95,7 @@ struct StatPrinter {
line_ending: LineEnding,
summarize: bool,
total_text: String,
writer: BufWriter<std::io::Stdout>, // 64KiB buffer for efficient output
}

#[derive(PartialEq, Clone)]
Expand Down Expand Up @@ -816,7 +817,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 @@ -848,8 +849,13 @@ impl StatPrinter {
}

if self.total {
print!("{}\t{}", self.convert_size(grand_total), self.total_text);
print!("{}", self.line_ending);
write!(
self.writer,
"{}\t{}",
self.convert_size(grand_total),
self.total_text
)?;
write!(self.writer, "{}", self.line_ending)?;
}

Ok(())
Expand All @@ -876,30 +882,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 @@ -1046,7 +1059,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 @@ -1065,6 +1078,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()), // 64KiB buffer
};

if stat_printer.inodes
Expand Down
Loading