use std::io::Cursor;
use std::time::Instant;
use zesven::write::{WriteOptions, Writer};
use zesven::{ArchivePath, WriteFilter};
struct Measurement {
name: &'static str,
wall: f64,
single_entry_wall: f64,
entries: usize,
input: usize,
output: usize,
}
impl Measurement {
fn throughput(&self) -> f64 {
self.input as f64 / self.wall / (1024.0 * 1024.0)
}
fn ratio(&self) -> f64 {
self.output as f64 / self.input as f64
}
fn speedup(&self) -> f64 {
self.single_entry_wall * self.entries as f64 / self.wall
}
}
fn compressible(len: usize) -> Vec<u8> {
const KEYWORDS: [&str; 12] = [
"let", "fn", "pub", "struct", "impl", "match", "return", "if", "for", "while", "mut",
"const",
];
const NAMES: [&str; 12] = [
"options",
"encoder",
"dictionary",
"buffer",
"entry",
"folder",
"stream",
"checksum",
"header",
"writer",
"payload",
"archive",
];
let mut data = Vec::with_capacity(len);
let mut state = 0x9E37_79B9_7F4A_7C15u64;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
while data.len() < len {
let r = next();
let line = match r % 5 {
0 => format!(
" {} {}_{} = {};\n",
KEYWORDS[(r >> 8) as usize % 12],
NAMES[(r >> 16) as usize % 12],
r % 1000,
r % 65536,
),
1 => format!(
"pub fn {}_{}(&self, {}: &[u8]) -> Result<{}> {{\n",
NAMES[(r >> 8) as usize % 12],
r % 100,
NAMES[(r >> 24) as usize % 12],
NAMES[(r >> 32) as usize % 12],
),
2 => format!(
" // {} the {} before {} it\n",
KEYWORDS[(r >> 8) as usize % 12],
NAMES[(r >> 16) as usize % 12],
NAMES[(r >> 24) as usize % 12],
),
3 => format!(" {}({});\n", NAMES[(r >> 8) as usize % 12], r % 4096),
_ => " }\n".to_string(),
};
data.extend_from_slice(line.as_bytes());
}
data.truncate(len);
data
}
fn incompressible(len: usize) -> Vec<u8> {
let mut data = Vec::with_capacity(len);
let mut state = 0x2545_F491_4F6C_DD1Du64;
while data.len() < len {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
data.extend_from_slice(&state.to_le_bytes());
}
data.truncate(len);
data
}
fn write_entries(options: WriteOptions, data: &[u8], count: usize) -> (f64, usize) {
let start = Instant::now();
let mut writer = Writer::create(Cursor::new(Vec::with_capacity(data.len() * count)))
.unwrap()
.options(options);
for i in 0..count {
writer
.add_bytes(
ArchivePath::new(&format!("entry-{i:03}.bin")).unwrap(),
data,
)
.unwrap();
}
let (result, _cursor) = writer.finish_into_inner().unwrap();
(
start.elapsed().as_secs_f64(),
result.compressed_size as usize,
)
}
fn measure(name: &'static str, options: WriteOptions, data: &[u8], count: usize) -> Measurement {
let (single_entry_wall, _) = write_entries(options.clone(), data, 1);
let (wall, output) = write_entries(options, data, count);
Measurement {
name,
wall,
single_entry_wall,
entries: count,
input: data.len() * count,
output,
}
}
struct Expectation {
max_ratio: f64,
min_speedup: f64,
min_throughput: f64,
}
fn main() {
let cores = std::thread::available_parallelism().map_or(1, |n| n.get());
let entries = (cores * 2).clamp(8, 32);
println!("{cores} cores, {entries} entries of 4 MB each\n");
let source = compressible(4 << 20);
let opaque = incompressible(4 << 20);
let cases: Vec<(Measurement, Expectation)> = vec![
(
measure(
"non-solid, level 5, compressible",
WriteOptions::new(),
&source,
entries,
),
Expectation {
max_ratio: 0.14,
min_speedup: 1.5,
min_throughput: 2.0,
},
),
(
measure(
"non-solid, level 1, compressible",
WriteOptions::new().level(1).unwrap(),
&source,
entries,
),
Expectation {
max_ratio: 0.21,
min_speedup: 1.5,
min_throughput: 5.0,
},
),
(
measure(
"non-solid, level 5, incompressible",
WriteOptions::new(),
&opaque,
entries,
),
Expectation {
max_ratio: 1.01,
min_speedup: 1.5,
min_throughput: 1.5,
},
),
(
measure(
"solid, level 5, compressible",
WriteOptions::new().solid(),
&source,
entries,
),
Expectation {
max_ratio: 0.07,
min_speedup: 1.1,
min_throughput: 2.0,
},
),
(
measure(
"non-solid, delta filter, compressible",
WriteOptions::new().filter(WriteFilter::delta(4)),
&source,
entries,
),
Expectation {
max_ratio: 0.17,
min_speedup: 1.5,
min_throughput: 2.0,
},
),
];
println!(
"{:<40} {:>8} {:>9} {:>9} {:>7}",
"scenario", "MB/s", "ratio", "speedup", "wall"
);
for (m, _) in &cases {
println!(
"{:<40} {:>8.1} {:>9.4} {:>8.1}x {:>6.2}s",
m.name,
m.throughput(),
m.ratio(),
m.speedup(),
m.wall,
);
}
println!();
let mut failures = Vec::new();
for (m, expect) in &cases {
if m.ratio() > expect.max_ratio {
failures.push(format!(
"{}: compressed to {:.4} of the input, worse than the {:.4} allowed",
m.name,
m.ratio(),
expect.max_ratio,
));
}
if cores > 2 && m.speedup() < expect.min_speedup {
failures.push(format!(
"{}: only {:.1}x faster than compressing the entries one by one, \
expected {:.1}x - the work is not being spread",
m.name,
m.speedup(),
expect.min_speedup,
));
}
if m.throughput() < expect.min_throughput {
failures.push(format!(
"{}: {:.1} MB/s, below the {:.1} MB/s floor",
m.name,
m.throughput(),
expect.min_throughput,
));
}
}
if failures.is_empty() {
println!("all scenarios within expectations");
return;
}
for failure in &failures {
eprintln!("FAIL: {failure}");
}
std::process::exit(1);
}