fix(bench): eliminate SIGBUS handler static mut UB with Once + raw atomics (closes #33)

Replace `static mut OLD_SIGBUS_HANDLER` with AtomicU8 + AtomicPtr to
remove data race UB when concurrent benchmarks call open() from multiple
threads.

Key changes:
- Use `Once::call_once` to guarantee single handler installation
- Publish old handler to atomics BEFORE installing new handler (closes
  the handler-active-but-state-unpublished race window)
- Read atomics with Acquire in signal handler (async-signal-safe)
- Align si_addr to page boundary before mmap(MAP_FIXED)
- Add concurrent test: 8 threads open all 5 variants simultaneously
This commit is contained in:
dailz
2026-06-05 13:22:02 +08:00
parent 534a089b58
commit dad5f5a635
19 changed files with 3562 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
use std::collections::HashMap;
use crate::metrics::MetricsCollector;
use crate::mmap_reader::{
MmapReaderPhaseAware, MmapReaderPlain, MmapReaderPopulate, MmapReaderRandom,
MmapReaderSequential,
};
use crate::pread_reader::{PreadReaderPlain, PreadReaderRandom, PreadReaderSequential};
use crate::runner::BenchConfig;
use crate::types::BenchmarkResult;
use crate::FileReaderBackend;
pub fn run(config: &BenchConfig) -> Vec<BenchmarkResult> {
let mut results = Vec::new();
results.extend(bench_parallel_reads::<MmapReaderPlain>(
"mmap", "plain", config,
));
results.extend(bench_parallel_reads::<MmapReaderSequential>(
"mmap",
"sequential",
config,
));
results.extend(bench_parallel_reads::<MmapReaderRandom>(
"mmap", "random", config,
));
results.extend(bench_parallel_reads::<MmapReaderPopulate>(
"mmap", "populate", config,
));
results.extend(bench_parallel_reads::<MmapReaderPhaseAware>(
"mmap",
"phase_aware",
config,
));
results.extend(bench_parallel_reads::<PreadReaderPlain>(
"pread", "plain", config,
));
results.extend(bench_parallel_reads::<PreadReaderRandom>(
"pread", "random", config,
));
results.extend(bench_parallel_reads::<PreadReaderSequential>(
"pread",
"sequential",
config,
));
results
}
fn bench_parallel_reads<B: FileReaderBackend + Send + 'static>(
backend: &str,
variant: &str,
config: &BenchConfig,
) -> Vec<BenchmarkResult> {
let path = config.test_file.clone();
let iterations = if config.quick_mode { 250 } else { 1000 };
let total_lines = {
let reader = B::open(&path).expect("Failed to open file for line count");
let count = reader.total_lines();
reader.close();
count
};
let overall_start = std::time::Instant::now();
let num_threads = 4usize;
let handles: Vec<_> = (0..num_threads)
.map(|thread_id| {
let path = path.clone();
std::thread::spawn(move || {
let reader = B::open(&path).expect("Failed to open file in thread");
let mut latencies = Vec::with_capacity(iterations);
for i in 0..iterations {
let line_idx = (thread_id * iterations + i) % total_lines.max(1);
let t = std::time::Instant::now();
let _ = reader.get_line(line_idx);
latencies.push(t.elapsed().as_micros() as u64);
}
reader.close();
latencies
})
})
.collect();
let thread_latencies: Vec<Vec<u64>> = handles
.into_iter()
.map(|h| h.join().expect("Thread panicked"))
.collect();
let total_elapsed = overall_start.elapsed();
let all_latencies: Vec<u64> = thread_latencies.into_iter().flatten().collect();
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("num_threads".into(), num_threads as f64);
extra.insert("iterations_per_thread".into(), iterations as f64);
extra.insert("total_time_us".into(), total_elapsed.as_micros() as f64);
extra.insert("total_lines".into(), total_lines as f64);
vec![BenchmarkResult {
category: "concurrent".into(),
test_name: "parallel_reads".into(),
backend: backend.into(),
variant: variant.into(),
latency_us: all_latencies,
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}

View File

@@ -0,0 +1,269 @@
use std::collections::HashMap;
use crate::data_gen;
use crate::metrics::MetricsCollector;
use crate::mmap_reader::MmapReaderPlain;
use crate::pread_reader::PreadReaderPlain;
use crate::runner::BenchConfig;
use crate::types::BenchmarkResult;
use crate::FileReaderBackend;
pub fn run(config: &BenchConfig) -> Vec<BenchmarkResult> {
let mut results = Vec::new();
let dir = tempfile::tempdir().expect("Failed to create temp dir");
results.extend(bench_append_visibility_mmap(config, dir.path()));
results.extend(bench_append_visibility_pread(config, dir.path()));
results.extend(bench_remap_cost(config, dir.path()));
results.extend(bench_scroll_during_append(config, dir.path()));
results.extend(bench_high_frequency_append(config, dir.path()));
results
}
fn bench_append_visibility_mmap(
config: &BenchConfig,
dir: &std::path::Path,
) -> Vec<BenchmarkResult> {
let path = data_gen::generate_growable_file(dir).expect("Failed to create growable file");
let append_count: usize = if config.quick_mode { 100 } else { 1000 };
let mut reader = MmapReaderPlain::open(&path).expect("Failed to open growable file");
let original_lines = reader.total_lines();
let original_size = reader.file_size();
data_gen::append_lines(&path, append_count).expect("Failed to append lines");
let new_metadata = std::fs::metadata(&path).expect("Failed to read metadata");
let new_size = new_metadata.len() as usize;
let remap_start = std::time::Instant::now();
reader.remap(new_size).expect("Failed to remap");
let remap_elapsed = remap_start.elapsed();
let new_line_bytes =
reader.read_range(original_size, (new_size - original_size as usize).min(256));
let visible = new_line_bytes.is_some();
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("original_lines".into(), original_lines as f64);
extra.insert("appended_lines".into(), append_count as f64);
extra.insert("new_bytes_visible".into(), visible as u64 as f64);
extra.insert("original_size".into(), original_size as f64);
extra.insert("new_size".into(), new_size as f64);
extra.insert("remap_us".into(), remap_elapsed.as_micros() as f64);
reader.close();
vec![BenchmarkResult {
category: "growth".into(),
test_name: "append_visibility_mmap".into(),
backend: "mmap".into(),
variant: "plain".into(),
latency_us: vec![remap_elapsed.as_micros() as u64],
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}
fn bench_append_visibility_pread(
config: &BenchConfig,
dir: &std::path::Path,
) -> Vec<BenchmarkResult> {
let sub_dir = dir.join("pread_growth");
let path = data_gen::generate_growable_file(&sub_dir).expect("Failed to create growable file");
let append_count: usize = if config.quick_mode { 100 } else { 1000 };
let reader = PreadReaderPlain::open(&path).expect("Failed to open growable file");
let original_lines = reader.total_lines();
reader.close();
data_gen::append_lines(&path, append_count).expect("Failed to append lines");
let reopen_start = std::time::Instant::now();
let new_reader = PreadReaderPlain::open(&path).expect("Failed to reopen file");
let reopen_elapsed = reopen_start.elapsed();
let new_lines = new_reader.total_lines();
let can_read_new = new_lines > original_lines;
if can_read_new {
let last_line = new_lines.saturating_sub(1);
let _ = new_reader.get_line(last_line);
}
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("original_lines".into(), original_lines as f64);
extra.insert("appended_lines".into(), append_count as f64);
extra.insert("new_total_lines".into(), new_lines as f64);
extra.insert("reopen_us".into(), reopen_elapsed.as_micros() as f64);
new_reader.close();
vec![BenchmarkResult {
category: "growth".into(),
test_name: "append_visibility_pread".into(),
backend: "pread".into(),
variant: "plain".into(),
latency_us: vec![reopen_elapsed.as_micros() as u64],
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}
fn bench_remap_cost(config: &BenchConfig, dir: &std::path::Path) -> Vec<BenchmarkResult> {
let sub_dir = dir.join("remap_cost");
let append_count: usize = if config.quick_mode { 100 } else { 1000 };
let iterations: usize = if config.quick_mode { 5 } else { 20 };
let mut latencies = Vec::with_capacity(iterations);
for _ in 0..iterations {
let path = data_gen::generate_growable_file(&sub_dir).expect("Failed to create file");
let mut reader = MmapReaderPlain::open(&path).expect("Failed to open file");
data_gen::append_lines(&path, append_count).expect("Failed to append");
let new_size = std::fs::metadata(&path).expect("metadata").len() as usize;
let t = std::time::Instant::now();
reader.remap(new_size).expect("remap failed");
latencies.push(t.elapsed().as_micros() as u64);
reader.close();
let _ = std::fs::remove_file(&path);
}
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("appended_per_iter".into(), append_count as f64);
extra.insert("iterations".into(), iterations as f64);
vec![BenchmarkResult {
category: "growth".into(),
test_name: "remap_cost".into(),
backend: "mmap".into(),
variant: "plain".into(),
latency_us: latencies,
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}
fn bench_scroll_during_append(config: &BenchConfig, dir: &std::path::Path) -> Vec<BenchmarkResult> {
let sub_dir = dir.join("scroll_append");
let path = data_gen::generate_growable_file(&sub_dir).expect("Failed to create growable file");
let duration_secs: u64 = if config.quick_mode { 2 } else { 10 };
let append_rate: usize = if config.quick_mode { 1000 } else { 10000 };
let bg_path = path.clone();
let bg_handle = std::thread::spawn(move || {
let batch_size = 100;
let batch_interval =
std::time::Duration::from_micros(1_000_000 / (append_rate / batch_size).max(1) as u64);
let start = std::time::Instant::now();
while start.elapsed().as_secs() < duration_secs {
data_gen::append_lines(&bg_path, batch_size).ok();
std::thread::sleep(batch_interval);
}
});
let reader = PreadReaderPlain::open(&path).expect("Failed to open file");
let mut frame_latencies = Vec::new();
let mut current_line = 0usize;
let scroll_start = std::time::Instant::now();
while scroll_start.elapsed().as_secs() < duration_secs {
let t = std::time::Instant::now();
let _ = reader.get_line(current_line);
frame_latencies.push(t.elapsed().as_micros() as u64);
current_line += 1;
}
reader.close();
bg_handle.join().ok();
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("duration_secs".into(), duration_secs as f64);
extra.insert("append_rate_per_sec".into(), append_rate as f64);
extra.insert("frames_rendered".into(), frame_latencies.len() as f64);
vec![BenchmarkResult {
category: "growth".into(),
test_name: "scroll_during_append".into(),
backend: "pread".into(),
variant: "plain".into(),
latency_us: frame_latencies,
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}
fn bench_high_frequency_append(
config: &BenchConfig,
dir: &std::path::Path,
) -> Vec<BenchmarkResult> {
let sub_dir = dir.join("high_freq_append");
let path = data_gen::generate_growable_file(&sub_dir).expect("Failed to create growable file");
let duration_secs: u64 = if config.quick_mode { 3 } else { 30 };
let append_rate: usize = if config.quick_mode { 1000 } else { 10000 };
let batch_size: usize = 100;
let batches_per_sec = append_rate / batch_size;
let total_batches = (duration_secs as usize * batches_per_sec).max(1);
let mut detect_latencies = Vec::with_capacity(total_batches);
for _ in 0..total_batches {
data_gen::append_lines(&path, batch_size).expect("Failed to append");
let t = std::time::Instant::now();
if let Ok(reader) = PreadReaderPlain::open(&path) {
let total = reader.total_lines();
let _ = reader.get_line(total.saturating_sub(1));
reader.close();
}
detect_latencies.push(t.elapsed().as_micros() as u64);
std::thread::sleep(std::time::Duration::from_micros(
1_000_000 / batches_per_sec as u64,
));
}
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("duration_secs".into(), duration_secs as f64);
extra.insert("append_rate_per_sec".into(), append_rate as f64);
extra.insert("batch_size".into(), batch_size as f64);
extra.insert("total_batches".into(), total_batches as f64);
vec![BenchmarkResult {
category: "growth".into(),
test_name: "high_frequency_append".into(),
backend: "pread".into(),
variant: "plain".into(),
latency_us: detect_latencies,
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}

View File

@@ -0,0 +1,246 @@
use std::collections::HashMap;
use crate::metrics::MetricsCollector;
use crate::mmap_reader::{
MmapReaderPhaseAware, MmapReaderPlain, MmapReaderPopulate, MmapReaderRandom,
MmapReaderSequential,
};
use crate::pread_reader::{PreadReaderPlain, PreadReaderRandom, PreadReaderSequential};
use crate::runner::BenchConfig;
use crate::types::BenchmarkResult;
use crate::FileReaderBackend;
pub fn run(config: &BenchConfig) -> Vec<BenchmarkResult> {
let mut results = Vec::new();
results.extend(bench_near_jump::<MmapReaderPlain>("mmap", "plain", config));
results.extend(bench_near_jump::<MmapReaderSequential>(
"mmap",
"sequential",
config,
));
results.extend(bench_near_jump::<MmapReaderRandom>(
"mmap", "random", config,
));
results.extend(bench_near_jump::<MmapReaderPopulate>(
"mmap", "populate", config,
));
results.extend(bench_near_jump::<MmapReaderPhaseAware>(
"mmap",
"phase_aware",
config,
));
results.extend(bench_near_jump::<PreadReaderPlain>(
"pread", "plain", config,
));
results.extend(bench_near_jump::<PreadReaderRandom>(
"pread", "random", config,
));
results.extend(bench_near_jump::<PreadReaderSequential>(
"pread",
"sequential",
config,
));
results.extend(bench_far_jump::<MmapReaderPlain>("mmap", "plain", config));
results.extend(bench_far_jump::<MmapReaderSequential>(
"mmap",
"sequential",
config,
));
results.extend(bench_far_jump::<MmapReaderRandom>("mmap", "random", config));
results.extend(bench_far_jump::<MmapReaderPopulate>(
"mmap", "populate", config,
));
results.extend(bench_far_jump::<MmapReaderPhaseAware>(
"mmap",
"phase_aware",
config,
));
results.extend(bench_far_jump::<PreadReaderPlain>("pread", "plain", config));
results.extend(bench_far_jump::<PreadReaderRandom>(
"pread", "random", config,
));
results.extend(bench_far_jump::<PreadReaderSequential>(
"pread",
"sequential",
config,
));
results.extend(bench_jump_end::<MmapReaderPlain>("mmap", "plain", config));
results.extend(bench_jump_end::<PreadReaderPlain>("pread", "plain", config));
results.extend(bench_reverse_scan::<MmapReaderPlain>(
"mmap", "plain", config,
));
results.extend(bench_reverse_scan::<PreadReaderPlain>(
"pread", "plain", config,
));
results
}
fn bench_near_jump<B: FileReaderBackend>(
backend: &str,
variant: &str,
config: &BenchConfig,
) -> Vec<BenchmarkResult> {
let path = &config.test_file;
let reader = B::open(path).expect("Failed to open file");
let total = reader.total_lines();
let iterations: usize = if config.quick_mode { 10 } else { 100 };
let mut latencies = Vec::with_capacity(iterations);
let mut current = 0usize;
for _ in 0..iterations {
let target = (current + 15).min(total.saturating_sub(1));
let t = std::time::Instant::now();
let _ = reader.get_line(target);
latencies.push(t.elapsed().as_micros() as u64);
current = target;
}
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
reader.close();
vec![BenchmarkResult {
category: "jump".into(),
test_name: "near_jump".into(),
backend: backend.into(),
variant: variant.into(),
latency_us: latencies,
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra: HashMap::new(),
}]
}
fn bench_far_jump<B: FileReaderBackend>(
backend: &str,
variant: &str,
config: &BenchConfig,
) -> Vec<BenchmarkResult> {
let path = &config.test_file;
let reader = B::open(path).expect("Failed to open file");
let total = reader.total_lines();
let repetitions: usize = if config.quick_mode { 3 } else { 10 };
let fractions = [0.25, 0.50, 0.75];
let mut latencies = Vec::new();
let mut extra = HashMap::new();
for &frac in &fractions {
let target = ((total as f64 * frac) as usize).min(total.saturating_sub(1));
for _ in 0..repetitions {
let t = std::time::Instant::now();
let _ = reader.get_line(target);
latencies.push(t.elapsed().as_micros() as u64);
}
}
extra.insert("jump_positions".into(), 3.0);
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
reader.close();
vec![BenchmarkResult {
category: "jump".into(),
test_name: "far_jump".into(),
backend: backend.into(),
variant: variant.into(),
latency_us: latencies,
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}
fn bench_jump_end<B: FileReaderBackend>(
backend: &str,
variant: &str,
config: &BenchConfig,
) -> Vec<BenchmarkResult> {
let path = &config.test_file;
let reader = B::open(path).expect("Failed to open file");
let total = reader.total_lines();
let iterations: usize = if config.quick_mode { 5 } else { 10 };
let last_line = total.saturating_sub(1);
let mut latencies = Vec::with_capacity(iterations);
for _ in 0..iterations {
let t = std::time::Instant::now();
let _ = reader.get_line(last_line);
latencies.push(t.elapsed().as_micros() as u64);
}
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("last_line_idx".into(), last_line as f64);
reader.close();
vec![BenchmarkResult {
category: "jump".into(),
test_name: "jump_end".into(),
backend: backend.into(),
variant: variant.into(),
latency_us: latencies,
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}
fn bench_reverse_scan<B: FileReaderBackend>(
backend: &str,
variant: &str,
config: &BenchConfig,
) -> Vec<BenchmarkResult> {
let path = &config.test_file;
let reader = B::open(path).expect("Failed to open file");
let total = reader.total_lines();
let iterations: usize = if config.quick_mode { 5 } else { 10 };
let mut latencies = Vec::with_capacity(iterations * 35);
for _ in 0..iterations {
// Read lines backwards from end: last line, last-1, ..., last-34
let start = total.saturating_sub(35);
for i in (start..total).rev() {
let t = std::time::Instant::now();
let _ = reader.get_line(i);
latencies.push(t.elapsed().as_micros() as u64);
}
}
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
reader.close();
let mut extra = HashMap::new();
extra.insert("lines_per_scan".into(), 35.0);
extra.insert("iterations".into(), iterations as f64);
vec![BenchmarkResult {
category: "jump".into(),
test_name: "reverse_scan".into(),
backend: backend.into(),
variant: variant.into(),
latency_us: latencies,
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}

View File

@@ -0,0 +1,233 @@
use std::collections::HashMap;
use crate::metrics::MetricsCollector;
use crate::mmap_reader::{
MmapReaderPhaseAware, MmapReaderPlain, MmapReaderPopulate, MmapReaderRandom,
MmapReaderSequential,
};
use crate::pread_reader::{PreadReaderPlain, PreadReaderRandom, PreadReaderSequential};
use crate::runner::BenchConfig;
use crate::types::BenchmarkResult;
use crate::FileReaderBackend;
pub fn run(config: &BenchConfig) -> Vec<BenchmarkResult> {
let mut results = Vec::new();
results.extend(bench_idle_rss::<MmapReaderPlain>("mmap", "plain", config));
results.extend(bench_idle_rss::<MmapReaderSequential>(
"mmap",
"sequential",
config,
));
results.extend(bench_idle_rss::<MmapReaderRandom>("mmap", "random", config));
results.extend(bench_idle_rss::<MmapReaderPopulate>(
"mmap", "populate", config,
));
results.extend(bench_idle_rss::<MmapReaderPhaseAware>(
"mmap",
"phase_aware",
config,
));
results.extend(bench_idle_rss::<PreadReaderPlain>("pread", "plain", config));
results.extend(bench_idle_rss::<PreadReaderRandom>(
"pread", "random", config,
));
results.extend(bench_idle_rss::<PreadReaderSequential>(
"pread",
"sequential",
config,
));
results.extend(bench_scroll_rss::<MmapReaderPlain>("mmap", "plain", config));
results.extend(bench_scroll_rss::<PreadReaderPlain>(
"pread", "plain", config,
));
results.extend(bench_jump_end_rss::<MmapReaderPlain>(
"mmap", "plain", config,
));
results.extend(bench_jump_end_rss::<PreadReaderPlain>(
"pread", "plain", config,
));
results.extend(bench_rss_reclaim::<MmapReaderPlain>(
"mmap", "plain", config,
));
results.extend(bench_rss_reclaim::<PreadReaderPlain>(
"pread", "plain", config,
));
results
}
fn bench_idle_rss<B: FileReaderBackend>(
backend: &str,
variant: &str,
config: &BenchConfig,
) -> Vec<BenchmarkResult> {
let reader = B::open(&config.test_file).expect("Failed to open file");
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("total_lines".into(), reader.total_lines() as f64);
extra.insert(
"file_size_mb".into(),
reader.file_size() as f64 / (1024.0 * 1024.0),
);
reader.close();
vec![BenchmarkResult {
category: "memory".into(),
test_name: "idle_rss".into(),
backend: backend.into(),
variant: variant.into(),
latency_us: vec![],
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}
fn bench_scroll_rss<B: FileReaderBackend>(
backend: &str,
variant: &str,
config: &BenchConfig,
) -> Vec<BenchmarkResult> {
let reader = B::open(&config.test_file).expect("Failed to open file");
let total = reader.total_lines();
let sample_interval = 100_000;
let max_lines = if config.quick_mode { 100_000 } else { total };
let mut rss_samples = Vec::new();
let mut hwm_samples = Vec::new();
for i in (0..max_lines).step_by(sample_interval) {
let _ = reader.get_line(i);
let rss = MetricsCollector::read_rss();
rss_samples.push(rss.vm_rss_kb);
hwm_samples.push(rss.vm_hwm_kb);
}
let final_rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("rss_samples_count".into(), rss_samples.len() as f64);
extra.insert(
"max_rss_kb".into(),
rss_samples.iter().copied().fold(0u64, u64::max) as f64,
);
extra.insert(
"max_hwm_kb".into(),
hwm_samples.iter().copied().fold(0u64, u64::max) as f64,
);
extra.insert("lines_read".into(), max_lines.min(total) as f64);
reader.close();
vec![BenchmarkResult {
category: "memory".into(),
test_name: "scroll_rss".into(),
backend: backend.into(),
variant: variant.into(),
latency_us: vec![],
rss_kb: final_rss.vm_rss_kb,
rss_peak_kb: final_rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}
fn bench_jump_end_rss<B: FileReaderBackend>(
backend: &str,
variant: &str,
config: &BenchConfig,
) -> Vec<BenchmarkResult> {
let reader = B::open(&config.test_file).expect("Failed to open file");
let total = reader.total_lines();
let last_line = total.saturating_sub(1);
let _ = reader.get_line(last_line);
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("last_line_idx".into(), last_line as f64);
extra.insert("total_lines".into(), total as f64);
reader.close();
vec![BenchmarkResult {
category: "memory".into(),
test_name: "jump_end_rss".into(),
backend: backend.into(),
variant: variant.into(),
latency_us: vec![],
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}
fn bench_rss_reclaim<B: FileReaderBackend>(
backend: &str,
variant: &str,
config: &BenchConfig,
) -> Vec<BenchmarkResult> {
let reader = B::open(&config.test_file).expect("Failed to open file");
let total = reader.total_lines();
let last_line = total.saturating_sub(1);
let _ = reader.get_line(last_line);
let wait_secs: u64 = if config.quick_mode { 5 } else { 30 };
let sample_interval: u64 = 5;
let num_samples = (wait_secs / sample_interval) as usize;
let mut rss_samples = Vec::with_capacity(num_samples);
let mut hwm_samples = Vec::with_capacity(num_samples);
for _ in 0..num_samples {
std::thread::sleep(std::time::Duration::from_secs(sample_interval));
let rss = MetricsCollector::read_rss();
rss_samples.push(rss.vm_rss_kb);
hwm_samples.push(rss.vm_hwm_kb);
}
let final_rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
reader.close();
let mut extra = HashMap::new();
extra.insert("wait_total_secs".into(), wait_secs as f64);
extra.insert("rss_samples".into(), rss_samples.len() as f64);
if let (Some(&first), Some(&last)) = (rss_samples.first(), rss_samples.last()) {
extra.insert("rss_first_kb".into(), first as f64);
extra.insert("rss_last_kb".into(), last as f64);
extra.insert(
"rss_change_pct".into(),
if first > 0 {
((last as f64 - first as f64) / first as f64) * 100.0
} else {
0.0
},
);
}
vec![BenchmarkResult {
category: "memory".into(),
test_name: "rss_reclaim".into(),
backend: backend.into(),
variant: variant.into(),
latency_us: vec![],
rss_kb: final_rss.vm_rss_kb,
rss_peak_kb: final_rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}

View File

@@ -0,0 +1,7 @@
pub mod concurrent;
pub mod growth;
pub mod jump;
pub mod memory;
pub mod render;
pub mod rotation;
pub mod startup;

View File

@@ -0,0 +1,177 @@
use std::collections::HashMap;
use crate::metrics::MetricsCollector;
use crate::mmap_reader::{
MmapReaderPhaseAware, MmapReaderPlain, MmapReaderPopulate, MmapReaderRandom,
MmapReaderSequential,
};
use crate::pread_reader::{PreadReaderPlain, PreadReaderRandom, PreadReaderSequential};
use crate::runner::BenchConfig;
use crate::types::BenchmarkResult;
use crate::FileReaderBackend;
pub fn run(config: &BenchConfig) -> Vec<BenchmarkResult> {
let mut results = Vec::new();
results.extend(bench_single_frame::<MmapReaderPlain>(
"mmap", "plain", config,
));
results.extend(bench_single_frame::<MmapReaderSequential>(
"mmap",
"sequential",
config,
));
results.extend(bench_single_frame::<MmapReaderRandom>(
"mmap", "random", config,
));
results.extend(bench_single_frame::<MmapReaderPopulate>(
"mmap", "populate", config,
));
results.extend(bench_single_frame::<MmapReaderPhaseAware>(
"mmap",
"phase_aware",
config,
));
results.extend(bench_single_frame::<PreadReaderPlain>(
"pread", "plain", config,
));
results.extend(bench_single_frame::<PreadReaderRandom>(
"pread", "random", config,
));
results.extend(bench_single_frame::<PreadReaderSequential>(
"pread",
"sequential",
config,
));
results.extend(bench_continuous_scroll::<MmapReaderPlain>(
"mmap", "plain", config,
));
results.extend(bench_continuous_scroll::<MmapReaderSequential>(
"mmap",
"sequential",
config,
));
results.extend(bench_continuous_scroll::<MmapReaderRandom>(
"mmap", "random", config,
));
results.extend(bench_continuous_scroll::<MmapReaderPopulate>(
"mmap", "populate", config,
));
results.extend(bench_continuous_scroll::<MmapReaderPhaseAware>(
"mmap",
"phase_aware",
config,
));
results.extend(bench_continuous_scroll::<PreadReaderPlain>(
"pread", "plain", config,
));
results.extend(bench_continuous_scroll::<PreadReaderRandom>(
"pread", "random", config,
));
results.extend(bench_continuous_scroll::<PreadReaderSequential>(
"pread",
"sequential",
config,
));
results
}
fn bench_single_frame<B: FileReaderBackend>(
backend: &str,
variant: &str,
config: &BenchConfig,
) -> Vec<BenchmarkResult> {
let path = &config.test_file;
let reader = B::open(path).expect("Failed to open file");
let total = reader.total_lines();
let mut results = Vec::new();
let positions = [
("head", 0),
("middle", total / 2),
("tail", total.saturating_sub(35)),
];
for (pos_name, start_line) in positions {
let mut latencies = Vec::with_capacity(35);
for i in 0..35 {
let line_idx = start_line + i;
if line_idx >= total {
break;
}
let t = std::time::Instant::now();
let _ = reader.get_line(line_idx);
latencies.push(t.elapsed().as_micros() as u64);
}
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
results.push(BenchmarkResult {
category: "render".into(),
test_name: format!("single_frame_{pos_name}"),
backend: backend.into(),
variant: variant.into(),
latency_us: latencies,
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra: HashMap::new(),
});
}
reader.close();
results
}
fn bench_continuous_scroll<B: FileReaderBackend>(
backend: &str,
variant: &str,
config: &BenchConfig,
) -> Vec<BenchmarkResult> {
let path = &config.test_file;
let reader = B::open(path).expect("Failed to open file");
let total = reader.total_lines();
let iterations = if config.quick_mode { 100 } else { 1000 };
let timeout = if config.quick_mode {
std::time::Duration::from_secs(1)
} else {
std::time::Duration::from_secs(10)
};
let mut latencies = Vec::with_capacity(iterations);
let start = std::time::Instant::now();
for i in 0..iterations {
if start.elapsed() > timeout {
break;
}
let line_idx = i % total.max(1);
let t = std::time::Instant::now();
let _ = reader.get_line(line_idx);
latencies.push(t.elapsed().as_micros() as u64);
}
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("total_lines_scrolled".into(), latencies.len() as f64);
reader.close();
vec![BenchmarkResult {
category: "render".into(),
test_name: "continuous_scroll".into(),
backend: backend.into(),
variant: variant.into(),
latency_us: latencies,
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}

View File

@@ -0,0 +1,180 @@
use std::collections::HashMap;
use crate::data_gen;
use crate::metrics::MetricsCollector;
use crate::mmap_reader::{self, MmapReaderPlain};
use crate::pread_reader::PreadReaderPlain;
use crate::runner::BenchConfig;
use crate::types::BenchmarkResult;
use crate::FileReaderBackend;
pub fn run(config: &BenchConfig) -> Vec<BenchmarkResult> {
let mut results = Vec::new();
let dir = tempfile::tempdir().expect("Failed to create temp dir");
results.extend(bench_truncate_safety_mmap(config, dir.path()));
results.extend(bench_truncate_safety_pread(config, dir.path()));
results.extend(bench_rotation_detection(config, dir.path()));
results
}
fn bench_truncate_safety_mmap(
_config: &BenchConfig,
dir: &std::path::Path,
) -> Vec<BenchmarkResult> {
let sub_dir = dir.join("trunc_mmap");
let path = data_gen::generate_growable_file(&sub_dir).expect("Failed to create file");
let iterations: usize = if _config.quick_mode { 3 } else { 10 };
let mut latencies = Vec::with_capacity(iterations);
let mut sigbus_detected = 0usize;
for _ in 0..iterations {
mmap_reader::reset_sigbus_flag();
let reader = MmapReaderPlain::open(&path).expect("Failed to open file");
let original_size = reader.file_size();
let truncate_size = original_size / 2;
data_gen::truncate_file(&path, truncate_size).expect("Failed to truncate");
let t = std::time::Instant::now();
let mid_offset = original_size as u64 / 2;
let _ = reader.read_range(mid_offset, 64);
latencies.push(t.elapsed().as_micros() as u64);
if mmap_reader::sigbus_flag() {
sigbus_detected += 1;
}
reader.close();
let mut f = std::fs::File::create(&path).expect("Failed to recreate file");
use std::io::Write;
for i in 0..1000u64 {
writeln!(f, "restored line {i}").unwrap();
}
}
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("iterations".into(), iterations as f64);
extra.insert("sigbus_detected".into(), sigbus_detected as f64);
extra.insert("crashed".into(), 0.0);
vec![BenchmarkResult {
category: "rotation".into(),
test_name: "truncate_safety_mmap".into(),
backend: "mmap".into(),
variant: "plain".into(),
latency_us: latencies,
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}
fn bench_truncate_safety_pread(
_config: &BenchConfig,
dir: &std::path::Path,
) -> Vec<BenchmarkResult> {
let sub_dir = dir.join("trunc_pread");
let path = data_gen::generate_growable_file(&sub_dir).expect("Failed to create file");
let iterations: usize = if _config.quick_mode { 3 } else { 10 };
let mut latencies = Vec::with_capacity(iterations);
let mut error_count = 0usize;
for _ in 0..iterations {
let reader = PreadReaderPlain::open(&path).expect("Failed to open file");
let original_size = reader.file_size();
let truncate_size = original_size / 2;
data_gen::truncate_file(&path, truncate_size).expect("Failed to truncate");
let t = std::time::Instant::now();
let mid_offset = original_size as u64 / 2;
let result = reader.read_range(mid_offset, 64);
latencies.push(t.elapsed().as_micros() as u64);
if result.is_none() {
error_count += 1;
}
reader.close();
let mut f = std::fs::File::create(&path).expect("Failed to recreate file");
use std::io::Write;
for i in 0..1000u64 {
writeln!(f, "restored line {i}").unwrap();
}
}
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("iterations".into(), iterations as f64);
extra.insert("errors_returned".into(), error_count as f64);
extra.insert("crashed".into(), 0.0);
vec![BenchmarkResult {
category: "rotation".into(),
test_name: "truncate_safety_pread".into(),
backend: "pread".into(),
variant: "plain".into(),
latency_us: latencies,
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}
fn bench_rotation_detection(_config: &BenchConfig, dir: &std::path::Path) -> Vec<BenchmarkResult> {
let sub_dir = dir.join("rotation_detect");
let iterations: usize = if _config.quick_mode { 3 } else { 10 };
let mut latencies = Vec::with_capacity(iterations);
let mut detected_count = 0usize;
for _ in 0..iterations {
let path = data_gen::generate_growable_file(&sub_dir).expect("Failed to create file");
let original_inode = MetricsCollector::get_inode(&path).expect("Failed to get inode");
let _rotated = data_gen::rotate_file(&path).expect("Failed to rotate file");
let t = std::time::Instant::now();
let detected = MetricsCollector::detect_rotation(original_inode, &path);
latencies.push(t.elapsed().as_micros() as u64);
if detected {
detected_count += 1;
}
let _ = std::fs::remove_file(&path);
let rotated = sub_dir.join("growable.log.1");
let _ = std::fs::remove_file(&rotated);
}
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let mut extra = HashMap::new();
extra.insert("iterations".into(), iterations as f64);
extra.insert("rotations_detected".into(), detected_count as f64);
vec![BenchmarkResult {
category: "rotation".into(),
test_name: "rotation_detection".into(),
backend: "both".into(),
variant: "plain".into(),
latency_us: latencies,
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra,
}]
}

View File

@@ -0,0 +1,129 @@
use std::collections::HashMap;
use std::path::Path;
use crate::metrics::MetricsCollector;
use crate::mmap_reader::{
MmapReaderPhaseAware, MmapReaderPlain, MmapReaderPopulate, MmapReaderRandom,
MmapReaderSequential,
};
use crate::pread_reader::{PreadReaderPlain, PreadReaderRandom, PreadReaderSequential};
use crate::runner::BenchConfig;
use crate::types::BenchmarkResult;
use crate::FileReaderBackend;
pub fn run(config: &BenchConfig) -> Vec<BenchmarkResult> {
let mut results = Vec::new();
results.extend(bench_hot_open::<MmapReaderPlain>("mmap", "plain", config));
results.extend(bench_hot_open::<MmapReaderSequential>(
"mmap",
"sequential",
config,
));
results.extend(bench_hot_open::<MmapReaderRandom>("mmap", "random", config));
results.extend(bench_hot_open::<MmapReaderPopulate>(
"mmap", "populate", config,
));
results.extend(bench_hot_open::<MmapReaderPhaseAware>(
"mmap",
"phase_aware",
config,
));
results.extend(bench_hot_open::<PreadReaderPlain>("pread", "plain", config));
results.extend(bench_hot_open::<PreadReaderRandom>(
"pread", "random", config,
));
results.extend(bench_hot_open::<PreadReaderSequential>(
"pread",
"sequential",
config,
));
if !config.quick_mode {
if MetricsCollector::clear_file_cache(&config.test_file).is_ok() {
results.extend(bench_cold_open::<MmapReaderPlain>("mmap", "plain", config));
results.extend(bench_cold_open::<MmapReaderSequential>(
"mmap",
"sequential",
config,
));
results.extend(bench_cold_open::<MmapReaderRandom>(
"mmap", "random", config,
));
results.extend(bench_cold_open::<MmapReaderPopulate>(
"mmap", "populate", config,
));
results.extend(bench_cold_open::<MmapReaderPhaseAware>(
"mmap",
"phase_aware",
config,
));
results.extend(bench_cold_open::<PreadReaderPlain>(
"pread", "plain", config,
));
results.extend(bench_cold_open::<PreadReaderRandom>(
"pread", "random", config,
));
results.extend(bench_cold_open::<PreadReaderSequential>(
"pread",
"sequential",
config,
));
}
}
results
}
fn bench_hot_open<B: FileReaderBackend>(
backend: &str,
variant: &str,
config: &BenchConfig,
) -> Vec<BenchmarkResult> {
open_and_measure::<B>(backend, variant, &config.test_file, "hot_open")
}
fn bench_cold_open<B: FileReaderBackend>(
backend: &str,
variant: &str,
config: &BenchConfig,
) -> Vec<BenchmarkResult> {
let _ = MetricsCollector::clear_file_cache(&config.test_file);
open_and_measure::<B>(backend, variant, &config.test_file, "cold_open")
}
fn open_and_measure<B: FileReaderBackend>(
backend: &str,
variant: &str,
path: &Path,
test_name: &str,
) -> Vec<BenchmarkResult> {
let start = std::time::Instant::now();
let reader = B::open(path).expect("Failed to open file");
let elapsed = start.elapsed();
let rss = MetricsCollector::read_rss();
let faults = MetricsCollector::read_page_faults();
let result = BenchmarkResult {
category: "startup".into(),
test_name: test_name.into(),
backend: backend.into(),
variant: variant.into(),
latency_us: vec![elapsed.as_micros() as u64],
rss_kb: rss.vm_rss_kb,
rss_peak_kb: rss.vm_hwm_kb,
page_faults: faults.minor_faults + faults.major_faults,
extra: {
let mut m = HashMap::new();
m.insert("total_lines".into(), reader.total_lines() as f64);
m.insert(
"file_size_mb".into(),
reader.file_size() as f64 / (1024.0 * 1024.0),
);
m
},
};
reader.close();
vec![result]
}