use std::path::{Path, PathBuf};
use std::time::Instant;
use disktree::index::{ScanProgress, scan_with_threads};
struct Row {
label: String,
secs: f64,
files: u64,
dirs: u64,
nodes: usize,
bytes: u64,
}
fn report(rows: &[Row]) {
for r in rows {
println!(
" {:<14} {:>8.3} s | {:>9} files {:>8} dirs {:>9} nodes | {:>9.0} files/s {:>8.0} dirs/s | {:.2} GB",
r.label,
r.secs,
r.files,
r.dirs,
r.nodes,
r.files as f64 / r.secs,
r.dirs as f64 / r.secs,
r.bytes as f64 / 1e9,
);
}
}
fn run_one(path: &Path, threads: Option<usize>) -> Row {
let progress = ScanProgress::default();
let start = Instant::now();
let tree = scan_with_threads(path.to_path_buf(), &progress, threads);
let secs = start.elapsed().as_secs_f64();
let root = &tree.nodes[tree.root];
Row {
label: match threads {
Some(n) => format!("threads={n}"),
None => "threads=default".to_owned(),
},
secs,
files: root.n_files,
dirs: tree.nodes.iter().filter(|n| n.is_dir).count() as u64,
nodes: tree.nodes.len(),
bytes: root.size,
}
}
fn split_children(path: &Path) -> (Vec<PathBuf>, Vec<PathBuf>) {
let mut kids: Vec<PathBuf> = std::fs::read_dir(path)
.into_iter()
.flatten()
.flatten()
.filter(|e| {
e.file_type()
.map(|t| t.is_dir() && !t.is_symlink())
.unwrap_or(false)
})
.map(|e| e.path())
.collect();
kids.sort();
let weights: Vec<usize> = kids
.iter()
.map(|k| std::fs::read_dir(k).into_iter().flatten().count().max(1))
.collect();
let mut order: Vec<usize> = (0..kids.len()).collect();
order.sort_by(|&a, &b| weights[b].cmp(&weights[a]));
let (mut a, mut b) = (Vec::new(), Vec::new());
let (mut wa, mut wb) = (0usize, 0usize);
for i in order {
if wa <= wb {
wa += weights[i];
a.push(kids[i].clone());
} else {
wb += weights[i];
b.push(kids[i].clone());
}
}
(a, b)
}
fn scan_group(group: &[PathBuf], threads: usize, label: &str) -> Row {
let progress = ScanProgress::default();
let start = Instant::now();
let mut files = 0;
let mut dirs = 0;
let mut nodes = 0;
let mut bytes = 0;
for p in group {
let tree = scan_with_threads(p.clone(), &progress, Some(threads));
let root = &tree.nodes[tree.root];
files += root.n_files;
bytes += root.size;
nodes += tree.nodes.len();
dirs += tree.nodes.iter().filter(|n| n.is_dir).count() as u64;
}
Row {
label: label.to_owned(),
secs: start.elapsed().as_secs_f64(),
files,
dirs,
nodes,
bytes,
}
}
fn main() {
let argv: Vec<String> = std::env::args().skip(1).collect();
let mut path: Option<String> = None;
let mut runs: Option<usize> = None;
let mut threads_arg: Option<String> = None;
let mut split_arg: Option<String> = None;
let mut i = 0;
while i < argv.len() {
match argv[i].as_str() {
"--threads" => {
threads_arg = argv.get(i + 1).cloned();
i += 2;
}
"--split" => {
split_arg = argv.get(i + 1).cloned();
i += 2;
}
other if other.starts_with("--") => {
eprintln!("unknown flag: {other}");
std::process::exit(2);
}
other => {
if path.is_none() {
path = Some(other.to_owned());
} else if runs.is_none() {
match other.parse() {
Ok(n) => runs = Some(n),
Err(_) => {
eprintln!("expected a run count, got: {other}");
std::process::exit(2);
}
}
} else {
eprintln!("unexpected argument: {other}");
std::process::exit(2);
}
i += 1;
}
}
}
let path = PathBuf::from(path.unwrap_or_else(|| "C:\\".to_owned()));
let runs = runs.unwrap_or(3);
let flag = |name: &str| -> Option<String> {
match name {
"--threads" => threads_arg.clone(),
"--split" => split_arg.clone(),
_ => None,
}
};
println!("target: {}", path.display());
if let Some(spec) = flag("--split") {
let sizes: Vec<usize> = spec
.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect();
let [ta, tb] = match sizes[..] {
[a, b] => [a, b],
_ => {
eprintln!("--split needs exactly two thread counts, e.g. --split 32,128");
std::process::exit(1);
}
};
let (ga, gb) = split_children(&path);
println!(
"split: {} dirs vs {} dirs (one cold pass each)",
ga.len(),
gb.len()
);
let ra = scan_group(&ga, ta, &format!("A threads={ta}"));
let rb = scan_group(&gb, tb, &format!("B threads={tb}"));
report(&[ra, rb]);
println!("compare dirs/s. Node counts must be within ~5% for the pair to be fair.");
return;
}
let threads = flag("--threads").and_then(|t| t.parse::<usize>().ok());
let mut rows = Vec::new();
for _ in 0..runs {
rows.push(run_one(&path, threads));
}
report(&rows);
let best = rows.iter().map(|r| r.secs).fold(f64::MAX, f64::min);
let mean = rows.iter().map(|r| r.secs).sum::<f64>() / rows.len() as f64;
println!("best: {best:.3} s mean: {mean:.3} s");
}