#![cfg(feature = "net")]
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use net::adapter::net::{
arm_send_drain_histo, send_batch_stats, send_drain_histo_snapshot, send_drain_max,
EntityKeypair, MeshNode, MeshNodeConfig, SocketBufferConfig, StreamConfig,
};
const TEST_BUFFER_SIZE: usize = 4 * 1024 * 1024;
const PSK: [u8; 32] = [0x42u8; 32];
fn test_config() -> MeshNodeConfig {
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let mut cfg = MeshNodeConfig::new(addr, PSK)
.with_heartbeat_interval(Duration::from_millis(200))
.with_session_timeout(Duration::from_secs(10))
.with_handshake(3, Duration::from_secs(2));
cfg.socket_buffers = SocketBufferConfig {
send_buffer_size: TEST_BUFFER_SIZE,
recv_buffer_size: TEST_BUFFER_SIZE,
};
cfg
}
async fn build_node() -> Arc<MeshNode> {
let cfg = test_config();
let keypair = EntityKeypair::generate();
Arc::new(MeshNode::new(keypair, cfg).await.expect("MeshNode::new"))
}
async fn handshake(a: &Arc<MeshNode>, b: &Arc<MeshNode>) {
let a_id = a.node_id();
let b_id = b.node_id();
let b_pub = *b.public_key();
let b_addr = b.local_addr();
let b_clone = b.clone();
let accept = tokio::spawn(async move { b_clone.accept(a_id).await });
a.connect(b_addr, &b_pub, b_id).await.expect("connect");
accept.await.expect("accept task").expect("accept");
a.start();
b.start();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn measure_scheduled_stream_drain_depth() {
arm_send_drain_histo();
let node_a = build_node().await;
let node_b = build_node().await;
handshake(&node_a, &node_b).await;
let b_id = node_b.node_id();
let scheduler = node_a.router().scheduler();
let queued_before = scheduler.total_queued();
let stream = node_a
.open_stream(
b_id,
0x4000_0000_0000_00FF,
StreamConfig::new()
.with_scheduled(true)
.with_window_bytes(0)
.with_fairness_weight(1),
)
.expect("open scheduled stream");
const BURST: usize = 800; const ROUNDS: usize = 40;
let event = Bytes::from(vec![0xABu8; 1024]);
let events: Vec<Bytes> = std::iter::repeat_n(event, BURST).collect();
let mut sent_ok = 0usize;
let mut backpressure = 0usize;
for _ in 0..ROUNDS {
match node_a.send_on_stream(&stream, &events).await {
Ok(()) => sent_ok += 1,
Err(_) => backpressure += 1,
}
tokio::time::sleep(Duration::from_millis(8)).await;
}
tokio::time::sleep(Duration::from_millis(300)).await;
let queued_after = scheduler.total_queued();
let histo_single = send_drain_histo_snapshot();
print_histo(
"single saturated scheduled stream (1 producer)",
&histo_single,
&[0u64; 9],
&format!(
"bursts sent_ok={sent_ok} backpressure={backpressure} \
enqueued {queued_before} -> {queued_after}"
),
);
const STREAMS: usize = 16;
const ROUNDS_MULTI: usize = 8;
let node = node_a.clone();
let mut handles = Vec::new();
for s in 0..STREAMS {
let node = node.clone();
let events = events.clone();
handles.push(tokio::spawn(async move {
let stream = node
.open_stream(
b_id,
0x4000_0000_0001_0000 | s as u64,
StreamConfig::new()
.with_scheduled(true)
.with_window_bytes(0)
.with_fairness_weight(1),
)
.expect("open scheduled stream");
for _ in 0..ROUNDS_MULTI {
let _ = node.send_on_stream(&stream, &events).await;
}
}));
}
for h in handles {
let _ = h.await;
}
tokio::time::sleep(Duration::from_millis(300)).await;
let histo_multi_total = send_drain_histo_snapshot();
let (flushes, batched_packets) = send_batch_stats();
let avg_batch = if flushes > 0 {
batched_packets as f64 / flushes as f64
} else {
0.0
};
print_histo(
&format!("{STREAMS} concurrent scheduled streams ({STREAMS} producers)"),
&histo_multi_total,
&histo_single, &format!(
"enqueued (cumulative) {queued_after} -> {} | longest single drain run = {} packets",
scheduler.total_queued(),
send_drain_max(),
),
);
eprintln!(
" batched-drain path: {flushes} flushes (= sendmmsg syscalls on Linux) for \
{batched_packets} packets => avg {avg_batch:.1} packets/syscall \
({:.0}x fewer send syscalls)\n",
avg_batch.max(1.0),
);
let total_runs: u64 = histo_single.iter().sum();
assert!(
queued_after > queued_before,
"scheduled stream must route through the scheduler (enqueue counter \
{queued_before} -> {queued_after}) — else this measurement is vacuous",
);
assert!(
total_runs > 0,
"send loop recorded no drain runs while armed — instrument not wired?",
);
}
fn print_histo(title: &str, now: &[u64; 9], base: &[u64; 9], header: &str) {
let labels = [
"1", "2-3", "4-7", "8-15", "16-31", "32-63", "64-127", "128-255", "256+",
];
let mut total_runs = 0u64;
let mut weighted = 0u64;
eprintln!("\n=== drain run-length: {title} ===");
eprintln!(" {header}");
for (i, label) in labels.iter().enumerate() {
let count = now[i].saturating_sub(base[i]);
total_runs += count;
weighted += count * (1u64 << i);
let bar = "#".repeat(count.min(60) as usize);
eprintln!(" run-len {label:>8} : {count:>8} {bar}");
}
eprintln!(" total runs: {total_runs} lower-bound packets (sum count*2^bucket): {weighted}");
eprintln!("==============================================\n");
}