use std::fs;
use std::io::{Cursor, Read, Seek, SeekFrom};
use std::sync::{Arc, Mutex};
use ewf_image::{EwfWriter, Image, OpenOptions, WriteCompression, WriteOptions};
use tempfile::tempdir;
const CHUNK_SIZE: usize = 512;
const CHUNK_COUNT: usize = 2_048;
#[derive(Debug, Clone, Copy, Default)]
struct ReadSnapshot {
calls: u64,
bytes: u64,
four_byte_calls: u64,
seek_from_end_calls: u64,
}
impl ReadSnapshot {
fn delta(self, earlier: Self) -> Self {
Self {
calls: self.calls.saturating_sub(earlier.calls),
bytes: self.bytes.saturating_sub(earlier.bytes),
four_byte_calls: self.four_byte_calls.saturating_sub(earlier.four_byte_calls),
seek_from_end_calls: self
.seek_from_end_calls
.saturating_sub(earlier.seek_from_end_calls),
}
}
}
struct CountingReader {
cursor: Cursor<Vec<u8>>,
counters: Arc<Mutex<ReadSnapshot>>,
}
impl Read for CountingReader {
fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
let read = self.cursor.read(buffer)?;
let mut counters = self.counters.lock().expect("counter lock poisoned");
counters.calls = counters.calls.saturating_add(1);
counters.bytes = counters
.bytes
.saturating_add(u64::try_from(read).unwrap_or(u64::MAX));
if buffer.len() == 4 {
counters.four_byte_calls = counters.four_byte_calls.saturating_add(1);
}
Ok(read)
}
}
impl Seek for CountingReader {
fn seek(&mut self, position: SeekFrom) -> std::io::Result<u64> {
if matches!(position, SeekFrom::End(_)) {
let mut counters = self.counters.lock().expect("counter lock poisoned");
counters.seek_from_end_calls = counters.seek_from_end_calls.saturating_add(1);
}
self.cursor.seek(position)
}
}
fn synthetic_e01() -> (Vec<u8>, Vec<u8>) {
let directory = tempdir().unwrap();
let path = directory.path().join("reader-performance.E01");
let data: Vec<u8> = (0..CHUNK_SIZE * CHUNK_COUNT)
.map(|index| (index % 251) as u8)
.collect();
let options = WriteOptions {
sectors_per_chunk: 1,
bytes_per_sector: CHUNK_SIZE as u32,
compression: WriteCompression::None,
..WriteOptions::default()
};
let mut writer = EwfWriter::create(&path, options).unwrap();
writer.write_all(&data).unwrap();
writer.finish().unwrap();
(fs::read(path).unwrap(), data)
}
fn open_counted(bytes: Vec<u8>, options: OpenOptions) -> (Image, Arc<Mutex<ReadSnapshot>>) {
let counters = Arc::new(Mutex::new(ReadSnapshot::default()));
let reader = CountingReader {
cursor: Cursor::new(bytes),
counters: Arc::clone(&counters),
};
let image =
Image::open_readers_with_options([("reader-performance.E01", reader)], options).unwrap();
(image, counters)
}
fn read_chunk_starts(
image: &Image,
expected: &[u8],
counters: &Arc<Mutex<ReadSnapshot>>,
) -> ReadSnapshot {
let before = *counters.lock().expect("counter lock poisoned");
for chunk in 0..CHUNK_COUNT {
let offset = chunk * CHUNK_SIZE;
let mut byte = [0];
assert_eq!(image.read_at(&mut byte, offset as u64).unwrap(), 1);
assert_eq!(byte[0], expected[offset]);
}
counters
.lock()
.expect("counter lock poisoned")
.delta(before)
}
#[test]
fn table_page_cache_reduces_sequential_table_reads() {
let (bytes, expected) = synthetic_e01();
let (uncached, uncached_counters) = open_counted(
bytes.clone(),
OpenOptions::default().with_table_entry_cache_size_bytes(0),
);
let (cached, cached_counters) = open_counted(
bytes,
OpenOptions::default().with_table_entry_cache_size_bytes(4 * 1024 * 1024),
);
let uncached_reads = read_chunk_starts(&uncached, &expected, &uncached_counters);
let cached_reads = read_chunk_starts(&cached, &expected, &cached_counters);
assert!(cached_reads.calls < uncached_reads.calls);
}
#[test]
fn disabled_table_page_cache_uses_exact_table_entry_reads() {
let (bytes, expected) = synthetic_e01();
let (image, counters) = open_counted(
bytes,
OpenOptions::default().with_table_entry_cache_size_bytes(0),
);
let reads = read_chunk_starts(&image, &expected, &counters);
assert!(reads.four_byte_calls > 0);
}
#[test]
fn reader_statistics_are_optional_and_shared_with_cache_information() {
let (bytes, expected) = synthetic_e01();
let (disabled, _) = open_counted(bytes.clone(), OpenOptions::default());
assert!(disabled.reader_statistics().is_none());
let options = OpenOptions::default()
.with_chunk_cache_size_bytes(2 * CHUNK_SIZE)
.with_table_entry_cache_size_bytes(4 * 1024 * 1024)
.with_reader_statistics(true);
let (image, _) = open_counted(bytes, options);
let opened = image.reader_statistics().unwrap();
assert_eq!(opened.segment_parses(), 1);
assert_eq!(opened.segment_handle_opens(), 1);
assert!(opened.table_checksum_bytes() > 0);
let mut first = [0];
image.read_at(&mut first, 0).unwrap();
let mut second = [0];
image.read_at(&mut second, 0).unwrap();
let _cursor = image.cursor();
let statistics = image
.clone()
.reader_statistics()
.unwrap()
.saturating_delta(opened);
assert_eq!(first, second);
assert_eq!(first[0], expected[0]);
assert_eq!(statistics.cursors_created(), 1);
assert_eq!(statistics.chunk_cache_misses(), 1);
assert_eq!(statistics.chunk_cache_hits(), 1);
assert!(statistics.table_page_cache_misses() >= 1);
assert!(statistics.table_page_cache_hits() >= 1);
assert!(statistics.encoded_bytes_read() > 0);
assert!(statistics.decoded_bytes() > 0);
let cache = image.reader_cache_info();
assert_eq!(cache.chunk_cache_capacity_bytes(), (2 * CHUNK_SIZE) as u64);
assert_eq!(cache.table_entry_cache_capacity_bytes(), 4 * 1024 * 1024);
assert!(cache.table_entry_cache_current_bytes() > 0);
assert!(cache.table_entry_cache_peak_bytes() >= cache.table_entry_cache_current_bytes());
assert!(cache.table_entry_cache_peak_bytes() <= cache.table_entry_cache_capacity_bytes());
}