Skip to main content

hexz_cli/cmd/sys/
bench.rs

1//! Implementation of the `hexz bench` command.
2//!
3//! Runs read throughput benchmarks against a Hexz snapshot to measure decompression
4//! and I/O performance. This helps validate storage backend configuration and identify
5//! bottlenecks in snapshot access patterns.
6//!
7//! # Benchmarks
8//!
9//! The benchmark suite currently includes:
10//!
11//! - **Sequential read test**: Reads the entire disk stream sequentially in 1 MiB chunks
12//!   and measures total throughput (MB/s). This tests decompression speed and storage
13//!   backend bandwidth.
14//!
15//! Future benchmarks may include:
16//! - Random IOPS testing (4 KiB random reads)
17//! - Cache effectiveness measurements
18//! - Multi-threaded access patterns
19//!
20//! # Performance Expectations
21//!
22//! Typical sequential read throughput:
23//! - **LZ4 compression**: 800-2000 MB/s (decompression-bound)
24//! - **Zstd compression**: 400-800 MB/s (decompression-bound)
25//! - **Local SSD backend**: Usually CPU-bound (decompression bottleneck)
26//! - **Remote backends (S3, HTTP)**: May be network-bound at 100-500 MB/s
27//!
28//! # Output
29//!
30//! The benchmark displays:
31//! - Snapshot size (logical disk size)
32//! - Progress bar during the test
33//! - Total bytes read
34//! - Test duration
35//! - Average throughput (MB/s)
36
37use anyhow::Result;
38use hexz_core::File;
39use hexz_core::api::file::SnapshotStream;
40use hexz_core::store::local::FileBackend;
41use indicatif::{HumanBytes, ProgressBar, ProgressStyle};
42use std::path::PathBuf;
43use std::sync::Arc;
44use std::time::Instant;
45
46/// Execute the benchmark command on a Hexz snapshot.
47///
48/// This function runs a sequential read benchmark to measure snapshot read performance.
49/// It opens the snapshot, configures the appropriate decompressor, and reads the entire
50/// disk stream in 1 MiB chunks while measuring throughput.
51///
52/// # Benchmark Methodology
53///
54/// The test performs a single sequential pass over the entire disk stream:
55/// 1. Opens the snapshot and loads the header
56/// 2. Configures the decompressor (LZ4 or Zstd) with any embedded dictionary
57/// 3. Reads the disk stream sequentially in 1 MiB chunks
58/// 4. Measures total time and calculates throughput
59///
60/// # Arguments
61///
62/// * `snap_path` - Path to the `.hxz` snapshot file
63/// * `_block_size` - Reserved for future random I/O tests (currently unused)
64/// * `_duration` - Reserved for time-limited tests (currently unused)
65/// * `_threads` - Reserved for multi-threaded tests (currently unused)
66///
67/// # Performance Notes
68///
69/// This benchmark tests the full read path including:
70/// - Storage backend I/O (file reads, network fetches)
71/// - Decompression (LZ4 or Zstd)
72/// - Block cache effectiveness (if cache is enabled)
73///
74/// For accurate results:
75/// - Run on a representative hardware configuration
76/// - Ensure the snapshot is not cached in system page cache (or run multiple iterations)
77/// - Compare results across compression algorithms and block sizes
78///
79/// # Example Output
80///
81/// ```text
82/// Benchmarking snapshot: "vm-snapshot.hxz"
83/// Image Size: 10.0 GB
84///
85/// Running Sequential Read Test (1 pass)...
86/// [████████████████████] 10.0 GB/10.0 GB (250 MB/s)
87///
88/// Total Read: 10.0 GB
89/// Duration: 40.23s
90/// Throughput: 248.57 MB/s
91/// ```
92pub fn run(
93    snap_path: PathBuf,
94    _block_size: Option<u32>,
95    _duration: Option<u64>,
96    _threads: Option<usize>,
97) -> Result<()> {
98    println!("Benchmarking snapshot: {:?}", snap_path);
99    let backend = Arc::new(FileBackend::new(&snap_path)?);
100    let snap = File::open(backend, None)?;
101    let disk_size = snap.size(SnapshotStream::Disk);
102
103    println!("Image Size: {}", HumanBytes(disk_size));
104
105    // Sequential Read Test
106    println!("\nRunning Sequential Read Test (1 pass)...");
107    let start = Instant::now();
108    let mut offset = 0;
109    let chunk_size = 1024 * 1024; // 1 MiB chunks
110    let mut total_read = 0;
111    let pb = ProgressBar::new(disk_size);
112    pb.set_style(ProgressStyle::default_bar()
113        .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec})")
114        .unwrap_or_else(|_| ProgressStyle::default_bar()));
115
116    while offset < disk_size {
117        let len = std::cmp::min(chunk_size, (disk_size - offset) as usize);
118        let _data = snap.read_at(SnapshotStream::Disk, offset, len)?;
119        offset += len as u64;
120        total_read += len as u64;
121        pb.inc(len as u64);
122    }
123    pb.finish_with_message("Done");
124
125    let duration = start.elapsed();
126    let mb = total_read as f64 / 1024.0 / 1024.0;
127    let throughput = mb / duration.as_secs_f64();
128
129    println!("Total Read: {}", HumanBytes(total_read));
130    println!("Duration: {:.2?}", duration);
131    println!("Throughput: {:.2} MB/s", throughput);
132
133    // Random Read Test (if requested or default)
134    // Note: Implementing true random IOPS benchmark requires threads and duration control.
135    // For now, we provide a basic sequential bench.
136
137    Ok(())
138}