Files
rustfs/crates/io-metrics/src/performance.rs
T
2026-03-30 00:30:57 +08:00

312 lines
9.5 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.
//! Performance metrics structure with atomic counters.
//!
//! Provides a shared metrics instance that can be used across all RustFS
//! components for consistent performance monitoring.
//!
//! # Example
//!
//! ```rust
//! use rustfs_io_metrics::PerformanceMetrics;
//!
//! let metrics = PerformanceMetrics::new();
//! metrics.record_cache_hit();
//! metrics.record_bytes_read(1024);
//! ```
use std::sync::atomic::{AtomicU64, Ordering};
/// Performance metrics with atomic counters.
///
/// Thread-safe metrics structure that can be shared across threads.
/// All fields use atomic operations for lock-free access.
#[derive(Debug)]
pub struct PerformanceMetrics {
// ===== Cache Metrics =====
/// Total cache hits (all levels)
pub cache_hits: AtomicU64,
/// Total cache misses
pub cache_misses: AtomicU64,
/// L1 cache hits (hot objects < 1MB)
pub l1_cache_hits: AtomicU64,
/// L2 cache hits (standard objects < 10MB)
pub l2_cache_hits: AtomicU64,
// ===== I/O Metrics =====
/// Total bytes read from disk
pub total_bytes_read: AtomicU64,
/// Total bytes written to disk
pub total_bytes_written: AtomicU64,
/// Disk read operation count
pub disk_read_count: AtomicU64,
/// Disk write operation count
pub disk_write_count: AtomicU64,
/// Average I/O latency in microseconds
pub avg_io_latency_us: AtomicU64,
/// P95 I/O latency in microseconds
pub p95_io_latency_us: AtomicU64,
/// P99 I/O latency in microseconds
pub p99_io_latency_us: AtomicU64,
// ===== Concurrency Metrics =====
/// Current concurrent requests
pub current_concurrent_requests: AtomicU64,
/// Peak concurrent requests
pub peak_concurrent_requests: AtomicU64,
// ===== Error Metrics =====
/// Total errors
pub total_errors: AtomicU64,
/// Timeout errors
pub timeout_errors: AtomicU64,
/// Disk errors
pub disk_errors: AtomicU64,
}
impl PerformanceMetrics {
/// Create a new PerformanceMetrics instance with all values initialized to zero.
pub fn new() -> Self {
Self {
cache_hits: AtomicU64::new(0),
cache_misses: AtomicU64::new(0),
l1_cache_hits: AtomicU64::new(0),
l2_cache_hits: AtomicU64::new(0),
total_bytes_read: AtomicU64::new(0),
total_bytes_written: AtomicU64::new(0),
disk_read_count: AtomicU64::new(0),
disk_write_count: AtomicU64::new(0),
avg_io_latency_us: AtomicU64::new(0),
p95_io_latency_us: AtomicU64::new(0),
p99_io_latency_us: AtomicU64::new(0),
current_concurrent_requests: AtomicU64::new(0),
peak_concurrent_requests: AtomicU64::new(0),
total_errors: AtomicU64::new(0),
timeout_errors: AtomicU64::new(0),
disk_errors: AtomicU64::new(0),
}
}
/// Calculate the cache hit rate (0.0 to 1.0).
///
/// Returns 0.0 if there have been no cache accesses.
pub fn cache_hit_rate(&self) -> f64 {
let hits = self.cache_hits.load(Ordering::Relaxed);
let misses = self.cache_misses.load(Ordering::Relaxed);
let total = hits + misses;
if total == 0 { 0.0 } else { hits as f64 / total as f64 }
}
/// Get the L1 cache hit rate (0.0 to 1.0).
///
/// Returns the ratio of L1 hits to total cache hits.
pub fn l1_hit_rate(&self) -> f64 {
let l1_hits = self.l1_cache_hits.load(Ordering::Relaxed);
let total_hits = self.cache_hits.load(Ordering::Relaxed);
if total_hits == 0 {
0.0
} else {
l1_hits as f64 / total_hits as f64
}
}
// ===== Cache Recording Methods =====
/// Record a cache hit.
#[inline]
pub fn record_cache_hit(&self) {
self.cache_hits.fetch_add(1, Ordering::Relaxed);
}
/// Record a cache miss.
#[inline]
pub fn record_cache_miss(&self) {
self.cache_misses.fetch_add(1, Ordering::Relaxed);
}
/// Record an L1 cache hit (includes recording total cache hit).
#[inline]
pub fn record_l1_hit(&self) {
self.l1_cache_hits.fetch_add(1, Ordering::Relaxed);
self.record_cache_hit();
}
/// Record an L2 cache hit (includes recording total cache hit).
#[inline]
pub fn record_l2_hit(&self) {
self.l2_cache_hits.fetch_add(1, Ordering::Relaxed);
self.record_cache_hit();
}
// ===== I/O Recording Methods =====
/// Record bytes read.
#[inline]
pub fn record_bytes_read(&self, bytes: u64) {
self.total_bytes_read.fetch_add(bytes, Ordering::Relaxed);
}
/// Record bytes written.
#[inline]
pub fn record_bytes_written(&self, bytes: u64) {
self.total_bytes_written.fetch_add(bytes, Ordering::Relaxed);
}
/// Record a disk read operation.
#[inline]
pub fn record_disk_read(&self) {
self.disk_read_count.fetch_add(1, Ordering::Relaxed);
}
/// Record a disk write operation.
#[inline]
pub fn record_disk_write(&self) {
self.disk_write_count.fetch_add(1, Ordering::Relaxed);
}
// ===== Concurrency Recording Methods =====
/// Update concurrent request count and track peak.
#[inline]
pub fn update_concurrent_requests(&self, count: u64) {
self.current_concurrent_requests.store(count, Ordering::Relaxed);
// Update peak using lock-free CAS loop
let mut peak = self.peak_concurrent_requests.load(Ordering::Relaxed);
loop {
if count <= peak {
break;
}
match self
.peak_concurrent_requests
.compare_exchange_weak(peak, count, Ordering::Relaxed, Ordering::Relaxed)
{
Ok(_) => break,
Err(new_peak) => peak = new_peak,
}
}
}
// ===== Error Recording Methods =====
/// Record a generic error.
#[inline]
pub fn record_error(&self) {
self.total_errors.fetch_add(1, Ordering::Relaxed);
}
/// Record a timeout error (includes recording total error).
#[inline]
pub fn record_timeout(&self) {
self.timeout_errors.fetch_add(1, Ordering::Relaxed);
self.record_error();
}
/// Record a disk error (includes recording total error).
#[inline]
pub fn record_disk_error(&self) {
self.disk_errors.fetch_add(1, Ordering::Relaxed);
self.record_error();
}
}
impl Default for PerformanceMetrics {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_creation() {
let metrics = PerformanceMetrics::new();
assert_eq!(metrics.cache_hits.load(Ordering::Relaxed), 0);
assert_eq!(metrics.cache_misses.load(Ordering::Relaxed), 0);
}
#[test]
fn test_cache_hit_rate() {
let metrics = PerformanceMetrics::new();
// No accesses yet
assert_eq!(metrics.cache_hit_rate(), 0.0);
// Record some hits and misses
for _ in 0..5 {
metrics.record_cache_hit();
}
for _ in 0..3 {
metrics.record_cache_miss();
}
assert_eq!(metrics.cache_hit_rate(), 5.0 / 8.0);
}
#[test]
fn test_l1_hit_rate() {
let metrics = PerformanceMetrics::new();
metrics.record_l1_hit(); // Records both L1 and total
metrics.record_l2_hit(); // Records L2 and total
metrics.record_cache_hit(); // Direct total hit
assert_eq!(metrics.cache_hits.load(Ordering::Relaxed), 3);
assert_eq!(metrics.l1_cache_hits.load(Ordering::Relaxed), 1);
assert_eq!(metrics.l1_hit_rate(), 1.0 / 3.0);
}
#[test]
fn test_io_recording() {
let metrics = PerformanceMetrics::new();
metrics.record_bytes_read(1024 * 1024); // 1MB
metrics.record_disk_read();
assert_eq!(metrics.total_bytes_read.load(Ordering::Relaxed), 1024 * 1024);
assert_eq!(metrics.disk_read_count.load(Ordering::Relaxed), 1);
}
#[test]
fn test_concurrent_tracking() {
let metrics = PerformanceMetrics::new();
metrics.update_concurrent_requests(5);
assert_eq!(metrics.current_concurrent_requests.load(Ordering::Relaxed), 5);
assert_eq!(metrics.peak_concurrent_requests.load(Ordering::Relaxed), 5);
metrics.update_concurrent_requests(3);
assert_eq!(metrics.current_concurrent_requests.load(Ordering::Relaxed), 3);
assert_eq!(metrics.peak_concurrent_requests.load(Ordering::Relaxed), 5); // Peak stays at 5
}
#[test]
fn test_error_recording() {
let metrics = PerformanceMetrics::new();
metrics.record_timeout();
assert_eq!(metrics.total_errors.load(Ordering::Relaxed), 1);
assert_eq!(metrics.timeout_errors.load(Ordering::Relaxed), 1);
metrics.record_disk_error();
assert_eq!(metrics.total_errors.load(Ordering::Relaxed), 2);
assert_eq!(metrics.disk_errors.load(Ordering::Relaxed), 1);
}
}