Files
rustfs/crates/io-core/src/lock_optimizer.rs
T

394 lines
11 KiB
Rust

// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Lock optimization utilities.
//!
//! This module provides lock optimization strategies and statistics
//! to improve concurrent performance.
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
/// Lock optimization configuration.
#[derive(Debug, Clone)]
pub struct LockOptimizeConfig {
/// Whether optimization is enabled.
pub enabled: bool,
/// Lock acquire timeout.
pub acquire_timeout: Duration,
/// Maximum hold time warning threshold.
pub max_hold_time_warning: Duration,
/// Enable adaptive spinning.
pub adaptive_spin: bool,
/// Maximum spin iterations.
pub max_spin_iterations: usize,
}
impl Default for LockOptimizeConfig {
fn default() -> Self {
Self {
enabled: true,
acquire_timeout: Duration::from_secs(5),
max_hold_time_warning: Duration::from_millis(100),
adaptive_spin: true,
max_spin_iterations: 1000,
}
}
}
/// Lock statistics.
#[derive(Debug, Default)]
pub struct LockStats {
/// Number of locks acquired.
pub locks_acquired: AtomicU64,
/// Number of locks released early (before timeout).
pub locks_released_early: AtomicU64,
/// Total hold time in nanoseconds.
pub total_hold_time_ns: AtomicU64,
/// Maximum hold time in nanoseconds.
pub max_hold_time_ns: AtomicU64,
/// Number of contention events.
pub contentions: AtomicU64,
/// Number of spin successes.
pub spin_successes: AtomicU64,
/// Number of spin failures.
pub spin_failures: AtomicU64,
}
impl LockStats {
/// Create new lock statistics.
pub fn new() -> Self {
Self::default()
}
/// Record a lock acquisition.
pub fn record_acquire(&self) {
self.locks_acquired.fetch_add(1, Ordering::Relaxed);
}
/// Record a lock release.
pub fn record_release(&self, hold_time: Duration) {
let ns = hold_time.as_nanos() as u64;
self.total_hold_time_ns.fetch_add(ns, Ordering::Relaxed);
// Update max hold time
let mut current = self.max_hold_time_ns.load(Ordering::Relaxed);
while ns > current {
match self
.max_hold_time_ns
.compare_exchange_weak(current, ns, Ordering::Relaxed, Ordering::Relaxed)
{
Ok(_) => break,
Err(actual) => current = actual,
}
}
}
/// Record an early release.
pub fn record_early_release(&self) {
self.locks_released_early.fetch_add(1, Ordering::Relaxed);
}
/// Record a contention event.
pub fn record_contention(&self) {
self.contentions.fetch_add(1, Ordering::Relaxed);
}
/// Record a spin success.
pub fn record_spin_success(&self) {
self.spin_successes.fetch_add(1, Ordering::Relaxed);
}
/// Record a spin failure.
pub fn record_spin_failure(&self) {
self.spin_failures.fetch_add(1, Ordering::Relaxed);
}
/// Get total locks acquired.
pub fn total_acquired(&self) -> u64 {
self.locks_acquired.load(Ordering::Relaxed)
}
/// Get average hold time.
pub fn avg_hold_time(&self) -> Duration {
let total = self.total_hold_time_ns.load(Ordering::Relaxed);
let count = self.locks_acquired.load(Ordering::Relaxed);
total.checked_div(count).map(Duration::from_nanos).unwrap_or(Duration::ZERO)
}
/// Get maximum hold time.
pub fn max_hold_time(&self) -> Duration {
Duration::from_nanos(self.max_hold_time_ns.load(Ordering::Relaxed))
}
/// Get contention rate.
pub fn contention_rate(&self) -> f64 {
let acquired = self.locks_acquired.load(Ordering::Relaxed);
let contentions = self.contentions.load(Ordering::Relaxed);
if acquired == 0 {
0.0
} else {
contentions as f64 / acquired as f64
}
}
/// Get spin success rate.
pub fn spin_success_rate(&self) -> f64 {
let successes = self.spin_successes.load(Ordering::Relaxed);
let failures = self.spin_failures.load(Ordering::Relaxed);
let total = successes + failures;
if total == 0 { 0.0 } else { successes as f64 / total as f64 }
}
/// Reset all statistics.
pub fn reset(&self) {
self.locks_acquired.store(0, Ordering::Relaxed);
self.locks_released_early.store(0, Ordering::Relaxed);
self.total_hold_time_ns.store(0, Ordering::Relaxed);
self.max_hold_time_ns.store(0, Ordering::Relaxed);
self.contentions.store(0, Ordering::Relaxed);
self.spin_successes.store(0, Ordering::Relaxed);
self.spin_failures.store(0, Ordering::Relaxed);
}
}
/// Lock optimizer.
pub struct LockOptimizer {
/// Configuration.
config: LockOptimizeConfig,
/// Statistics.
stats: LockStats,
/// Current spin iterations (adaptive).
current_spin: AtomicUsize,
}
impl LockOptimizer {
/// Create a new lock optimizer.
pub fn new(config: LockOptimizeConfig) -> Self {
Self {
config,
stats: LockStats::new(),
current_spin: AtomicUsize::new(100),
}
}
/// Create with default configuration.
pub fn with_defaults() -> Self {
Self::new(LockOptimizeConfig::default())
}
/// Get the configuration.
pub fn config(&self) -> &LockOptimizeConfig {
&self.config
}
/// Get the statistics.
pub fn stats(&self) -> &LockStats {
&self.stats
}
/// Record lock acquisition.
pub fn on_acquire(&self) {
if !self.config.enabled {
return;
}
self.stats.record_acquire();
}
/// Record lock release.
pub fn on_release(&self, hold_time: Duration) {
if !self.config.enabled {
return;
}
self.stats.record_release(hold_time);
// Check for early release
if hold_time < self.config.acquire_timeout / 2 {
self.stats.record_early_release();
}
}
/// Record contention.
pub fn on_contention(&self) {
if !self.config.enabled {
return;
}
self.stats.record_contention();
}
/// Perform adaptive spin.
///
/// Returns true if the lock was acquired during spinning.
pub fn try_spin<F>(&self, mut try_acquire: F) -> bool
where
F: FnMut() -> bool,
{
if !self.config.enabled || !self.config.adaptive_spin {
return false;
}
let spin_count = self.current_spin.load(Ordering::Relaxed).min(self.config.max_spin_iterations);
for _ in 0..spin_count {
if try_acquire() {
self.stats.record_spin_success();
self.adapt_spin(true);
return true;
}
// Hint to the CPU that we're spinning
std::hint::spin_loop();
}
self.stats.record_spin_failure();
self.adapt_spin(false);
false
}
/// Adapt spin count based on success/failure.
fn adapt_spin(&self, success: bool) {
let current = self.current_spin.load(Ordering::Relaxed);
let new_count = if success {
// Increase spin count on success (up to max)
(current * 2).min(self.config.max_spin_iterations)
} else {
// Decrease spin count on failure (down to min)
(current / 2).max(10)
};
self.current_spin.store(new_count, Ordering::Relaxed);
}
/// Get current spin count.
pub fn current_spin_count(&self) -> usize {
self.current_spin.load(Ordering::Relaxed)
}
/// Check if hold time is excessive.
pub fn is_hold_time_excessive(&self, hold_time: Duration) -> bool {
hold_time > self.config.max_hold_time_warning
}
/// Reset statistics.
pub fn reset_stats(&self) {
self.stats.reset();
}
}
/// RAII guard for tracking lock hold time.
pub struct LockGuard<'a> {
optimizer: &'a LockOptimizer,
start: Instant,
}
impl<'a> LockGuard<'a> {
/// Create a new lock guard.
pub fn new(optimizer: &'a LockOptimizer) -> Self {
optimizer.on_acquire();
Self {
optimizer,
start: Instant::now(),
}
}
}
impl Drop for LockGuard<'_> {
fn drop(&mut self) {
self.optimizer.on_release(self.start.elapsed());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lock_stats() {
let stats = LockStats::new();
stats.record_acquire();
stats.record_acquire();
stats.record_release(Duration::from_millis(10));
stats.record_release(Duration::from_millis(20));
assert_eq!(stats.total_acquired(), 2);
assert!(stats.avg_hold_time() >= Duration::from_millis(15));
assert_eq!(stats.max_hold_time(), Duration::from_millis(20));
}
#[test]
fn test_contention_rate() {
let stats = LockStats::new();
stats.record_acquire();
stats.record_acquire();
stats.record_acquire();
stats.record_contention();
assert!((stats.contention_rate() - 0.3333333333333333).abs() < 0.01);
}
#[test]
fn test_spin_stats() {
let stats = LockStats::new();
stats.record_spin_success();
stats.record_spin_success();
stats.record_spin_failure();
assert!((stats.spin_success_rate() - 0.6666666666666666).abs() < 0.01);
}
#[test]
fn test_lock_optimizer() {
let optimizer = LockOptimizer::with_defaults();
{
let _guard = LockGuard::new(&optimizer);
std::thread::sleep(Duration::from_millis(10));
}
assert_eq!(optimizer.stats().total_acquired(), 1);
assert!(optimizer.stats().avg_hold_time() >= Duration::from_millis(10));
}
#[test]
fn test_adaptive_spin() {
let optimizer = LockOptimizer::with_defaults();
// Simulate successful spin
let acquired = optimizer.try_spin(|| true);
assert!(acquired);
assert!(optimizer.current_spin_count() > 100); // Should increase
// Simulate failed spin
let acquired = optimizer.try_spin(|| false);
assert!(!acquired);
assert!(optimizer.current_spin_count() < 200); // Should decrease
}
#[test]
fn test_disabled_optimizer() {
let config = LockOptimizeConfig {
enabled: false,
..Default::default()
};
let optimizer = LockOptimizer::new(config);
optimizer.on_acquire();
optimizer.on_release(Duration::from_millis(10));
// Should not track when disabled
assert_eq!(optimizer.stats().total_acquired(), 0);
}
}