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:
269
crates/bench/src/suites/growth.rs
Normal file
269
crates/bench/src/suites/growth.rs
Normal 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,
|
||||
}]
|
||||
}
|
||||
Reference in New Issue
Block a user