Files
foxhunt/crates/ml-stress-testing/src/load_generator.rs
jgrusewski 4676fe79e2 refactor(ml): extract observability, stress-testing, security into sub-crates
- ml-observability (1.2K lines): alerts, dashboards, metrics modules.
  Depends on ml-core + common (ModelType). 4 tests passing.

- ml-stress-testing (1.3K lines): load_generator, market_simulator,
  performance_analyzer modules. Depends on ml-core + common + config.
  5 tests passing.

- ml-security (1.4K lines): anomaly_detector, prediction_validator
  modules. Depends on ml-core + ml-ensemble (EnsembleDecision,
  ModelVote, TradingAction). 17 tests passing.

Total: 18 sub-crates extracted from ml monolith.
Workspace: 0 errors, ml tests 876 + 26 in new sub-crates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:17:22 +01:00

108 lines
3.1 KiB
Rust

//! Load generation for ML model stress testing
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Semaphore;
/// Load generation profiles
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum LoadProfile {
Constant,
Ramp,
Spike,
Burst,
Sine,
}
/// Traffic pattern configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrafficPattern {
pub profile: LoadProfile,
pub base_rps: u32,
pub peak_rps: u32,
pub pattern_duration_seconds: u64,
}
/// Load generator for stress testing
#[derive(Debug)]
pub struct LoadGenerator {
target_rps: u32,
concurrent_limit: Arc<Semaphore>,
load_multiplier: f64,
}
impl LoadGenerator {
pub fn new(target_rps: u32, concurrent_connections: u32) -> Result<Self> {
Ok(Self {
target_rps,
concurrent_limit: Arc::new(Semaphore::new(concurrent_connections as usize)),
load_multiplier: 1.0,
})
}
pub fn set_load_multiplier(&mut self, multiplier: f64) {
self.load_multiplier = multiplier;
}
pub fn get_current_rps(&self) -> u32 {
(self.target_rps as f64 * self.load_multiplier) as u32
}
/// Generate load according to traffic pattern
pub async fn generate_load<F>(&self, pattern: TrafficPattern, mut operation: F) -> Result<()>
where
F: FnMut() -> Result<()> + Send,
{
let start_time = Instant::now();
let pattern_duration = Duration::from_secs(pattern.pattern_duration_seconds);
while start_time.elapsed() < pattern_duration {
let elapsed_ratio = start_time.elapsed().as_secs_f64() / pattern_duration.as_secs_f64();
let current_rps = self.calculate_rps_for_pattern(&pattern, elapsed_ratio);
let interval = Duration::from_secs_f64(1.0 / current_rps as f64);
// Acquire semaphore permit for concurrency control
let _permit = self.concurrent_limit.acquire().await?;
// Execute operation
operation()?;
tokio::time::sleep(interval).await;
}
Ok(())
}
fn calculate_rps_for_pattern(&self, pattern: &TrafficPattern, elapsed_ratio: f64) -> u32 {
let base = pattern.base_rps as f64;
let peak = pattern.peak_rps as f64;
let current_rps = match pattern.profile {
LoadProfile::Constant => base,
LoadProfile::Ramp => base + (peak - base) * elapsed_ratio,
LoadProfile::Spike => {
if elapsed_ratio > 0.8 && elapsed_ratio < 0.9 {
peak
} else {
base
}
},
LoadProfile::Burst => {
if elapsed_ratio.rem_euclid(0.2) < 0.1 {
peak
} else {
base
}
},
LoadProfile::Sine => {
base + (peak - base) * (std::f64::consts::PI * elapsed_ratio * 2.0).sin().abs()
},
};
(current_rps * self.load_multiplier) as u32
}
}