use moonpool_core::{OpenOptions, StorageFile, StorageProvider};
use moonpool_sim::{SimWorld, StorageConfiguration};
use std::net::IpAddr;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
const TEST_IP_STR: &str = "127.0.0.1";
fn test_ip() -> IpAddr {
TEST_IP_STR.parse().expect("valid IP")
}
fn local_runtime() -> tokio::runtime::LocalRuntime {
tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build_local(Default::default())
.expect("Failed to build local runtime")
}
async fn run_and_measure_time<F, Fut>(mut sim: SimWorld, f: F) -> Duration
where
F: FnOnce(moonpool_sim::SimStorageProvider) -> Fut,
Fut: std::future::Future<Output = std::io::Result<()>> + 'static,
{
let provider = sim.storage_provider(test_ip());
let handle = tokio::task::spawn_local(f(provider));
while !handle.is_finished() {
while sim.pending_event_count() > 0 {
sim.step();
}
tokio::task::yield_now().await;
}
handle.await.expect("task panicked").expect("io error");
sim.current_time()
}
#[test]
fn test_low_iops_increases_latency() {
local_runtime().block_on(async {
let base = Duration::from_micros(10);
let high_iops_config = StorageConfiguration {
iops: 1_000_000, bandwidth: 1_000_000_000,
read_latency: base..base,
write_latency: base..base,
sync_latency: base..base,
..StorageConfiguration::fast_local()
};
let low_iops_config = StorageConfiguration {
iops: 100, bandwidth: 1_000_000_000,
read_latency: base..base,
write_latency: base..base,
sync_latency: base..base,
..StorageConfiguration::fast_local()
};
let mut sim_high = SimWorld::new();
sim_high.set_storage_config(high_iops_config);
let high_time = run_and_measure_time(sim_high, |provider| async move {
let mut file = provider
.open("iops_test.txt", OpenOptions::create_write())
.await?;
for _ in 0..10 {
file.write_all(b"x").await?;
}
Ok(())
})
.await;
let mut sim_low = SimWorld::new();
sim_low.set_storage_config(low_iops_config);
let low_time = run_and_measure_time(sim_low, |provider| async move {
let mut file = provider
.open("iops_test.txt", OpenOptions::create_write())
.await?;
for _ in 0..10 {
file.write_all(b"x").await?;
}
Ok(())
})
.await;
println!("High IOPS (1M): {:?}", high_time);
println!("Low IOPS (100): {:?}", low_time);
assert!(
low_time > high_time,
"Low IOPS should take longer: {:?} vs {:?}",
low_time,
high_time
);
assert!(
low_time >= Duration::from_millis(50),
"Low IOPS operations should take substantial time: {:?}",
low_time
);
});
}
#[test]
fn test_low_bandwidth_increases_latency() {
local_runtime().block_on(async {
let base = Duration::from_micros(10);
let high_bw_config = StorageConfiguration {
iops: 1_000_000,
bandwidth: 1_000_000_000, read_latency: base..base,
write_latency: base..base,
sync_latency: base..base,
..StorageConfiguration::fast_local()
};
let low_bw_config = StorageConfiguration {
iops: 1_000_000,
bandwidth: 10_000, read_latency: base..base,
write_latency: base..base,
sync_latency: base..base,
..StorageConfiguration::fast_local()
};
let data = vec![0u8; 1024];
let mut sim_high = SimWorld::new();
sim_high.set_storage_config(high_bw_config);
let data_clone = data.clone();
let high_time = run_and_measure_time(sim_high, |provider| async move {
let mut file = provider
.open("bw_test.txt", OpenOptions::create_write())
.await?;
file.write_all(&data_clone).await?;
Ok(())
})
.await;
let mut sim_low = SimWorld::new();
sim_low.set_storage_config(low_bw_config);
let low_time = run_and_measure_time(sim_low, |provider| async move {
let mut file = provider
.open("bw_test.txt", OpenOptions::create_write())
.await?;
file.write_all(&data).await?;
Ok(())
})
.await;
println!("High BW (1GB/s): {:?}", high_time);
println!("Low BW (10KB/s): {:?}", low_time);
assert!(
low_time > high_time,
"Low bandwidth should take longer: {:?} vs {:?}",
low_time,
high_time
);
assert!(
low_time >= Duration::from_millis(50),
"Low bandwidth transfer should take substantial time: {:?}",
low_time
);
});
}
#[test]
fn test_large_file_bandwidth_constraint() {
local_runtime().block_on(async {
let base = Duration::from_micros(1);
let config = StorageConfiguration {
iops: 1_000_000,
bandwidth: 100_000, read_latency: base..base,
write_latency: base..base,
sync_latency: base..base,
..StorageConfiguration::fast_local()
};
let mut sim = SimWorld::new();
sim.set_storage_config(config);
let data = vec![0u8; 10 * 1024];
let time = run_and_measure_time(sim, |provider| async move {
let mut file = provider
.open("large.txt", OpenOptions::create_write())
.await?;
file.write_all(&data).await?;
Ok(())
})
.await;
println!("10KB at 100KB/s: {:?}", time);
assert!(
time >= Duration::from_millis(80),
"Large file should respect bandwidth: {:?}",
time
);
assert!(
time <= Duration::from_millis(200),
"Latency shouldn't be excessive: {:?}",
time
);
});
}
#[test]
fn test_iops_and_bandwidth_combine() {
local_runtime().block_on(async {
let base = Duration::from_micros(1);
let config = StorageConfiguration {
iops: 100, bandwidth: 10_000, read_latency: base..base,
write_latency: base..base,
sync_latency: base..base,
..StorageConfiguration::fast_local()
};
let mut sim = SimWorld::new();
sim.set_storage_config(config);
let data = vec![0u8; 1024];
let time = run_and_measure_time(sim, |provider| async move {
let mut file = provider
.open("combined.txt", OpenOptions::create_write())
.await?;
file.write_all(&data).await?;
Ok(())
})
.await;
println!("1KB with IOPS=100, BW=10KB/s: {:?}", time);
assert!(
time >= Duration::from_millis(50),
"Combined constraints should add up: {:?}",
time
);
});
}
#[test]
fn test_read_bandwidth_constraint() {
local_runtime().block_on(async {
let base = Duration::from_micros(1);
let config = StorageConfiguration {
iops: 1_000_000,
bandwidth: 50_000, read_latency: base..base,
write_latency: base..base,
sync_latency: base..base,
..StorageConfiguration::fast_local()
};
let mut sim = SimWorld::new();
sim.set_storage_config(config.clone());
let provider = sim.storage_provider(test_ip());
let handle = tokio::task::spawn_local(async move {
let mut file = provider
.open("read_bw.txt", OpenOptions::create_write())
.await?;
file.write_all(&vec![b'x'; 5 * 1024]).await?; file.sync_all().await?;
Ok::<_, std::io::Error>(())
});
while !handle.is_finished() {
while sim.pending_event_count() > 0 {
sim.step();
}
tokio::task::yield_now().await;
}
handle.await.expect("task panicked").expect("io error");
let start_time = sim.current_time();
let provider2 = sim.storage_provider(test_ip());
let handle2 = tokio::task::spawn_local(async move {
let mut file = provider2
.open("read_bw.txt", OpenOptions::read_only())
.await?;
let mut buf = Vec::new();
file.read_to_end(&mut buf).await?;
Ok::<_, std::io::Error>(buf.len())
});
while !handle2.is_finished() {
while sim.pending_event_count() > 0 {
sim.step();
}
tokio::task::yield_now().await;
}
let bytes_read = handle2.await.expect("task panicked").expect("io error");
let read_time = sim.current_time() - start_time;
println!("Read {} bytes at 50KB/s: {:?}", bytes_read, read_time);
assert!(
read_time >= Duration::from_millis(50),
"Read should respect bandwidth: {:?}",
read_time
);
});
}
#[test]
fn test_sequential_small_ops_iops_limited() {
local_runtime().block_on(async {
let base = Duration::from_micros(1);
let config = StorageConfiguration {
iops: 50, bandwidth: 1_000_000_000,
read_latency: base..base,
write_latency: base..base,
sync_latency: base..base,
..StorageConfiguration::fast_local()
};
let mut sim = SimWorld::new();
sim.set_storage_config(config);
let time = run_and_measure_time(sim, |provider| async move {
let mut file = provider
.open("seq_ops.txt", OpenOptions::create_write())
.await?;
for i in 0..5 {
file.write_all(&[i]).await?;
}
Ok(())
})
.await;
println!("5 small ops at 50 IOPS: {:?}", time);
assert!(
time >= Duration::from_millis(50),
"Sequential ops should be IOPS limited: {:?}",
time
);
});
}
#[test]
fn test_fast_local_minimal_overhead() {
local_runtime().block_on(async {
let mut sim = SimWorld::new();
sim.set_storage_config(StorageConfiguration::fast_local());
let data = vec![0u8; 100 * 1024];
let time = run_and_measure_time(sim, |provider| async move {
let mut file = provider
.open("fast.txt", OpenOptions::create_write())
.await?;
file.write_all(&data).await?;
file.sync_all().await?;
Ok(())
})
.await;
println!("100KB with fast_local: {:?}", time);
assert!(
time < Duration::from_millis(10),
"fast_local should have minimal overhead: {:?}",
time
);
});
}
#[test]
fn test_hdd_vs_ssd_performance() {
local_runtime().block_on(async {
let hdd_config = StorageConfiguration {
iops: 150,
bandwidth: 150_000_000, read_latency: Duration::from_millis(5)..Duration::from_millis(5),
write_latency: Duration::from_millis(5)..Duration::from_millis(5),
sync_latency: Duration::from_millis(10)..Duration::from_millis(10),
..StorageConfiguration::fast_local()
};
let ssd_config = StorageConfiguration {
iops: 50_000,
bandwidth: 500_000_000, read_latency: Duration::from_micros(100)..Duration::from_micros(100),
write_latency: Duration::from_micros(100)..Duration::from_micros(100),
sync_latency: Duration::from_millis(1)..Duration::from_millis(1),
..StorageConfiguration::fast_local()
};
let mut sim_hdd = SimWorld::new();
sim_hdd.set_storage_config(hdd_config);
let hdd_time = run_and_measure_time(sim_hdd, |provider| async move {
let mut file = provider
.open("random.txt", OpenOptions::create_write())
.await?;
for i in 0..10u8 {
file.write_all(&[i]).await?;
}
file.sync_all().await?;
Ok(())
})
.await;
let mut sim_ssd = SimWorld::new();
sim_ssd.set_storage_config(ssd_config);
let ssd_time = run_and_measure_time(sim_ssd, |provider| async move {
let mut file = provider
.open("random.txt", OpenOptions::create_write())
.await?;
for i in 0..10u8 {
file.write_all(&[i]).await?;
}
file.sync_all().await?;
Ok(())
})
.await;
println!("HDD (150 IOPS): {:?}", hdd_time);
println!("SSD (50K IOPS): {:?}", ssd_time);
assert!(
hdd_time > ssd_time,
"HDD should be slower for random I/O: {:?} vs {:?}",
hdd_time,
ssd_time
);
});
}