mod common;
use common::*;
use std::time::{Duration, Instant};
use hyperfile::config::{HyperFileConfigBuilder, HyperFileMetaConfig, HyperFileRuntimeConfig};
use hyperfile::file::flags::{FileFlags, HyperFileFlags};
use hyperfile::file::hyper::Hyper;
use hyperfile::file::mode::{FileMode, HyperFileMode};
use hyperfile::meta_format::BlockPtrFormat;
use hyperfile::staging::config::StagingConfig;
const MIB: usize = 1 << 20;
fn env_num(name: &str, default: usize) -> usize {
std::env::var(name).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
}
fn chunk(k: usize) -> Vec<u8> {
(0..MIB).map(|i| (k as u8).wrapping_mul(31) ^ (i as u8) ^ ((i >> 12) as u8)).collect()
}
async fn objects(client: &aws_sdk_s3::Client, uri: &str) -> Vec<f64> {
let (bucket, prefix) = uri.trim_start_matches("s3://").split_once('/').unwrap();
let mut v = Vec::new();
let mut pages = client.list_objects_v2().bucket(bucket).prefix(format!("{}/", prefix)).into_paginator().send();
while let Some(page) = pages.next().await {
for o in page.expect("list").contents().iter().filter(|o| !o.key().unwrap_or("").ends_with("/inode")) {
v.push(o.size().unwrap_or(0) as f64 / MIB as f64);
}
}
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
v
}
enum Handle {
Direct(Hyper<'static>),
#[cfg(feature = "reactor")]
Reactor(hyperfile::file::fh::HyperFileHandler<'static>),
}
impl Handle {
async fn write(&mut self, off: usize, buf: &[u8]) {
match self {
Self::Direct(h) => { h.fs_write(off, buf).await.expect("write"); },
#[cfg(feature = "reactor")]
Self::Reactor(fh) => { fh.fh_write(off, buf).await.expect("write"); },
}
}
async fn flush(&mut self) {
match self {
Self::Direct(h) => { h.fs_flush().await.expect("flush"); },
#[cfg(feature = "reactor")]
Self::Reactor(fh) => { fh.fh_flush().await.expect("flush"); },
}
}
async fn finish(self) -> hyperfile::file::FlushTimingSnapshot {
match self {
Self::Direct(mut h) => { let t = h.flush_timing().snapshot(); let _ = h.fs_release().await; t },
#[cfg(feature = "reactor")]
Self::Reactor(mut fh) => { let t = fh.fh_flush_timing().await.expect("timing"); let _ = fh.fh_release().await; t },
}
}
}
async fn run(client: &aws_sdk_s3::Client, fmt: BlockPtrFormat, ahead: usize, partials: bool, mib: usize, rate: usize)
-> (Duration, Duration, Duration, u64, Vec<f64>, hyperfile::file::FlushTimingSnapshot)
{
let hot = env_num("BENCH_HOT_MIB", 0);
let span = if hot > 0 { hot.min(mib) } else { mib };
let tf = TestFile::new(client).await;
let mut rt = HyperFileRuntimeConfig::default();
let default_limits = std::env::var("BENCH_LIMITS").is_ok_and(|v| v == "default");
if !default_limits {
rt.data_cache_dirty_max_bytes_threshold = 1 << 40;
rt.data_cache_dirty_max_blocks_threshold = (1 << 40) / 4096;
if !partials {
rt.segment_buffer_size = 1 << 40;
}
}
rt.data_cache_dirty_max_flush_interval = u64::MAX / 2;
rt.data_cache_blocks = 0;
rt.stream_segment_bytes = ahead;
let mut meta = HyperFileMetaConfig::default();
meta.block_ptr_format = fmt;
let config = HyperFileConfigBuilder::new()
.with_staging_config(&StagingConfig::new_s3_uri(tf.uri(), None))
.with_meta_config(&meta)
.with_runtime_config(&rt)
.build();
let hyper = Hyper::create(client.clone(), config.clone(), HyperFileFlags::from_flags(FileFlags::rdwr()),
HyperFileMode::from_mode(FileMode::default_file())).await.expect("create");
#[cfg(feature = "reactor")]
let reactor = hyperfile_reactor::Reactor::new_current().expect("reactor");
let mut h = if std::env::var("BENCH_REACTOR").is_ok_and(|v| v == "1") {
#[cfg(feature = "reactor")]
{ Handle::Reactor(hyperfile::file::fh::HyperFileHandler::fh_from_hyper(&reactor, hyper).await.expect("handler")) }
#[cfg(not(feature = "reactor"))]
{ panic!("BENCH_REACTOR needs the reactor feature") }
} else {
Handle::Direct(hyper)
};
let data: Vec<Vec<u8>> = (0..8).map(chunk).collect();
let mut longest = Duration::ZERO;
let start = Instant::now();
for k in 0..mib {
if rate > 0 {
let due = Duration::from_secs_f64(k as f64 / rate as f64 * (MIB as f64 / 1e6));
if let Some(wait) = due.checked_sub(start.elapsed()) {
tokio::time::sleep(wait).await;
}
}
let t = Instant::now();
h.write((k % span) * MIB, &data[k % 8]).await;
longest = longest.max(t.elapsed());
}
let wrote = start.elapsed();
let t = Instant::now();
h.flush().await;
let flushed = t.elapsed();
let timing = h.finish().await;
let mut c = Hyper::open(client.clone(), config, HyperFileFlags::from_flags(FileFlags::rdonly())).await.expect("open");
let mut buf = vec![0u8; MIB];
let mut bad = 0;
for k in (0..span).step_by(37).chain([span - 1]) {
let last = (mib - 1 - k) / span * span + k;
let n = c.fs_read(k * MIB, &mut buf).await.expect("read");
if n != MIB || buf != data[last % 8] {
bad += 1;
}
}
let _ = c.fs_release().await;
let n = objects(client, tf.uri()).await;
tf.cleanup(client).await;
(wrote, flushed, longest, bad, n, timing)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[ignore]
async fn bench_stream_flush() {
let client = make_client().await;
let mib = env_num("BENCH_MIB", 512);
let rates: Vec<usize> = std::env::var("BENCH_RATES").unwrap_or_else(|_| "0,400,200".into())
.split(',').filter_map(|r| r.parse().ok()).collect();
let sizes: Vec<usize> = std::env::var("BENCH_SIZES_MIB").unwrap_or_else(|_| "4,16,256,512,1024".into())
.split(',').filter_map(|r| r.parse().ok()).collect();
println!("BENCH {} MiB; stream_segment_bytes {:?} MiB", mib, sizes);
for rate in rates {
let mut cases: Vec<(String, BlockPtrFormat, usize)> = vec![("parted, all at flush".into(), BlockPtrFormat::PartedSegment, 0)];
cases.extend(sizes.iter().map(|m| (format!("stream {} MiB", m), BlockPtrFormat::StreamSegment, m * MIB)));
for (name, fmt, a) in cases {
let (w, f, longest, bad, n, t) = run(&client, fmt, a, false, mib, rate).await;
let hot = env_num("BENCH_HOT_MIB", 0);
let span = if hot > 0 { hot.min(mib) } else { mib } as u64;
let uploaded = if a == 0 && fmt == BlockPtrFormat::PartedSegment { span << 20 }
else { t.stream_sent_bytes + t.stream_flush_bytes };
let sizes = if n.is_empty() { String::new() } else {
format!("{:.0}/{:.0}/{:.0}", n[0], n[n.len() / 2], n[n.len() - 1]) };
println!("BENCH rate {:>4} MB/s {:<22} flush {:>5.2} s total {:>6.2} s longest write {:>5.3} s objects {:>4} (MiB min/med/max {:>11}) sent ahead {:>4} MiB (died in flight {:>4}) at flush {:>4} MiB data uploaded {:>4.1}x meta {:>4.1} MiB {}",
if rate == 0 { "max".to_string() } else { rate.to_string() }, name,
f.as_secs_f64(), (w + f).as_secs_f64(), longest.as_secs_f64(), n.len(), sizes,
t.stream_sent_bytes >> 20, t.stream_dead_bytes >> 20, t.stream_flush_bytes >> 20,
uploaded as f64 / (span << 20) as f64, t.stream_meta_bytes as f64 / MIB as f64,
if bad == 0 { "ok" } else { "DATA WRONG" });
assert_eq!(bad, 0);
}
}
}