Runs cargo fmt --all; all 573 tests still passing, clippy still clean. No logic changes — formatting only. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
243 lines
9.7 KiB
Rust
243 lines
9.7 KiB
Rust
//! WAN pipelining latency simulation benchmark — Track 8.
|
||
//!
|
||
//! Measures throughput of the pipelined push protocol (sliding window W) under
|
||
//! simulated WAN latency. RTT delay is injected into the server's Ack sending
|
||
//! path via `tokio::time::sleep`, keeping the benchmark fully self-contained
|
||
//! (no OS-level traffic shaping required).
|
||
//!
|
||
//! # What it demonstrates
|
||
//!
|
||
//! The stop-and-wait baseline (W=1) sends one packet, waits one RTT for an
|
||
//! Ack, then sends the next. For 100 packets at 50 ms RTT, that is 5 seconds.
|
||
//! A W=16 pipeline keeps 16 packets in-flight simultaneously, reducing total
|
||
//! time to roughly ceil(100/16) × RTT ≈ 7 × 50 ms = 350 ms (15× faster).
|
||
//!
|
||
//! # Groups
|
||
//!
|
||
//! `wan_pipeline/rtt_{R}ms_{label}` — 100-revision push, RTT=R ms, window=W
|
||
//!
|
||
//! Run:
|
||
//! cargo bench -p clawsync-agent -- wan_pipeline_bench
|
||
|
||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||
use std::path::PathBuf;
|
||
use std::sync::Arc;
|
||
use std::time::Duration;
|
||
|
||
use clawhdf5_onion::format::NO_PARENT;
|
||
use clawhdf5_onion::writer::OnionFile;
|
||
use clawsync_onion::differ::diff_revisions;
|
||
use clawsync_onion::manifest::ClawSyncManifest;
|
||
use clawsync_transport::protocol::SyncMessage;
|
||
use clawsync_transport::tcp::{TcpConnection, TcpServer};
|
||
use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main};
|
||
use tempfile::TempDir;
|
||
use tokio::sync::{Semaphore, mpsc};
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
const H5_MAGIC: &[u8] = b"\x89HDF\r\n\x1a\n";
|
||
/// Number of revisions used in every bench iteration.
|
||
const N: usize = 100;
|
||
|
||
fn any_addr() -> SocketAddr {
|
||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)
|
||
}
|
||
|
||
/// Build an OnionFile with `n` committed revisions in `dir`.
|
||
fn make_onion_n(dir: &TempDir, n: usize) -> (PathBuf, OnionFile) {
|
||
let h5 = dir.path().join("src.h5");
|
||
std::fs::write(&h5, H5_MAGIC).unwrap();
|
||
let mut onion = OnionFile::create(&h5, 4096).unwrap();
|
||
for i in 0..n {
|
||
let mut s = onion.begin_session(None).unwrap();
|
||
s.record_page(0, &vec![i as u8; 4096]);
|
||
onion.commit_session(s, Some(&format!("rev {i}"))).unwrap();
|
||
}
|
||
onion.flush().unwrap();
|
||
(h5, onion)
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// Latency-injecting server
|
||
//
|
||
// Receives exactly N LayerPacket messages, sleeping `rtt_ms` before each Ack
|
||
// to simulate the round-trip time of a WAN link. Uses the ManifestRequest
|
||
// protocol (no IBLT) for simplicity.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
async fn serve_with_latency(server: TcpServer, rtt_ms: f64) {
|
||
let (mut conn, _) = server.accept().await.unwrap();
|
||
|
||
// Client sends ManifestRequest; server declares it has 0 revisions so
|
||
// the client will push all N of its own.
|
||
let client_rev_count = match conn.recv().await.unwrap() {
|
||
SyncMessage::ManifestRequest { revision_count, .. } => revision_count,
|
||
other => panic!("server expected ManifestRequest, got {other:?}"),
|
||
};
|
||
|
||
conn.send(&SyncMessage::ManifestResponse {
|
||
manifest: ClawSyncManifest {
|
||
agent_id: "bench-server".to_string(),
|
||
file_blake3: [0u8; 32],
|
||
revision_count: 0,
|
||
head_revision: 0,
|
||
head_blake3: [0u8; 32],
|
||
revisions: vec![],
|
||
last_write: 0.0,
|
||
},
|
||
})
|
||
.await
|
||
.unwrap();
|
||
|
||
let delay = Duration::from_secs_f64(rtt_ms / 1000.0);
|
||
let mut received = 0u64;
|
||
loop {
|
||
match conn.recv().await.unwrap() {
|
||
SyncMessage::LayerPacket { packet } => {
|
||
let rev = packet.revision;
|
||
// Inject simulated WAN RTT before sending Ack.
|
||
tokio::time::sleep(delay).await;
|
||
conn.send(&SyncMessage::Ack { revision: rev })
|
||
.await
|
||
.unwrap();
|
||
received += 1;
|
||
}
|
||
SyncMessage::SyncComplete { .. } => break,
|
||
other => panic!("server: unexpected {other:?}"),
|
||
}
|
||
}
|
||
assert_eq!(received, client_rev_count);
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// Pipelined push client (configurable window)
|
||
//
|
||
// Uses the ManifestRequest protocol and the W=`window` sliding-window pipeline
|
||
// identical to the real cmd_push implementation.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
async fn pipelined_push(addr: SocketAddr, onion: &OnionFile, window: usize) {
|
||
let mut conn = TcpConnection::connect(addr).await.unwrap();
|
||
|
||
let rev_count = onion.revision_count();
|
||
conn.send(&SyncMessage::ManifestRequest {
|
||
agent_id: "bench-client".to_string(),
|
||
head_revision: rev_count.saturating_sub(1),
|
||
revision_count: rev_count,
|
||
})
|
||
.await
|
||
.unwrap();
|
||
|
||
match conn.recv().await.unwrap() {
|
||
SyncMessage::ManifestResponse { .. } => {} // server has 0 revisions — we push all
|
||
other => panic!("client expected ManifestResponse, got {other:?}"),
|
||
}
|
||
|
||
let packets = diff_revisions(onion, NO_PARENT).unwrap();
|
||
let total = packets.len();
|
||
|
||
let (mut read_half, mut write_half) = conn.into_split();
|
||
let semaphore = Arc::new(Semaphore::new(window));
|
||
let (meta_tx, mut meta_rx) = mpsc::unbounded_channel::<u64>(); // bytes per packet
|
||
let sem_writer = semaphore.clone();
|
||
|
||
let writer = tokio::spawn(async move {
|
||
for packet in packets {
|
||
let bytes = packet.page_data_size() as u64;
|
||
sem_writer.acquire().await.unwrap().forget();
|
||
meta_tx.send(bytes).unwrap();
|
||
write_half
|
||
.send(&SyncMessage::LayerPacket { packet })
|
||
.await
|
||
.unwrap();
|
||
}
|
||
write_half // return so we can send SyncComplete
|
||
});
|
||
|
||
let mut sent = 0u64;
|
||
let mut bytes_sent = 0u64;
|
||
while sent < total as u64 {
|
||
match read_half.recv().await.unwrap() {
|
||
SyncMessage::Ack { .. } => {
|
||
semaphore.add_permits(1);
|
||
bytes_sent += meta_rx.recv().await.unwrap();
|
||
sent += 1;
|
||
}
|
||
other => panic!("client: unexpected {other:?}"),
|
||
}
|
||
}
|
||
|
||
let mut write_half = writer.await.unwrap();
|
||
write_half
|
||
.send(&SyncMessage::SyncComplete {
|
||
revisions_transferred: sent,
|
||
bytes_transferred: bytes_sent,
|
||
})
|
||
.await
|
||
.unwrap();
|
||
write_half.shutdown().await.unwrap();
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// Criterion benchmark
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
fn bench_wan_pipeline(c: &mut Criterion) {
|
||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||
.worker_threads(4)
|
||
.enable_all()
|
||
.build()
|
||
.unwrap();
|
||
|
||
let mut group = c.benchmark_group("wan_pipeline");
|
||
// Each iteration sleeps (rtt_ms × N/window) ms, so use fewer samples.
|
||
group.sample_size(10);
|
||
group.throughput(Throughput::Elements(N as u64));
|
||
|
||
// (rtt_ms, window, label)
|
||
// At 1 ms RTT: W=1 ≈ 100 ms/iter, W=16 ≈ 7 ms/iter
|
||
// At 5 ms RTT: W=1 ≈ 500 ms/iter, W=16 ≈ 32 ms/iter
|
||
let params: &[(f64, usize, &str)] = &[
|
||
(1.0, 1, "rtt_1ms_W1"),
|
||
(1.0, 4, "rtt_1ms_W4"),
|
||
(1.0, 16, "rtt_1ms_W16"),
|
||
(5.0, 1, "rtt_5ms_W1"),
|
||
(5.0, 4, "rtt_5ms_W4"),
|
||
(5.0, 16, "rtt_5ms_W16"),
|
||
];
|
||
|
||
for &(rtt_ms, window, label) in params {
|
||
group.bench_function(label, |b| {
|
||
b.iter_batched(
|
||
|| {
|
||
// SETUP (not timed): build source onion and bind server
|
||
let dir = TempDir::new().unwrap();
|
||
let (path, onion) = make_onion_n(&dir, N);
|
||
let (server, addr) = rt.block_on(async {
|
||
let s = TcpServer::bind(any_addr()).await.unwrap();
|
||
let a = s.local_addr;
|
||
(s, a)
|
||
});
|
||
(dir, path, onion, server, addr)
|
||
},
|
||
|(dir, path, onion, server, addr)| {
|
||
// TIMED: full push with simulated WAN latency
|
||
let _ = (dir, path); // keep TempDir alive
|
||
rt.block_on(async {
|
||
let server_task = tokio::spawn(serve_with_latency(server, rtt_ms));
|
||
pipelined_push(addr, &onion, window).await;
|
||
server_task.await.unwrap();
|
||
});
|
||
},
|
||
BatchSize::SmallInput,
|
||
);
|
||
});
|
||
}
|
||
|
||
group.finish();
|
||
}
|
||
|
||
criterion_group!(benches, bench_wan_pipeline);
|
||
criterion_main!(benches);
|