use std::io;
use std::time::{Duration, Instant};
use xxhash_rust::xxh64::xxh64 as xxh64_oneshot;
use super::compress_strategy::CompressionStrategy;
use super::config::{
BenchConfig, ACTIVEPERIOD_NANOSEC, COOLPERIOD_SEC, DECOMP_MULT, MB, TIMELOOP_NANOSEC,
};
use super::decompress_binding::{decompress_frame_block, FrameDecompressor};
use crate::block::{compress_bound, decompress_safe_using_dict};
const LZ4_MAX_INPUT_SIZE: usize = 0x7E00_0000;
struct BlockParam {
src_offset: usize,
src_size: usize,
c_buf: Vec<u8>,
c_size: usize,
res_buf: Vec<u8>,
res_size: usize,
}
pub struct BlockParams {
pub src: Vec<u8>,
pub compressed: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct BenchResult {
pub src_size: usize,
pub compressed_size: usize,
pub ratio: f64,
pub compress_speed_mb_s: f64,
pub decompress_speed_mb_s: f64,
pub c_level: i32,
}
pub fn bench_mem(
src: &[u8],
display_name: &str,
config: &BenchConfig,
c_level: i32,
strategy: &mut dyn CompressionStrategy,
decompressor: &mut FrameDecompressor,
dict: &[u8],
file_sizes: &[usize],
) -> io::Result<BenchResult> {
let src_size = src.len();
let block_size = if config.block_size >= 32 && !config.decode_only {
config.block_size
} else {
src_size
} + if src_size == 0 { 1 } else { 0 };
let nb_files = if file_sizes.is_empty() {
1
} else {
file_sizes.len()
};
let max_nb_blocks = src_size.div_ceil(block_size) + nb_files;
let dec_multiplier: usize = if config.decode_only { 255 } else { 1 };
let max_in_size: usize = LZ4_MAX_INPUT_SIZE / dec_multiplier;
let max_dec_size: usize = if src_size < max_in_size {
src_size * dec_multiplier
} else {
LZ4_MAX_INPUT_SIZE
};
let _ = max_dec_size;
let mut block_table: Vec<BlockParam> = Vec::with_capacity(max_nb_blocks);
{
let single_file_sizes = [src_size];
let effective_sizes: &[usize] = if file_sizes.is_empty() {
&single_file_sizes
} else {
file_sizes
};
let mut src_offset = 0usize;
for &file_size_item in effective_sizes {
let mut remaining = file_size_item;
let nb_blocks_for_this_file = remaining.div_ceil(block_size);
for _ in 0..nb_blocks_for_this_file {
let this_block_size = remaining.min(block_size);
let c_room = compress_bound(this_block_size as i32) as usize;
let res_max_size = this_block_size * dec_multiplier;
let res_capa = if this_block_size < max_in_size {
res_max_size
} else {
LZ4_MAX_INPUT_SIZE
};
let mut c_buf = vec![0u8; c_room];
let res_buf = vec![0u8; res_capa];
let c_size_init = if config.decode_only {
let copy_len = this_block_size.min(c_buf.len());
c_buf[..copy_len].copy_from_slice(&src[src_offset..src_offset + copy_len]);
copy_len
} else {
0
};
block_table.push(BlockParam {
src_offset,
src_size: this_block_size,
c_buf,
c_size: c_size_init,
res_buf,
res_size: 0,
});
src_offset += this_block_size;
remaining -= this_block_size;
}
}
if block_table.is_empty() {
let c_room = compress_bound(0) as usize;
block_table.push(BlockParam {
src_offset: 0,
src_size: 0,
c_buf: vec![0u8; c_room],
c_size: 0,
res_buf: vec![],
res_size: 0,
});
}
}
let display_name: &str = if display_name.len() > 17 {
&display_name[display_name.len() - 17..]
} else {
display_name
};
for block in &mut block_table {
block.c_buf.fill(b' ');
}
let mut fastest_c_ns: u64 = u64::MAX;
let mut fastest_d_ns: u64 = u64::MAX;
let crc_orig: u64 = xxh64_oneshot(src, 0);
let mut cool_time = Instant::now();
let max_time_ns: u64 = config.nb_seconds as u64 * TIMELOOP_NANOSEC + 100;
let mut nb_compression_loops: u32 = ((5 * MB) / (src_size + 1)) as u32 + 1;
let mut nb_decode_loops: u32 = ((200 * MB) / (src_size + 1)) as u32 + 1;
let mut total_c_time_ns: u64 = 0;
let mut total_d_time_ns: u64 = 0;
let mut c_completed: bool = config.decode_only;
let mut d_completed: bool = false;
const NB_MARKS: usize = 4;
const MARKS: [&str; NB_MARKS] = [" |", " /", " =", "\\"];
let mut mark_nb: usize = 0;
let mut c_size: usize = src_size; let mut total_r_size: usize = src_size; let mut ratio: f64 = 0.0;
if config.display_level >= 2 {
eprint!("\r{:79}\r", "");
}
if config.nb_seconds == 0 {
nb_compression_loops = 1;
nb_decode_loops = 1;
}
let mut bench_error = false;
while !c_completed || !d_completed {
if cool_time.elapsed().as_nanos() as u64 > ACTIVEPERIOD_NANOSEC {
if config.display_level >= 2 {
eprint!("\rcooling down ... \r");
}
std::thread::sleep(Duration::from_secs(COOLPERIOD_SEC));
cool_time = Instant::now();
}
if config.display_level >= 2 {
eprint!(
"{}-{:<17.17} :{:>10} ->\r",
MARKS[mark_nb], display_name, total_r_size
);
}
if !c_completed {
for block in &mut block_table {
block.c_buf.fill(0xE5);
block.c_size = 0;
}
}
std::thread::sleep(Duration::from_millis(1));
std::thread::sleep(Duration::from_millis(1));
if !c_completed {
let time_start = Instant::now();
'compress_outer: for _ in 0..nb_compression_loops {
for block in &mut block_table {
let compressed = strategy.compress_block(
&src[block.src_offset..block.src_offset + block.src_size],
&mut block.c_buf,
);
match compressed {
Ok(n) => {
block.c_size = n;
}
Err(_) => {
eprintln!(
"LZ4 compression failed on block at offset {} ",
block.src_offset
);
bench_error = true;
break 'compress_outer;
}
}
}
}
let duration_ns = time_start.elapsed().as_nanos() as u64;
if duration_ns > 0 {
let per_loop = duration_ns / nb_compression_loops as u64;
if per_loop < fastest_c_ns {
fastest_c_ns = per_loop;
}
nb_compression_loops = (TIMELOOP_NANOSEC / fastest_c_ns) as u32 + 1;
} else {
assert!(nb_compression_loops < 40_000_000);
nb_compression_loops *= 100;
}
total_c_time_ns += duration_ns;
c_completed = total_c_time_ns > max_time_ns;
c_size = block_table.iter().map(|b| b.c_size).sum::<usize>();
if c_size == 0 {
c_size = 1; }
ratio = total_r_size as f64 / c_size as f64;
mark_nb = (mark_nb + 1) % NB_MARKS;
if config.display_level >= 2 {
eprint!(
"{}-{:<17.17} :{:>10} ->{:>10} ({:5.3}),{:6.1} MB/s\r",
MARKS[mark_nb],
display_name,
total_r_size,
c_size,
ratio,
(total_r_size as f64 / fastest_c_ns as f64) * 1000.0,
);
}
}
if !d_completed {
for block in &mut block_table {
block.res_buf.fill(0xD6);
}
}
std::thread::sleep(Duration::from_millis(5));
std::thread::sleep(Duration::from_millis(1));
if !d_completed {
let time_start = Instant::now();
'decode_outer: for _ in 0..nb_decode_loops {
for block in &mut block_table {
let in_max_size = i32::MAX as usize / dec_multiplier;
let res_capa = if block.src_size < in_max_size {
block.src_size * dec_multiplier
} else {
i32::MAX as usize
};
if config.decode_only {
let mut tmp = Vec::new();
match decompress_frame_block(
decompressor,
&block.c_buf[..block.c_size],
&mut tmp,
res_capa,
config.skip_checksums,
) {
Ok(n) => {
block.res_buf[..n].copy_from_slice(&tmp[..n]);
block.res_size = n;
}
Err(_) => {
eprintln!(
"LZ4F_decompress() failed on block at offset {} of size {} \nIs input using LZ4 Frame format ?",
block.src_offset, block.src_size
);
bench_error = true;
break 'decode_outer;
}
}
} else {
let result = unsafe {
decompress_safe_using_dict(
block.c_buf.as_ptr(),
block.res_buf.as_mut_ptr(),
block.c_size,
res_capa,
dict.as_ptr(),
dict.len(),
)
};
match result {
Ok(regen) => {
block.res_size = regen;
}
Err(_) => {
eprintln!(
"decompress_safe_using_dict() failed on block at offset {} of size {} ",
block.src_offset, block.src_size
);
bench_error = true;
break 'decode_outer;
}
}
}
}
}
let duration_ns = time_start.elapsed().as_nanos() as u64;
if duration_ns > 0 {
let per_loop = duration_ns / nb_decode_loops as u64;
if per_loop < fastest_d_ns {
fastest_d_ns = per_loop;
}
nb_decode_loops = (TIMELOOP_NANOSEC / fastest_d_ns) as u32 + 1;
} else {
assert!(nb_decode_loops < 40_000_000);
nb_decode_loops *= 100;
}
total_d_time_ns += duration_ns;
d_completed = total_d_time_ns > (DECOMP_MULT as u64 * max_time_ns);
}
if config.decode_only {
total_r_size = block_table.iter().map(|b| b.res_size).sum();
}
mark_nb = (mark_nb + 1) % NB_MARKS;
ratio = if c_size > 0 {
total_r_size as f64 / c_size as f64
} else {
0.0
};
let compress_speed = if fastest_c_ns > 0 && fastest_c_ns != u64::MAX {
(total_r_size as f64 / fastest_c_ns as f64) * 1000.0
} else {
0.0
};
let decompress_speed = if fastest_d_ns > 0 && fastest_d_ns != u64::MAX {
(total_r_size as f64 / fastest_d_ns as f64) * 1000.0
} else {
0.0
};
if config.display_level >= 2 {
eprint!(
"{}-{:<17.17} :{:>10} ->{:>10} ({:5.3}),{:6.1} MB/s, {:6.1} MB/s\r",
MARKS[mark_nb],
display_name,
total_r_size,
c_size,
ratio,
compress_speed,
decompress_speed,
);
}
if !config.decode_only {
let mut result_bytes: Vec<u8> =
Vec::with_capacity(block_table.iter().map(|b| b.res_size).sum());
for block in &block_table {
result_bytes.extend_from_slice(&block.res_buf[..block.res_size]);
}
let crc_check = xxh64_oneshot(&result_bytes, 0);
if crc_orig != crc_check {
eprintln!(
"\n!!! WARNING !!! {:17} : Invalid Checksum : {:x} != {:x} ",
display_name, crc_orig, crc_check
);
bench_error = true;
for (u, (&src_b, &res_b)) in src.iter().zip(result_bytes.iter()).enumerate() {
if src_b != res_b {
eprintln!("Decoding error at pos {} ", u);
break;
}
if u == src_size - 1 {
eprintln!("no difference detected");
}
}
break;
}
}
}
let compress_speed_mb_s = if fastest_c_ns > 0 && fastest_c_ns != u64::MAX {
(src_size as f64 / fastest_c_ns as f64) * 1000.0
} else {
0.0
};
let decompress_speed_mb_s = if fastest_d_ns > 0 && fastest_d_ns != u64::MAX {
(src_size as f64 / fastest_d_ns as f64) * 1000.0
} else {
0.0
};
if config.display_level >= 2 {
eprintln!("{:2}#", c_level);
}
if config.display_level == 1 {
print!(
"-{:<3}{:>11} ({:5.3}) {:6.2} MB/s {:6.1} MB/s {}",
c_level, c_size, ratio, compress_speed_mb_s, decompress_speed_mb_s, display_name,
);
if config.additional_param != 0 {
print!(" (param={})", config.additional_param);
}
println!();
}
if bench_error {
return Err(io::Error::other(
"benchmark reported errors (compression or checksum failure)",
));
}
Ok(BenchResult {
src_size,
compressed_size: c_size,
ratio,
compress_speed_mb_s,
decompress_speed_mb_s,
c_level,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bench::compress_strategy::build_compression_parameters;
use crate::bench::config::BenchConfig;
use crate::bench::decompress_binding::FrameDecompressor;
fn make_1mb_buf() -> Vec<u8> {
(0u8..=255).cycle().take(1024 * 1024).collect()
}
#[test]
fn bench_mem_1mb_level1() {
let src = make_1mb_buf();
let config = {
let mut c = BenchConfig::default();
c.set_nb_seconds(1); c
};
let mut strategy = build_compression_parameters(1, src.len(), src.len());
let mut decompressor = FrameDecompressor::new();
let result = bench_mem(
&src,
"test",
&config,
1,
&mut *strategy,
&mut decompressor,
b"",
&[],
);
let r = result.expect("bench_mem should succeed on a 1 MB buffer at level 1");
assert!(r.compressed_size > 0, "compressed_size must be non-zero");
assert!(
r.compress_speed_mb_s > 0.0,
"compression throughput must be positive"
);
assert!(
r.decompress_speed_mb_s > 0.0,
"decompression throughput must be positive"
);
}
#[test]
fn bench_mem_crc_passes() {
let src: Vec<u8> = b"hello world! "
.iter()
.cycle()
.take(64 * 1024)
.cloned()
.collect();
let config = {
let mut c = BenchConfig::default();
c.set_nb_seconds(1);
c
};
let mut strategy = build_compression_parameters(1, src.len(), src.len());
let mut decompressor = FrameDecompressor::new();
let result = bench_mem(
&src,
"crctest",
&config,
1,
&mut *strategy,
&mut decompressor,
b"",
&[],
);
assert!(result.is_ok(), "CRC check must pass: {:?}", result.err());
}
#[test]
fn bench_mem_zero_seconds_single_pass() {
let src: Vec<u8> = (0u8..128).cycle().take(4096).collect();
let config = {
let mut c = BenchConfig::default();
c.set_nb_seconds(0);
c
};
let mut strategy = build_compression_parameters(1, src.len(), src.len());
let mut decompressor = FrameDecompressor::new();
let result = bench_mem(
&src,
"zerotest",
&config,
1,
&mut *strategy,
&mut decompressor,
b"",
&[],
);
assert!(result.is_ok(), "single-pass bench_mem must succeed");
}
#[test]
fn bench_mem_hc_level() {
let src: Vec<u8> = b"aaaa".iter().cycle().take(32 * 1024).cloned().collect();
let config = {
let mut c = BenchConfig::default();
c.set_nb_seconds(1);
c
};
let mut strategy = build_compression_parameters(9, src.len(), src.len());
let mut decompressor = FrameDecompressor::new();
let result = bench_mem(
&src,
"hctest",
&config,
9,
&mut *strategy,
&mut decompressor,
b"",
&[],
);
assert!(
result.is_ok(),
"HC bench_mem must succeed: {:?}",
result.err()
);
}
#[test]
fn bench_result_fields_plausible() {
let src = make_1mb_buf();
let config = {
let mut c = BenchConfig::default();
c.set_nb_seconds(1);
c
};
let mut strategy = build_compression_parameters(1, src.len(), src.len());
let mut decompressor = FrameDecompressor::new();
let r = bench_mem(
&src,
"fields",
&config,
1,
&mut *strategy,
&mut decompressor,
b"",
&[],
)
.unwrap();
assert_eq!(r.src_size, 1024 * 1024);
assert_eq!(r.c_level, 1);
assert!(r.ratio > 0.0);
assert!(
r.compressed_size < src.len(),
"compressible input should shrink"
);
}
}