use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Instant;
use futures::{SinkExt, StreamExt};
use tokio_tungstenite::tungstenite::Message;
use nodedb_lite::engine::crdt::CrdtEngine;
use nodedb_types::sync::wire::*;
use nodedb_types::wire_version::WIRE_FORMAT_VERSION;
const ORIGIN_WS: &str = "ws://127.0.0.1:9090";
const NUM_CLIENTS: u32 = 100;
#[tokio::main]
async fn main() {
println!("=== Load Test: {NUM_CLIENTS} concurrent sync clients ===\n");
if tokio_tungstenite::connect_async(ORIGIN_WS).await.is_err() {
println!("SKIP: Origin not available at {ORIGIN_WS}");
return;
}
let connected = Arc::new(AtomicU32::new(0));
let handshook = Arc::new(AtomicU32::new(0));
let deltas_sent = Arc::new(AtomicU32::new(0));
let deltas_acked = Arc::new(AtomicU32::new(0));
let deltas_rejected = Arc::new(AtomicU32::new(0));
let errors = Arc::new(AtomicU32::new(0));
let start = Instant::now();
let mut handles = Vec::with_capacity(NUM_CLIENTS as usize);
for i in 0..NUM_CLIENTS {
let connected = Arc::clone(&connected);
let handshook = Arc::clone(&handshook);
let deltas_sent = Arc::clone(&deltas_sent);
let deltas_acked = Arc::clone(&deltas_acked);
let deltas_rejected = Arc::clone(&deltas_rejected);
let errors = Arc::clone(&errors);
handles.push(tokio::spawn(async move {
run_client(
i,
connected,
handshook,
deltas_sent,
deltas_acked,
deltas_rejected,
errors,
)
.await;
}));
}
for h in handles {
let _ = h.await;
}
let elapsed = start.elapsed();
println!("connected: {}", connected.load(Ordering::Relaxed));
println!("handshook: {}", handshook.load(Ordering::Relaxed));
println!("deltas sent: {}", deltas_sent.load(Ordering::Relaxed));
println!("deltas acked: {}", deltas_acked.load(Ordering::Relaxed));
println!(
"deltas rejected:{}",
deltas_rejected.load(Ordering::Relaxed)
);
println!("errors: {}", errors.load(Ordering::Relaxed));
println!("total time: {:.2}s", elapsed.as_secs_f64());
println!(
"throughput: {:.0} handshakes/sec",
handshook.load(Ordering::Relaxed) as f64 / elapsed.as_secs_f64()
);
let h = handshook.load(Ordering::Relaxed);
if h >= NUM_CLIENTS {
println!("\nresult: PASS — all {NUM_CLIENTS} clients connected and synced");
} else {
println!("\nresult: PARTIAL — {h}/{NUM_CLIENTS} connected");
std::process::exit(1);
}
}
async fn run_client(
id: u32,
connected: Arc<AtomicU32>,
handshook: Arc<AtomicU32>,
deltas_sent: Arc<AtomicU32>,
deltas_acked: Arc<AtomicU32>,
deltas_rejected: Arc<AtomicU32>,
errors: Arc<AtomicU32>,
) {
let (mut ws, _) = match tokio_tungstenite::connect_async(ORIGIN_WS).await {
Ok(ws) => ws,
Err(_) => {
errors.fetch_add(1, Ordering::Relaxed);
return;
}
};
connected.fetch_add(1, Ordering::Relaxed);
let hs = HandshakeMsg {
jwt_token: String::new(),
vector_clock: std::collections::HashMap::new(),
subscribed_shapes: Vec::new(),
client_version: format!("load-test-{id}"),
lite_id: String::new(),
epoch: 0,
wire_version: WIRE_FORMAT_VERSION,
};
if ws
.send(Message::Binary(
SyncFrame::try_encode(SyncMessageType::Handshake, &hs)
.expect("frame encode")
.to_bytes()
.into(),
))
.await
.is_err()
{
errors.fetch_add(1, Ordering::Relaxed);
return;
}
let resp = match tokio::time::timeout(std::time::Duration::from_secs(10), ws.next()).await {
Ok(Some(Ok(msg))) => msg,
_ => {
errors.fetch_add(1, Ordering::Relaxed);
return;
}
};
if let Some(frame) = SyncFrame::from_bytes(resp.into_data().as_ref())
&& let Some(ack) = frame.decode_body::<HandshakeAckMsg>()
{
if ack.success {
handshook.fetch_add(1, Ordering::Relaxed);
} else {
errors.fetch_add(1, Ordering::Relaxed);
return;
}
}
let mut engine = match CrdtEngine::new(1000 + id as u64) {
Ok(e) => e,
Err(_) => {
errors.fetch_add(1, Ordering::Relaxed);
return;
}
};
let _ = engine.upsert(
"load_test",
&format!("doc-{id}"),
&[("client_id", loro::LoroValue::I64(id as i64))],
);
let deltas = engine.pending_deltas();
if let Some(delta) = deltas.first() {
let msg = DeltaPushMsg {
collection: "load_test".into(),
document_id: format!("doc-{id}"),
delta: delta.delta_bytes.clone(),
peer_id: 1000 + id as u64,
mutation_id: 1,
checksum: 0,
device_valid_time_ms: None,
producer_id: 0,
epoch: 0,
seq: 0,
device_id: 0,
delta_signature: [0u8; 32],
};
if ws
.send(Message::Binary(
SyncFrame::try_encode(SyncMessageType::DeltaPush, &msg)
.expect("frame encode")
.to_bytes()
.into(),
))
.await
.is_ok()
{
deltas_sent.fetch_add(1, Ordering::Relaxed);
if let Ok(Some(Ok(resp))) =
tokio::time::timeout(std::time::Duration::from_secs(10), ws.next()).await
&& let Some(frame) = SyncFrame::from_bytes(resp.into_data().as_ref())
{
match frame.msg_type {
SyncMessageType::DeltaAck => {
deltas_acked.fetch_add(1, Ordering::Relaxed);
}
SyncMessageType::DeltaReject => {
deltas_rejected.fetch_add(1, Ordering::Relaxed);
}
_ => {}
}
}
}
}
let _ = ws.close(None).await;
}