use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use mira_core::block;
fn main() {
let mut args = std::env::args().skip(1);
let root = match args.next() {
Some(a) if a != "--help" && a != "-h" => PathBuf::from(a),
_ => {
eprintln!(
"usage: tier <data-dir>\n\nprices the cold tier on every block under <data-dir>"
);
std::process::exit(2);
}
};
let mut by_table: BTreeMap<String, [u64; 3]> = BTreeMap::new();
let mut blocks = 0;
let mut secs = [0f64; 3];
let mut reads = [0f64; 3];
let tmp = std::env::temp_dir().join(format!("mira-tier-{}", std::process::id()));
for path in tables(&root) {
let Ok(table) = block::open_table(&path) else {
eprintln!("skipping unreadable {}", path.display());
continue;
};
let Some(batch) = table.batches.first() else {
continue;
};
blocks += 1;
let row = by_table.entry(key(&root, &path)).or_default();
for (slot, write) in [
block::write_table as fn(&Path, &_) -> _,
block::write_table_zstd,
block::write_table_lz4,
]
.into_iter()
.enumerate()
{
let t0 = std::time::Instant::now();
let ok = write(&tmp, batch).is_ok();
secs[slot] += t0.elapsed().as_secs_f64();
if ok {
row[slot] += std::fs::metadata(&tmp).map(|m| m.len()).unwrap_or(0);
let t0 = std::time::Instant::now();
let back = block::open_table(&tmp);
reads[slot] += t0.elapsed().as_secs_f64();
assert!(back.is_ok(), "{} did not read back", tmp.display());
}
}
}
let _ = std::fs::remove_file(&tmp);
if blocks == 0 {
eprintln!("no readable .arrow tables under {}", root.display());
std::process::exit(1);
}
println!(
"{:<30} {:>12} {:>12} {:>7} {:>12} {:>7}",
"table", "plain", "zstd", "ratio", "lz4", "ratio"
);
let mut tot = [0u64; 3];
let mut sub = [0u64; 3];
let mut signal = String::new();
for (name, cols) in &by_table {
let this = name.split('/').next().unwrap_or_default();
if this != signal {
if !signal.is_empty() {
line(&signal, sub[0], sub[1], sub[2]);
}
signal = this.to_owned();
sub = [0; 3];
}
line(&format!(" {name}"), cols[0], cols[1], cols[2]);
for (i, v) in cols.iter().enumerate() {
sub[i] += v;
tot[i] += v;
}
}
line(&signal, sub[0], sub[1], sub[2]);
println!("{:-<86}", "");
line(&format!("{blocks} tables"), tot[0], tot[1], tot[2]);
let mib = tot[0] as f64 / (1024.0 * 1024.0);
println!(
"\ncompressed at {:.0} MiB/s zstd, {:.0} MiB/s lz4, one core \
({:.1}s and {:.1}s of CPU for {mib:.0} MiB)",
mib / secs[1],
mib / secs[2],
secs[1],
secs[2],
);
println!(
"read back in {:.1}s plain, {:.1}s zstd, {:.1}s lz4 (page-cache-warm, \
{blocks} tables) — zstd is {:.2}x the plain read",
reads[0],
reads[1],
reads[2],
reads[1] / reads[0],
);
}
fn key(root: &Path, path: &Path) -> String {
let rel = path.strip_prefix(root).unwrap_or(path);
let mut parts = rel.components().map(|c| c.as_os_str().to_string_lossy());
let signal = parts.next().unwrap_or_default();
let table = rel.file_name().unwrap_or_default().to_string_lossy();
format!("{signal}/{table}")
}
fn line(name: &str, plain: u64, zstd: u64, lz4: u64) {
let ratio = |c: u64| if c == 0 { 0.0 } else { plain as f64 / c as f64 };
println!(
"{name:<30} {plain:>12} {zstd:>12} {:>6.2}x {lz4:>12} {:>6.2}x",
ratio(zstd),
ratio(lz4)
);
}
fn tables(root: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|e| e == "arrow") {
out.push(path);
}
}
}
out.sort();
out
}