use crate::webgpu::error::{ComputeError, ComputeResult};
use std::collections::HashMap;
use std::sync::{
atomic::{AtomicU64, AtomicUsize, Ordering},
Arc, Mutex,
};
use std::time::{Duration, Instant};
#[cfg(feature = "gpu")]
use ::wgpu::{Buffer, BufferDescriptor, BufferUsages, Device};
#[cfg(not(feature = "gpu"))]
#[derive(Debug)]
pub struct Device;
#[cfg(not(feature = "gpu"))]
pub struct Queue;
#[cfg(not(feature = "gpu"))]
pub struct Buffer;
#[cfg(not(feature = "gpu"))]
pub struct BufferDescriptor<'a> {
pub label: Option<&'a str>,
pub size: u64,
pub usage: BufferUsages,
pub mapped_at_creation: bool,
}
#[cfg(not(feature = "gpu"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BufferUsages;
#[cfg(not(feature = "gpu"))]
impl BufferUsages {
pub const STORAGE: Self = BufferUsages;
pub const COPY_DST: Self = BufferUsages;
pub const COPY_SRC: Self = BufferUsages;
pub const UNIFORM: Self = BufferUsages;
pub const MAP_READ: Self = BufferUsages;
pub fn contains(&self, _other: Self) -> bool {
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BufferCategory {
Micro, Small, Medium, Large, XLarge, }
impl BufferCategory {
pub fn from_size(size: u64) -> Self {
const KB: u64 = 1024;
const MB: u64 = 1024 * 1024;
if size < KB {
Self::Micro
} else if size < MB {
Self::Small
} else if size < 10 * MB {
Self::Medium
} else if size < 100 * MB {
Self::Large
} else {
Self::XLarge
}
}
pub fn pool_config(&self) -> PoolTierConfig {
match self {
Self::Micro => PoolTierConfig {
max_buffers: 2048,
prealloc_count: 512,
cleanup_threshold: 0.95,
coalescing_enabled: true,
pressure_response_factor: 1.5,
},
Self::Small => PoolTierConfig {
max_buffers: 1024,
prealloc_count: 128,
cleanup_threshold: 0.9,
coalescing_enabled: true,
pressure_response_factor: 1.3,
},
Self::Medium => PoolTierConfig {
max_buffers: 256,
prealloc_count: 32,
cleanup_threshold: 0.8,
coalescing_enabled: false,
pressure_response_factor: 1.2,
},
Self::Large => PoolTierConfig {
max_buffers: 64,
prealloc_count: 8,
cleanup_threshold: 0.7,
coalescing_enabled: false,
pressure_response_factor: 1.1,
},
Self::XLarge => PoolTierConfig {
max_buffers: 16,
prealloc_count: 2,
cleanup_threshold: 0.6,
coalescing_enabled: false,
pressure_response_factor: 1.0,
},
}
}
pub fn size_range(&self) -> (u64, u64) {
const KB: u64 = 1024;
const MB: u64 = 1024 * 1024;
match self {
Self::Micro => (0, KB),
Self::Small => (KB, MB),
Self::Medium => (MB, 10 * MB),
Self::Large => (10 * MB, 100 * MB),
Self::XLarge => (100 * MB, u64::MAX),
}
}
pub fn expected_latency_ns(&self) -> u64 {
match self {
Self::Micro => 50_000, Self::Small => 100_000, Self::Medium => 500_000, Self::Large => 2_000_000, Self::XLarge => 10_000_000, }
}
}
#[derive(Debug, Clone)]
pub struct PoolTierConfig {
pub max_buffers: usize,
pub prealloc_count: usize,
pub cleanup_threshold: f32,
pub coalescing_enabled: bool,
pub pressure_response_factor: f32,
}
pub struct GpuBuffer {
#[cfg(feature = "gpu")]
pub buffer: Buffer,
#[cfg(not(feature = "gpu"))]
pub buffer: Buffer,
pub size: u64,
pub usage: BufferUsages,
pub category: BufferCategory,
created_at: Instant,
last_used: Instant,
use_count: AtomicU64,
allocation_id: u64,
performance_score: AtomicU64, }
impl GpuBuffer {
#[cfg(feature = "gpu")]
pub fn new(
device: &Device,
size: u64,
usage: BufferUsages,
label: Option<&str>,
allocation_id: u64,
) -> Self {
let buffer = device.create_buffer(&BufferDescriptor {
label,
size,
usage,
mapped_at_creation: false,
});
Self {
buffer,
size,
usage,
category: BufferCategory::from_size(size),
created_at: Instant::now(),
last_used: Instant::now(),
use_count: AtomicU64::new(0),
allocation_id,
performance_score: AtomicU64::new(500), }
}
#[cfg(not(feature = "gpu"))]
pub fn new(
_device: &Device,
size: u64,
usage: BufferUsages,
_label: Option<&str>,
allocation_id: u64,
) -> Self {
Self {
buffer: Buffer,
size,
usage,
category: BufferCategory::from_size(size),
created_at: Instant::now(),
last_used: Instant::now(),
use_count: AtomicU64::new(0),
allocation_id,
performance_score: AtomicU64::new(500),
}
}
pub fn age(&self) -> Duration {
self.created_at.elapsed()
}
pub fn idle_time(&self) -> Duration {
self.last_used.elapsed()
}
pub fn mark_used(&self) {
self.use_count.fetch_add(1, Ordering::Relaxed);
}
pub fn times_used(&self) -> u64 {
self.use_count.load(Ordering::Relaxed)
}
pub fn allocation_id(&self) -> u64 {
self.allocation_id
}
pub fn update_performance_score(&self, latency_ns: u64, throughput_mbps: f64) {
let expected_latency = self.category.expected_latency_ns();
let latency_score = if latency_ns <= expected_latency {
1000
} else {
((expected_latency as f64 / latency_ns as f64) * 1000.0) as u64
};
let throughput_score = (throughput_mbps.min(1000.0) * 1000.0 / 1000.0) as u64;
let combined_score = (latency_score + throughput_score) / 2;
self.performance_score
.store(combined_score, Ordering::Relaxed);
}
pub fn get_performance_score(&self) -> f32 {
self.performance_score.load(Ordering::Relaxed) as f32 / 1000.0
}
pub fn reuse_efficiency(&self) -> f32 {
let use_count = self.times_used() as f32;
let age_hours = self.age().as_secs_f32() / 3600.0;
if age_hours < 0.01 {
use_count * 10.0 } else {
use_count / age_hours.max(0.01)
}
}
}
impl std::fmt::Debug for GpuBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GpuBuffer")
.field("size", &self.size)
.field("category", &self.category)
.field("age", &self.age())
.field("times_used", &self.times_used())
.field("performance", &self.get_performance_score())
.finish()
}
}
#[derive(Debug)]
pub struct BufferTierPool {
buffers: Vec<GpuBuffer>,
config: PoolTierConfig,
coalescing_candidates: Vec<GpuBuffer>,
tier_stats: TierStatistics,
last_optimization: Instant,
}
#[derive(Debug, Default)]
pub struct TierStatistics {
pub cache_hits: AtomicU64,
pub cache_misses: AtomicU64,
pub coalescings: AtomicU64,
pub pressure_cleanups: AtomicU64,
pub avg_allocation_latency_ns: AtomicU64,
pub peak_buffer_count: AtomicUsize,
pub total_bytes_allocated: AtomicU64,
}
#[derive(Debug)]
pub struct AdvancedBufferPool {
device: Arc<Device>,
pools: Mutex<HashMap<BufferCategory, BufferTierPool>>,
next_allocation_id: AtomicU64,
global_stats: PoolGlobalStatistics,
pressure_circuit_breaker: Option<Arc<PressureCircuitBreaker>>,
optimization_thread_handle: Option<std::thread::JoinHandle<()>>,
}
#[derive(Debug, Default)]
pub struct PoolGlobalStatistics {
pub total_allocations: AtomicU64,
pub total_deallocations: AtomicU64,
pub total_cache_hits: AtomicU64,
pub total_cache_misses: AtomicU64,
pub memory_pressure_events: AtomicU64,
pub circuit_breaker_trips: AtomicU64,
pub total_memory_allocated: AtomicU64,
pub peak_memory_usage: AtomicU64,
pub avg_allocation_latency_ns: AtomicU64,
}
#[derive(Debug)]
pub struct PressureCircuitBreaker {
failure_threshold: usize,
recovery_timeout: Duration,
state: Mutex<CircuitBreakerState>,
failure_count: AtomicUsize,
last_failure: Mutex<Option<Instant>>,
}
#[derive(Debug, Clone, PartialEq)]
enum CircuitBreakerState {
Closed, Open, HalfOpen, }
impl PressureCircuitBreaker {
pub fn new(failure_threshold: usize, recovery_timeout: Duration) -> Self {
Self {
failure_threshold,
recovery_timeout,
state: Mutex::new(CircuitBreakerState::Closed),
failure_count: AtomicUsize::new(0),
last_failure: Mutex::new(None),
}
}
pub fn execute<F, R>(&self, operation: F) -> ComputeResult<R>
where
F: FnOnce() -> ComputeResult<R>,
{
let state = {
let mut state = self.state.lock().unwrap();
match state.clone() {
CircuitBreakerState::Open => {
if let Some(last_failure) = *self.last_failure.lock().unwrap() {
if last_failure.elapsed() >= self.recovery_timeout {
*state = CircuitBreakerState::HalfOpen;
CircuitBreakerState::HalfOpen
} else {
return Err(ComputeError::MemoryError(
"Circuit breaker is open due to memory pressure".to_string(),
));
}
} else {
CircuitBreakerState::Open
}
}
other => other,
}
};
match state {
CircuitBreakerState::Closed | CircuitBreakerState::HalfOpen => {
match operation() {
Ok(result) => {
self.failure_count.store(0, Ordering::Relaxed);
if state == CircuitBreakerState::HalfOpen {
*self.state.lock().unwrap() = CircuitBreakerState::Closed;
}
Ok(result)
}
Err(err) => {
self.record_failure();
Err(err)
}
}
}
CircuitBreakerState::Open => Err(ComputeError::MemoryError(
"Circuit breaker is open".to_string(),
)),
}
}
fn record_failure(&self) {
let failure_count = self.failure_count.fetch_add(1, Ordering::Relaxed) + 1;
*self.last_failure.lock().unwrap() = Some(Instant::now());
if failure_count >= self.failure_threshold {
*self.state.lock().unwrap() = CircuitBreakerState::Open;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Hash, Ord)]
pub enum MemoryPressure {
None = 0,
Low = 1,
Medium = 2,
High = 3,
Critical = 4,
}
impl MemoryPressure {
pub fn from_ratio(ratio: f32) -> Self {
match ratio {
r if r < 0.6 => MemoryPressure::None,
r if r < 0.7 => MemoryPressure::Low,
r if r < 0.8 => MemoryPressure::Medium,
r if r < 0.9 => MemoryPressure::High,
_ => MemoryPressure::Critical,
}
}
pub fn cleanup_aggressiveness(&self) -> f32 {
match self {
MemoryPressure::None => 0.1,
MemoryPressure::Low => 0.3,
MemoryPressure::Medium => 0.5,
MemoryPressure::High => 0.8,
MemoryPressure::Critical => 1.0,
}
}
}
impl AdvancedBufferPool {
pub fn new(device: Arc<Device>) -> Self {
let mut pools = HashMap::new();
for category in [
BufferCategory::Micro,
BufferCategory::Small,
BufferCategory::Medium,
BufferCategory::Large,
BufferCategory::XLarge,
] {
let config = category.pool_config();
pools.insert(
category,
BufferTierPool {
buffers: Vec::with_capacity(config.max_buffers),
config,
coalescing_candidates: Vec::new(),
tier_stats: TierStatistics::default(),
last_optimization: Instant::now(),
},
);
}
let circuit_breaker = Some(Arc::new(PressureCircuitBreaker::new(
5, Duration::from_secs(30), )));
Self {
device,
pools: Mutex::new(pools),
next_allocation_id: AtomicU64::new(1),
global_stats: PoolGlobalStatistics::default(),
pressure_circuit_breaker: circuit_breaker,
optimization_thread_handle: None,
}
}
pub fn get_buffer(
&self,
size: u64,
usage: BufferUsages,
label: Option<&str>,
) -> ComputeResult<GpuBuffer> {
let start_time = Instant::now();
let category = BufferCategory::from_size(size);
if let Some(ref circuit_breaker) = self.pressure_circuit_breaker {
return circuit_breaker
.execute(|| self.get_buffer_internal(size, usage, label, category, start_time));
}
self.get_buffer_internal(size, usage, label, category, start_time)
}
fn get_buffer_internal(
&self,
size: u64,
usage: BufferUsages,
label: Option<&str>,
category: BufferCategory,
start_time: Instant,
) -> ComputeResult<GpuBuffer> {
let mut pools = self.pools.lock().unwrap();
if let Some(tier_pool) = pools.get_mut(&category) {
if let Some(pos) = tier_pool.buffers.iter().position(|buf| {
buf.size >= size && buf.usage.contains(usage) && buf.size <= size * 2
}) {
let buffer = tier_pool.buffers.swap_remove(pos);
buffer.mark_used();
tier_pool
.tier_stats
.cache_hits
.fetch_add(1, Ordering::Relaxed);
self.global_stats
.total_cache_hits
.fetch_add(1, Ordering::Relaxed);
let latency_ns = start_time.elapsed().as_nanos() as u64;
tier_pool
.tier_stats
.avg_allocation_latency_ns
.store(latency_ns, Ordering::Relaxed);
buffer.update_performance_score(latency_ns, 1000.0);
return Ok(buffer);
}
if tier_pool.config.coalescing_enabled && tier_pool.coalescing_candidates.len() >= 2 {
if let Some(buffer) = self.try_coalesce_buffers(tier_pool, size, usage) {
tier_pool
.tier_stats
.coalescings
.fetch_add(1, Ordering::Relaxed);
return Ok(buffer);
}
}
}
self.create_new_buffer(size, usage, label, start_time)
}
fn create_new_buffer(
&self,
size: u64,
usage: BufferUsages,
label: Option<&str>,
start_time: Instant,
) -> ComputeResult<GpuBuffer> {
let allocation_id = self.next_allocation_id.fetch_add(1, Ordering::SeqCst);
let pressure = self.calculate_memory_pressure();
if pressure >= MemoryPressure::Critical {
self.global_stats
.memory_pressure_events
.fetch_add(1, Ordering::Relaxed);
return Err(ComputeError::MemoryError(format!(
"Critical memory pressure detected: {pressure:?}"
)));
}
let buffer = GpuBuffer::new(&self.device, size, usage, label, allocation_id);
self.global_stats
.total_allocations
.fetch_add(1, Ordering::Relaxed);
self.global_stats
.total_memory_allocated
.fetch_add(size, Ordering::Relaxed);
let current_memory = self
.global_stats
.total_memory_allocated
.load(Ordering::Relaxed);
let mut peak = self.global_stats.peak_memory_usage.load(Ordering::Relaxed);
while current_memory > peak {
match self.global_stats.peak_memory_usage.compare_exchange_weak(
peak,
current_memory,
Ordering::SeqCst,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(current) => peak = current,
}
}
let latency_ns = start_time.elapsed().as_nanos() as u64;
self.global_stats
.avg_allocation_latency_ns
.store(latency_ns, Ordering::Relaxed);
buffer.update_performance_score(latency_ns, 500.0);
Ok(buffer)
}
pub fn return_buffer(&self, buffer: GpuBuffer) {
let mut pools = self.pools.lock().unwrap();
if let Some(tier_pool) = pools.get_mut(&buffer.category) {
if self.should_retain_buffer(&buffer, tier_pool) {
if tier_pool.config.coalescing_enabled && buffer.size < 4096 {
tier_pool.coalescing_candidates.push(buffer);
} else {
tier_pool.buffers.push(buffer);
}
} else {
self.global_stats
.total_deallocations
.fetch_add(1, Ordering::Relaxed);
self.global_stats
.total_memory_allocated
.fetch_sub(buffer.size, Ordering::Relaxed);
}
}
}
fn should_retain_buffer(&self, buffer: &GpuBuffer, tier_pool: &BufferTierPool) -> bool {
if tier_pool.buffers.len() >= tier_pool.config.max_buffers {
return false;
}
if buffer.age() > Duration::from_secs(300) {
return false;
}
let efficiency = buffer.reuse_efficiency();
let performance_score = buffer.get_performance_score();
let retention_score = efficiency * 0.6 + performance_score * 0.4;
retention_score > tier_pool.config.cleanup_threshold
}
fn calculate_memory_pressure(&self) -> MemoryPressure {
let allocated = self
.global_stats
.total_memory_allocated
.load(Ordering::Relaxed) as f32;
let peak = self.global_stats.peak_memory_usage.load(Ordering::Relaxed) as f32;
let estimated_total = peak * 1.2; let pressure_ratio = allocated / estimated_total;
MemoryPressure::from_ratio(pressure_ratio)
}
fn try_coalesce_buffers(
&self,
tier_pool: &mut BufferTierPool,
size: u64,
usage: BufferUsages,
) -> Option<GpuBuffer> {
let mut total_size = 0u64;
let mut compatible_buffers = Vec::new();
tier_pool.coalescing_candidates.retain(|buf| {
if buf.usage.contains(usage) && total_size < size {
total_size += buf.size;
compatible_buffers.push(buf.allocation_id);
false } else {
true }
});
if compatible_buffers.len() >= 2 && total_size >= size {
let coalesced_size = total_size.next_power_of_two();
let allocation_id = self.next_allocation_id.fetch_add(1, Ordering::SeqCst);
let buffer = GpuBuffer::new(
&self.device,
coalesced_size,
usage,
Some("coalesced_buffer"),
allocation_id,
);
buffer.update_performance_score(50_000, 1500.0);
Some(buffer)
} else {
None
}
}
pub fn get_statistics(&self) -> PoolStatisticsSnapshot {
let pools = self.pools.lock().unwrap();
let mut tier_stats = HashMap::new();
for (&category, tier_pool) in pools.iter() {
tier_stats.insert(
category,
TierStatisticsSnapshot {
cache_hits: tier_pool.tier_stats.cache_hits.load(Ordering::Relaxed),
cache_misses: tier_pool.tier_stats.cache_misses.load(Ordering::Relaxed),
coalescings: tier_pool.tier_stats.coalescings.load(Ordering::Relaxed),
pressure_cleanups: tier_pool
.tier_stats
.pressure_cleanups
.load(Ordering::Relaxed),
avg_allocation_latency_ns: tier_pool
.tier_stats
.avg_allocation_latency_ns
.load(Ordering::Relaxed),
peak_buffer_count: tier_pool
.tier_stats
.peak_buffer_count
.load(Ordering::Relaxed),
current_buffer_count: tier_pool.buffers.len(),
coalescing_candidates: tier_pool.coalescing_candidates.len(),
},
);
}
PoolStatisticsSnapshot {
global: GlobalStatisticsSnapshot {
total_allocations: self.global_stats.total_allocations.load(Ordering::Relaxed),
total_deallocations: self
.global_stats
.total_deallocations
.load(Ordering::Relaxed),
total_cache_hits: self.global_stats.total_cache_hits.load(Ordering::Relaxed),
total_cache_misses: self.global_stats.total_cache_misses.load(Ordering::Relaxed),
memory_pressure_events: self
.global_stats
.memory_pressure_events
.load(Ordering::Relaxed),
circuit_breaker_trips: self
.global_stats
.circuit_breaker_trips
.load(Ordering::Relaxed),
total_memory_allocated: self
.global_stats
.total_memory_allocated
.load(Ordering::Relaxed),
peak_memory_usage: self.global_stats.peak_memory_usage.load(Ordering::Relaxed),
avg_allocation_latency_ns: self
.global_stats
.avg_allocation_latency_ns
.load(Ordering::Relaxed),
current_pressure: self.calculate_memory_pressure(),
},
tier_stats,
}
}
pub fn cleanup_with_pressure_response(&self, pressure: MemoryPressure) {
let mut pools = self.pools.lock().unwrap();
let aggressiveness = pressure.cleanup_aggressiveness();
for tier_pool in pools.values_mut() {
let cleanup_threshold = tier_pool.config.cleanup_threshold * aggressiveness;
let max_age = Duration::from_secs((300.0 * (1.0 - aggressiveness)) as u64);
let before_count = tier_pool.buffers.len();
tier_pool.buffers.retain(|buffer| {
!(buffer.age() > max_age || buffer.reuse_efficiency() < cleanup_threshold)
});
tier_pool.coalescing_candidates.retain(|buffer| {
buffer.age() <= max_age && buffer.reuse_efficiency() >= cleanup_threshold
});
let cleaned_count = before_count - tier_pool.buffers.len();
if cleaned_count > 0 {
tier_pool
.tier_stats
.pressure_cleanups
.fetch_add(cleaned_count as u64, Ordering::Relaxed);
}
}
}
}
#[derive(Debug, Clone)]
pub struct PoolStatisticsSnapshot {
pub global: GlobalStatisticsSnapshot,
pub tier_stats: HashMap<BufferCategory, TierStatisticsSnapshot>,
}
#[derive(Debug, Clone)]
pub struct GlobalStatisticsSnapshot {
pub total_allocations: u64,
pub total_deallocations: u64,
pub total_cache_hits: u64,
pub total_cache_misses: u64,
pub memory_pressure_events: u64,
pub circuit_breaker_trips: u64,
pub total_memory_allocated: u64,
pub peak_memory_usage: u64,
pub avg_allocation_latency_ns: u64,
pub current_pressure: MemoryPressure,
}
#[derive(Debug, Clone)]
pub struct TierStatisticsSnapshot {
pub cache_hits: u64,
pub cache_misses: u64,
pub coalescings: u64,
pub pressure_cleanups: u64,
pub avg_allocation_latency_ns: u64,
pub peak_buffer_count: usize,
pub current_buffer_count: usize,
pub coalescing_candidates: usize,
}
impl PoolStatisticsSnapshot {
pub fn cache_hit_ratio(&self) -> f32 {
let hits = self.global.total_cache_hits;
let misses = self.global.total_cache_misses;
if hits + misses > 0 {
hits as f32 / (hits + misses) as f32
} else {
0.0
}
}
pub fn memory_efficiency(&self) -> f32 {
if self.global.peak_memory_usage > 0 {
self.global.total_memory_allocated as f32 / self.global.peak_memory_usage as f32
} else {
0.0
}
}
pub fn performance_summary(&self) -> String {
format!(
"Pool Performance: {:.1}% cache hit rate, {:.2}ms avg latency, {:?} pressure",
self.cache_hit_ratio() * 100.0,
self.global.avg_allocation_latency_ns as f64 / 1_000_000.0,
self.global.current_pressure
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_buffer_category_from_size() {
assert_eq!(BufferCategory::from_size(512), BufferCategory::Micro);
assert_eq!(BufferCategory::from_size(512 * 1024), BufferCategory::Small);
assert_eq!(
BufferCategory::from_size(5 * 1024 * 1024),
BufferCategory::Medium
);
assert_eq!(
BufferCategory::from_size(50 * 1024 * 1024),
BufferCategory::Large
);
assert_eq!(
BufferCategory::from_size(500 * 1024 * 1024),
BufferCategory::XLarge
);
}
#[test]
fn test_memory_pressure_levels() {
assert_eq!(MemoryPressure::from_ratio(0.5), MemoryPressure::None);
assert_eq!(MemoryPressure::from_ratio(0.65), MemoryPressure::Low);
assert_eq!(MemoryPressure::from_ratio(0.75), MemoryPressure::Medium);
assert_eq!(MemoryPressure::from_ratio(0.85), MemoryPressure::High);
assert_eq!(MemoryPressure::from_ratio(0.95), MemoryPressure::Critical);
}
#[test]
fn test_pool_tier_config() {
let config = BufferCategory::Micro.pool_config();
assert_eq!(config.max_buffers, 2048);
assert!(config.coalescing_enabled);
assert_eq!(config.pressure_response_factor, 1.5);
let config = BufferCategory::XLarge.pool_config();
assert_eq!(config.max_buffers, 16);
assert!(!config.coalescing_enabled);
assert_eq!(config.pressure_response_factor, 1.0);
}
#[test]
fn test_circuit_breaker_states() {
let breaker = PressureCircuitBreaker::new(3, Duration::from_millis(100));
let result = breaker.execute(|| -> ComputeResult<i32> { Ok(42) });
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
for _ in 0..3 {
let _ = breaker.execute(|| -> ComputeResult<i32> {
Err(ComputeError::MemoryError("test failure".to_string()))
});
}
let result = breaker.execute(|| -> ComputeResult<i32> { Ok(42) });
assert!(result.is_err());
}
}