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

280
crates/bench/src/metrics.rs Normal file
View File

@@ -0,0 +1,280 @@
use std::fs::{self, File};
use std::os::unix::fs::MetadataExt;
use std::os::unix::io::AsRawFd;
use std::path::Path;
pub struct RssMetrics {
pub vm_rss_kb: u64,
pub vm_hwm_kb: u64,
}
pub struct PageFaultMetrics {
pub minor_faults: u64,
pub major_faults: u64,
}
pub struct MetricsCollector;
impl MetricsCollector {
/// Read VmRSS and VmHWM from /proc/self/status
pub fn read_rss() -> RssMetrics {
let status = fs::read_to_string("/proc/self/status").unwrap_or_default();
let mut vm_rss_kb: u64 = 0;
let mut vm_hwm_kb: u64 = 0;
for line in status.lines() {
if line.starts_with("VmRSS:") {
vm_rss_kb = parse_kb_value(line);
} else if line.starts_with("VmHWM:") {
vm_hwm_kb = parse_kb_value(line);
}
}
RssMetrics {
vm_rss_kb,
vm_hwm_kb,
}
}
/// Read page fault counts from getrusage
pub fn read_page_faults() -> PageFaultMetrics {
let usage =
nix::sys::resource::getrusage(nix::sys::resource::UsageWho::RUSAGE_SELF).unwrap();
PageFaultMetrics {
// getrusage() returns c_long (i64 on 64-bit Linux) — explicit as u64 conversion
minor_faults: usage.minor_page_faults() as u64,
major_faults: usage.major_page_faults() as u64,
}
}
/// Clear page cache (requires root: sync + drop_caches)
/// Falls back to doing nothing if no permission
pub fn clear_page_cache() -> std::io::Result<()> {
let _ = std::process::Command::new("sync").status();
fs::write("/proc/sys/vm/drop_caches", "1")
}
/// Clear file cache using posix_fadvise(DONTNEED) — no root required
pub fn clear_file_cache(path: &Path) -> std::io::Result<()> {
let file = File::open(path)?;
let len = file.metadata()?.len();
let ret = unsafe {
libc::posix_fadvise(file.as_raw_fd(), 0, len as i64, libc::POSIX_FADV_DONTNEED)
};
// posix_fadvise returns error code directly (not errno), 0 = success
if ret != 0 {
return Err(std::io::Error::from_raw_os_error(ret));
}
Ok(())
}
/// Reset VmHWM by writing to /proc/self/clear_refs (requires root)
pub fn reset_vm_hwm() -> std::io::Result<()> {
fs::write("/proc/self/clear_refs", "5").map_err(|e| {
if e.kind() == std::io::ErrorKind::PermissionDenied {
std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"VmHWM reset requires root (can't write /proc/self/clear_refs)",
)
} else {
e
}
})
}
/// Check if we can reset VmHWM (i.e., have root)
pub fn can_reset_vm_hwm() -> bool {
fs::write("/proc/self/clear_refs", "5").is_ok()
}
/// Get file inode number
pub fn get_inode(path: &Path) -> std::io::Result<u64> {
let meta = fs::metadata(path)?;
Ok(meta.ino())
}
/// Check if file was rotated (inode changed)
pub fn detect_rotation(original_inode: u64, path: &Path) -> bool {
Self::get_inode(path)
.map(|ino| ino != original_inode)
.unwrap_or(true)
}
}
fn parse_kb_value(line: &str) -> u64 {
// Format: "VmRSS: 12345 kB"
line.split_whitespace()
.nth(1)
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(0)
}
pub fn mean(data: &[u64]) -> f64 {
if data.is_empty() {
return 0.0;
}
data.iter().sum::<u64>() as f64 / data.len() as f64
}
/// Percentile of data at given fraction (0.01.0). Returns from a sorted copy.
pub fn percentile(data: &[u64], p: f64) -> u64 {
if data.is_empty() {
return 0;
}
let mut sorted: Vec<u64> = data.to_vec();
sorted.sort_unstable();
let idx = ((p * (sorted.len() - 1) as f64).round()) as usize;
sorted[idx.min(sorted.len() - 1)]
}
pub fn stdev(data: &[u64]) -> f64 {
if data.len() < 2 {
return 0.0;
}
let m = mean(data);
let variance: f64 = data
.iter()
.map(|&v| {
let d = v as f64 - m;
d * d
})
.sum::<f64>()
/ (data.len() - 1) as f64;
variance.sqrt()
}
pub fn p50(data: &[u64]) -> u64 {
percentile(data, 0.50)
}
pub fn p95(data: &[u64]) -> u64 {
percentile(data, 0.95)
}
pub fn p99(data: &[u64]) -> u64 {
percentile(data, 0.99)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rss_returns_values() {
let rss = MetricsCollector::read_rss();
assert!(
rss.vm_rss_kb > 0,
"VmRSS should be non-zero for a running process"
);
assert!(
rss.vm_hwm_kb > 0,
"VmHWM should be non-zero for a running process"
);
}
#[test]
fn test_page_faults_returns_values() {
let faults = MetricsCollector::read_page_faults();
assert!(
faults.minor_faults > 0,
"Should have some minor page faults"
);
}
#[test]
fn test_mean() {
let data = vec![100, 200, 300, 400, 500];
let result = mean(&data);
assert!(
(result - 300.0).abs() < f64::EPSILON,
"mean should be 300.0, got {result}"
);
}
#[test]
fn test_mean_empty() {
assert_eq!(mean(&[]), 0.0);
}
#[test]
fn test_percentile_p50() {
let data = vec![100, 200, 300, 400, 500];
assert_eq!(percentile(&data, 0.50), 300);
}
#[test]
fn test_percentile_p99() {
let data = vec![10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
let p99_result = percentile(&data, 0.99);
assert!(p99_result >= 90, "P99 should be near max, got {p99_result}");
}
#[test]
fn test_percentile_empty() {
assert_eq!(percentile(&[], 0.5), 0);
}
#[test]
fn test_stdev() {
let data = vec![100, 200, 300, 400, 500];
let s = stdev(&data);
assert!(s > 100.0, "stdev should be significant, got {s}");
assert!(s < 200.0, "stdev should be < 200, got {s}");
}
#[test]
fn test_stdev_single() {
assert_eq!(stdev(&[42]), 0.0);
assert_eq!(stdev(&[]), 0.0);
}
#[test]
fn test_parse_kb_value() {
assert_eq!(parse_kb_value("VmRSS: 12345 kB"), 12345);
assert_eq!(parse_kb_value("VmHWM:\t2048 kB"), 2048);
assert_eq!(parse_kb_value("VmRSS: 0 kB"), 0);
}
#[test]
fn test_parse_kb_value_malformed() {
assert_eq!(parse_kb_value("VmRSS: NaN kB"), 0);
assert_eq!(parse_kb_value("garbage"), 0);
}
#[test]
fn test_convenience_percentiles() {
let data = vec![10, 20, 30, 40, 50];
assert_eq!(p50(&data), 30);
assert_eq!(p95(&data), 50);
assert_eq!(p99(&data), 50);
}
#[test]
fn test_inode_for_existing_file() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let inode = MetricsCollector::get_inode(tmp.path()).unwrap();
assert!(inode > 0, "inode should be non-zero");
}
#[test]
fn test_detect_rotation_no_rotation() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let inode = MetricsCollector::get_inode(tmp.path()).unwrap();
assert!(!MetricsCollector::detect_rotation(inode, tmp.path()));
}
#[test]
fn test_detect_rotation_file_removed() {
let inode: u64 = 99999;
let result = MetricsCollector::detect_rotation(inode, Path::new("/no/such/file"));
assert!(result, "missing file should indicate rotation");
}
#[test]
fn test_clear_file_cache() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let result = MetricsCollector::clear_file_cache(tmp.path());
assert!(
result.is_ok(),
"clear_file_cache should succeed on temp file: {result:?}"
);
}
}