use super::*;
use flint_store::memory::MemoryStore;
use flint_store::{ObjectStore, RequestCounts};
use std::sync::Arc;
use std::time::Instant;
fn put(root: &std::path::Path, i: usize, body: &str) {
let p = root.join(format!("d{:04}/f{i:07}.txt", i / 1000));
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, body).unwrap();
}
fn got(root: &std::path::Path, i: usize) -> Option<String> {
std::fs::read_to_string(root.join(format!("d{:04}/f{i:07}.txt", i / 1000))).ok()
}
fn bench_store() -> (Arc<dyn ObjectStore>, &'static str, Option<Arc<MemoryStore>>) {
#[cfg(feature = "s3")]
if let Ok(endpoint) = std::env::var("FLINT_BENCH_S3_ENDPOINT") {
let env = |k: &str| std::env::var(k).unwrap_or_else(|_| panic!("{k} must be set with FLINT_BENCH_S3_ENDPOINT"));
let s = flint_store::s3::S3Store::with_credentials(
env("FLINT_BENCH_S3_BUCKET"),
endpoint,
"us-east-1",
env("FLINT_BENCH_S3_KEY"),
env("FLINT_BENCH_S3_SECRET"),
)
.expect("an S3 store from the FLINT_BENCH_S3_* settings");
return (Arc::new(s), "s3", None);
}
let m = Arc::new(MemoryStore::new());
(m.clone(), "memory", Some(m))
}
fn bench_arm() -> &'static str {
match std::env::var("FLINT_BENCH_ARM").as_deref() {
Ok("control") => "control",
Ok("noview") => "noview",
Ok("nogate") => "nogate",
Ok("nowait") => "nowait",
Ok("shipped") | Err(_) => "shipped",
Ok(other) => panic!("FLINT_BENCH_ARM={other}: no such arm"),
}
}
fn bench_scan_threads() -> Option<usize> {
let v = std::env::var("FLINT_BENCH_SCAN_THREADS").ok()?;
match v.parse() {
Ok(n) if n >= 1 => Some(n),
_ => panic!("FLINT_BENCH_SCAN_THREADS={v}: not a thread count"),
}
}
fn bench_syncer(store: &Arc<dyn ObjectStore>, root: &std::path::Path, prefix: &str) -> Syncer {
let mut cfg = LeanConfig::new(prefix, root);
if bench_arm() == "control" {
cfg.chunk_cache = None;
cfg.chunk_fetch_parallelism = 1;
cfg.collect_every_secs = 0;
}
if bench_arm() == "noview" {
cfg.chunk_view = false;
}
if bench_arm() == "nogate" {
cfg.racy_at_gate = false;
}
if bench_arm() == "nowait" {
cfg.racy_wait_min = 0;
}
if let Some(n) = bench_scan_threads() {
cfg.scan_threads = n;
}
let state = super::state::SyncerState::open(cfg.state_dir()).unwrap();
Syncer {
store: store.clone(),
cfg,
state,
lease: None,
cell_written_at: None,
noted_not_regular: Default::default(),
}
}
struct Ops {
store: Arc<dyn ObjectStore>,
last: RequestCounts,
}
impl Ops {
fn new(store: &Arc<dyn ObjectStore>) -> Self {
Self { store: store.clone(), last: store.request_counts().unwrap_or_default() }
}
fn take(&mut self) -> String {
let now = self.store.request_counts().unwrap_or_default();
let d = |a: u64, b: u64| b.saturating_sub(a);
let s = [
("get", d(self.last.get, now.get)),
("head", d(self.last.head, now.head)),
("put", d(self.last.put, now.put)),
("copy", d(self.last.copy, now.copy)),
("delete", d(self.last.delete, now.delete)),
("list", d(self.last.list, now.list)),
("multipart", d(self.last.multipart, now.multipart)),
]
.iter()
.filter(|(_, n)| *n > 0)
.map(|(k, n)| format!("{k}={n}"))
.collect::<Vec<_>>()
.join(" ");
self.last = now;
s
}
}
async fn timed(sc: &mut Syncer, ops: &mut Ops, n: usize, store: &str, label: &str) -> barrier::BarrierReport {
let t = Instant::now();
let r = sc.run_barrier().await.unwrap();
let ms = t.elapsed().as_millis() as u64;
println!("BENCH n={n} {label}_ms={ms} ops: {}", ops.take());
let line = serde_json::json!({
"store": store, "n": n, "barrier": label, "ms": ms,
"uploaded": r.uploaded.len(), "no_change": r.no_change, "steps": r.steps,
});
println!("STEPS {line}");
r
}
#[test]
#[ignore]
fn walk_cost_alone() {
let n: usize = std::env::var("FLINT_BENCH_N").ok().and_then(|v| v.parse().ok()).unwrap_or(1_000_000);
let walks: usize = std::env::var("FLINT_BENCH_WALKS").ok().and_then(|v| v.parse().ok()).unwrap_or(5);
let threads = bench_scan_threads().unwrap_or_else(super::scan::default_threads);
let tmp = tempfile::tempdir().unwrap();
let root = match std::env::var_os("FLINT_BENCH_WALK_DIR") {
Some(d) => std::path::PathBuf::from(d),
None => tmp.path().to_path_buf(),
};
if std::fs::read_dir(&root).map_or(true, |mut d| d.next().is_none()) {
for i in 0..n {
put(&root, i, "x");
}
unsafe { libc::sync() };
println!("WALK n={n} built {}", root.display());
}
let cpu = || {
let mut r: libc::rusage = unsafe { std::mem::zeroed() };
unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut r) };
let us = |t: libc::timeval| t.tv_sec as u64 * 1_000_000 + t.tv_usec as u64;
(us(r.ru_utime), us(r.ru_stime))
};
for k in 0..walks {
let (u0, s0) = cpu();
let t = Instant::now();
let got = super::scan::scan_with(&root, threads).unwrap();
let ms = t.elapsed().as_millis();
let (u1, s1) = cpu();
assert_eq!((got.entries.len(), got.held.len()), (n, 0));
println!(
"WALK n={n} threads={threads} walk={k} ms={ms} user_ms={} sys_ms={}",
(u1 - u0) / 1000,
(s1 - s0) / 1000
);
}
}
#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn barrier_cost_against_workspace_size() {
let n: usize = std::env::var("FLINT_BENCH_N").ok().and_then(|v| v.parse().ok()).unwrap_or(20_000);
let (store, kind, mem) = bench_store();
let prefix = format!("bench/{}", uuid::Uuid::new_v4());
let (dir_a, dir_b) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
let mut a = bench_syncer(&store, dir_a.path(), &prefix);
let mut b = bench_syncer(&store, dir_b.path(), &prefix);
let mut ops = Ops::new(&store);
a.checkout().await.unwrap();
for i in 0..n {
put(dir_a.path(), i, &format!("v1 of {i}"));
}
let t = Instant::now();
let mut seeded = 0;
for k in 0..6 {
seeded += timed(&mut a, &mut ops, n, kind, &format!("seed{k}")).await.uploaded.len();
if seeded >= n {
break;
}
}
assert_eq!(seeded, n, "the seed never published");
println!("BENCH n={n} seed_publish_ms={}", t.elapsed().as_millis());
let t = Instant::now();
unsafe { libc::sync() };
println!("BENCH n={n} seed_flush_ms={}", t.elapsed().as_millis());
ops.take();
let t = Instant::now();
b.checkout().await.unwrap();
println!("BENCH n={n} checkout_ms={} ops: {}", t.elapsed().as_millis(), ops.take());
let delay: u64 = std::env::var("FLINT_BENCH_DELAY_MS").ok().and_then(|v| v.parse().ok()).unwrap_or(0);
if let Some(m) = mem.as_ref().filter(|_| delay > 0) {
m.inject_request_delay_ms(delay);
}
println!("BENCH n={n} arm={} delay_ms={delay} scan_threads={}", bench_arm(), b.cfg.scan_threads);
let racy = |b: &Syncer| b.state.load_baseline().map(|x| x.entries.values().filter(|e| e.racy).count()).unwrap_or(0);
println!("BENCH n={n} racy_after_checkout={}", racy(&b));
for k in 0..3 {
timed(&mut b, &mut ops, n, kind, &format!("idle{k}")).await;
if k == 0 {
println!("BENCH n={n} racy_after_idle0={}", racy(&b));
if std::env::var("FLINT_BENCH_STOP").as_deref() == Ok("idle0") {
return;
}
}
}
let hot = n / 2;
put(dir_a.path(), hot, "v2 from the peer");
timed(&mut a, &mut ops, n, kind, "peer_publish_one").await;
let t = Instant::now();
let mut barriers = 0;
while got(dir_b.path(), hot).as_deref() != Some("v2 from the peer") {
assert!(barriers < 6, "the peer's change never arrived");
barriers += 1;
timed(&mut b, &mut ops, n, kind, &format!("arrive_barrier{barriers}")).await;
}
println!("BENCH n={n} arrival_total_ms={} barriers={barriers}", t.elapsed().as_millis());
for k in 0..2 {
timed(&mut b, &mut ops, n, kind, &format!("idle_after{k}")).await;
}
put(dir_b.path(), 1, "v2 from B");
timed(&mut b, &mut ops, n, kind, "own_publish_one").await;
for k in 0..3 {
put(dir_b.path(), 2, &format!("busy edit {k} {}", "x".repeat(k + 1)));
timed(&mut b, &mut ops, n, kind, &format!("busy{k}")).await;
}
timed(&mut b, &mut ops, n, kind, "idle_after_busy").await;
b.cfg.untracked_sweep_secs = 1;
b.cfg.collect_every_secs = b.cfg.collect_every_secs.min(1);
tokio::time::sleep(std::time::Duration::from_millis(2100)).await;
put(dir_b.path(), 3, "v2 from B, sweep due");
let r = timed(&mut b, &mut ops, n, kind, "own_publish_one_sweep_due").await;
println!("BENCH n={n} swept={}", r.swept);
let status = std::fs::read_to_string("/proc/self/status").unwrap_or_default();
let kib = |key: &str| {
status.lines().find(|l| l.starts_with(key)).and_then(|l| l.split_whitespace().nth(1)).unwrap_or("?").to_string()
};
println!(
"BENCH n={n} baseline_parses_b={} vm_hwm_kib={} vm_rss_kib={}",
b.state.baseline_parses(),
kib("VmHWM:"),
kib("VmRSS:")
);
}