use std::io::{Read, Write};
use std::process::{Command, Stdio};
use std::time::Instant;
use compcol::{Status, factory};
const LOREM: &[u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad \
minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea. ";
#[inline]
fn lcg(state: &mut u64) -> u64 {
*state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*state
}
fn fill(kind: &str, abs: u64, buf: &mut [u8]) {
match kind {
"text" => {
for (i, b) in buf.iter_mut().enumerate() {
let p = (abs + i as u64) % LOREM.len() as u64;
*b = LOREM[p as usize];
}
}
"seq" => {
for (i, b) in buf.iter_mut().enumerate() {
*b = (abs + i as u64) as u8;
}
}
"random" => {
for (i, b) in buf.iter_mut().enumerate() {
let pos = abs + i as u64;
let mut s = pos.wrapping_mul(0x9E3779B97F4A7C15) ^ 0xD1B54A32D192ED03;
s = lcg(&mut s);
*b = (s >> 33) as u8;
}
}
"runs" => {
for (i, b) in buf.iter_mut().enumerate() {
let pos = abs + i as u64;
*b = (pos >> 12) as u8;
}
}
"mixed" => {
for (i, b) in buf.iter_mut().enumerate() {
let pos = abs + i as u64;
if (pos >> 16) & 1 == 0 {
*b = LOREM[(pos % LOREM.len() as u64) as usize];
} else {
let mut s = pos.wrapping_mul(0x9E3779B97F4A7C15) ^ 0xD1B54A32D192ED03;
s = lcg(&mut s);
*b = (s >> 33) as u8;
}
}
}
_ => panic!("unknown kind {kind}"),
}
}
#[derive(Clone, Copy)]
struct Hash {
a: u64,
b: u64,
len: u64,
}
impl Hash {
fn new() -> Self {
Self {
a: 0xcbf29ce484222325,
b: 0x100000001b3 ^ 0x9E3779B97F4A7C15,
len: 0,
}
}
#[inline]
fn update(&mut self, data: &[u8]) {
let mut a = self.a;
let mut b = self.b;
for &x in data {
a = (a ^ x as u64).wrapping_mul(0x100000001b3);
b = (b ^ x as u64)
.wrapping_mul(0x9E3779B97F4A7C15)
.rotate_left(27);
}
self.a = a;
self.b = b;
self.len += data.len() as u64;
}
fn eq(&self, o: &Hash) -> bool {
self.a == o.a && self.b == o.b && self.len == o.len
}
fn show(&self) -> String {
format!("{:016x}{:016x}/{}", self.a, self.b, self.len)
}
}
const CHUNK: usize = 1 << 20; const BUF: usize = 1 << 18;
fn mb(bytes: u64) -> f64 {
bytes as f64 / 1e6
}
fn run_self(algo: &str, kind: &str, total: u64) -> Result<(), String> {
let mut enc = factory::encoder_by_name(algo).ok_or("unknown algo (encode)")?;
let mut dec = factory::decoder_by_name(algo).ok_or("unknown algo (decode)")?;
let mut hin = Hash::new();
let mut hout = Hash::new();
let mut comp_bytes: u64 = 0;
let mut enc_buf = vec![0u8; BUF];
let mut dec_buf = vec![0u8; BUF];
let mut genbuf = vec![0u8; CHUNK];
let feed = |dec: &mut Box<dyn compcol::Decoder>,
comp: &[u8],
hout: &mut Hash,
dec_buf: &mut [u8]|
-> Result<(), String> {
let mut c = 0;
while c < comp.len() {
let (p, status) = dec
.decode(&comp[c..], dec_buf)
.map_err(|e| format!("decode: {e}"))?;
c += p.consumed;
hout.update(&dec_buf[..p.written]);
match status {
Status::InputEmpty | Status::StreamEnd => {
if p.consumed == 0 && p.written == 0 {
break;
}
}
Status::OutputFull => {}
}
}
Ok(())
};
let t0 = Instant::now();
let mut produced: u64 = 0;
let mut next_report = 1u64 << 30;
while produced < total {
let n = CHUNK.min((total - produced) as usize);
fill(kind, produced, &mut genbuf[..n]);
hin.update(&genbuf[..n]);
let mut consumed = 0;
while consumed < n {
let (p, status) = enc
.encode(&genbuf[consumed..n], &mut enc_buf)
.map_err(|e| format!("encode: {e}"))?;
consumed += p.consumed;
comp_bytes += p.written as u64;
feed(&mut dec, &enc_buf[..p.written], &mut hout, &mut dec_buf)?;
match status {
Status::InputEmpty | Status::StreamEnd => {
if p.consumed == 0 && p.written == 0 {
break;
}
}
Status::OutputFull => {}
}
}
produced += n as u64;
if produced >= next_report {
let s = t0.elapsed().as_secs_f64();
eprintln!(
" .. {} GiB in, {:.1} MB comp, {:.0} MB/s",
produced >> 30,
mb(comp_bytes),
mb(produced) / s
);
next_report += 1 << 30;
}
}
loop {
let (p, status) = enc
.finish(&mut enc_buf)
.map_err(|e| format!("finish enc: {e}"))?;
comp_bytes += p.written as u64;
feed(&mut dec, &enc_buf[..p.written], &mut hout, &mut dec_buf)?;
if matches!(status, Status::StreamEnd) {
break;
}
if p.written == 0 {
break;
}
}
loop {
let (p, _s) = dec
.decode(&[], &mut dec_buf)
.map_err(|e| format!("drain dec: {e}"))?;
hout.update(&dec_buf[..p.written]);
if p.written == 0 {
break;
}
}
loop {
let (p, status) = dec
.finish(&mut dec_buf)
.map_err(|e| format!("finish dec: {e}"))?;
hout.update(&dec_buf[..p.written]);
if matches!(status, Status::StreamEnd) || p.written == 0 {
break;
}
}
let s = t0.elapsed().as_secs_f64();
let ok = hin.eq(&hout);
println!(
"{algo} {kind} {}B comp={}B ratio={:.4} {:.0} MB/s {}",
total,
comp_bytes,
comp_bytes as f64 / total as f64,
mb(total) / s,
if ok { "OK" } else { "MISMATCH" }
);
if !ok {
println!(" in ={}", hin.show());
println!(" out={}", hout.show());
return Err(format!(
"ROUND-TRIP MISMATCH: in.len={} out.len={}",
hin.len, hout.len
));
}
Ok(())
}
fn run_enc_native(algo: &str, kind: &str, total: u64) -> Result<(), String> {
if algo != "xz" {
return Err("enc-native only supports xz".into());
}
let mut hin = Hash::new();
let mut genbuf = vec![0u8; CHUNK];
let mut produced = 0u64;
while produced < total {
let n = CHUNK.min((total - produced) as usize);
fill(kind, produced, &mut genbuf[..n]);
hin.update(&genbuf[..n]);
produced += n as u64;
}
let mut child = Command::new("xz")
.args(["-d", "-c", "-T", "1"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("spawn xz: {e}"))?;
let mut xz_in = child.stdin.take().unwrap();
let mut xz_out = child.stdout.take().unwrap();
let algo_t = algo.to_string();
let kind_s = kind.to_string();
let writer = std::thread::spawn(move || -> Result<u64, String> {
let mut enc = factory::encoder_by_name(&algo_t).ok_or("unknown algo")?;
let mut enc_buf = vec![0u8; BUF];
let mut genbuf = vec![0u8; CHUNK];
let mut comp = 0u64;
let mut produced = 0u64;
while produced < total {
let n = CHUNK.min((total - produced) as usize);
fill(&kind_s, produced, &mut genbuf[..n]);
let mut consumed = 0;
while consumed < n {
let (p, status) = enc
.encode(&genbuf[consumed..n], &mut enc_buf)
.map_err(|e| format!("encode: {e}"))?;
consumed += p.consumed;
comp += p.written as u64;
xz_in
.write_all(&enc_buf[..p.written])
.map_err(|e| format!("pipe: {e}"))?;
if matches!(status, Status::InputEmpty | Status::StreamEnd)
&& p.consumed == 0
&& p.written == 0
{
break;
}
}
produced += n as u64;
}
loop {
let (p, status) = enc
.finish(&mut enc_buf)
.map_err(|e| format!("finish: {e}"))?;
comp += p.written as u64;
xz_in
.write_all(&enc_buf[..p.written])
.map_err(|e| format!("pipe: {e}"))?;
if matches!(status, Status::StreamEnd) || p.written == 0 {
break;
}
}
drop(xz_in);
Ok(comp)
});
let mut hout = Hash::new();
let mut rbuf = vec![0u8; BUF];
loop {
let k = xz_out
.read(&mut rbuf)
.map_err(|e| format!("read xz: {e}"))?;
if k == 0 {
break;
}
hout.update(&rbuf[..k]);
}
let comp = writer.join().map_err(|_| "writer panicked")??;
let status = child.wait().map_err(|e| format!("wait xz: {e}"))?;
if !status.success() {
return Err("native xz -d rejected our output".into());
}
let ok = hin.eq(&hout);
println!(
"{algo} {kind} {}B (our-enc -> native xz -d) comp={}B {}",
total,
comp,
if ok { "OK" } else { "MISMATCH" }
);
if !ok {
return Err(format!("MISMATCH: in.len={} out.len={}", hin.len, hout.len));
}
Ok(())
}
fn main() {
let a: Vec<String> = std::env::args().collect();
if a.len() < 5 {
eprintln!(
"usage: stress <algo> <text|random|mixed|runs|seq> <total_bytes> <self|enc-native>"
);
std::process::exit(2);
}
let (algo, kind, mode) = (&a[1], &a[2], &a[4]);
let total: u64 = a[3].parse().expect("total_bytes");
let r = match mode.as_str() {
"self" => run_self(algo, kind, total),
"enc-native" => run_enc_native(algo, kind, total),
_ => Err(format!("unknown mode {mode}")),
};
if let Err(e) = r {
eprintln!("FAIL: {e}");
std::process::exit(1);
}
}