network: per-peer connection badge (direct/relay, RTT, loss, bitrate)
Answer "am I actually P2P right now?" per peer. A 1 Hz session task snapshots the selected QUIC path of every live audio connection (IrohTransport::connection_stats), core::connstats::derive turns consecutive snapshots into RTT/loss/bitrate (path switches and counter resets invalidate the rate window), and the peer card shows a Direct/Relay badge with a hover tooltip for address, loss, and up/down bitrate. No new dependencies, no wire change. Loopback-integration-tested against real iroh endpoints; not yet field-verified on a 2-machine call (FEATURES.md row marked 🧪). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -244,6 +244,71 @@ async fn loopback_sequenced_audio_reaches_peer_and_decodes() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Connection transparency: over a real loopback link, `connection_stats()`
|
||||
/// must report the peer's selected path as direct (relay disabled here), with
|
||||
/// an IP remote address and counters that advance while audio flows — and the
|
||||
/// `connstats::derive` seam must turn two such snapshots into badge info with
|
||||
/// live rates.
|
||||
#[tokio::test]
|
||||
async fn connection_stats_report_a_direct_path_with_live_counters() {
|
||||
let a = spawn_node().await;
|
||||
let b = spawn_node().await;
|
||||
|
||||
a.lookup.add_endpoint_info(b.endpoint.addr());
|
||||
b.lookup.add_endpoint_info(a.endpoint.addr());
|
||||
|
||||
let a_id = a.endpoint.id();
|
||||
let b_id = b.endpoint.id();
|
||||
a.transport.admit_audio_sender(b_id);
|
||||
b.transport.admit_audio_sender(a_id);
|
||||
|
||||
// Keep B's receive path subscribed like production (drained implicitly).
|
||||
let _b_rx = b.transport.receive_datagrams().await.expect("subscribe B");
|
||||
|
||||
a.transport.connect_peer(b.endpoint.addr()).await;
|
||||
b.transport.connect_peer(a.endpoint.addr()).await;
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
|
||||
let snap = |stats: Vec<(iroh::EndpointId, peerspeak::network::PathSnapshot)>| {
|
||||
stats
|
||||
.into_iter()
|
||||
.find(|(id, _)| *id == b_id)
|
||||
.map(|(_, s)| s)
|
||||
.expect("peer B should appear in A's connection stats")
|
||||
};
|
||||
let s1 = snap(a.transport.connection_stats());
|
||||
assert!(!s1.is_relay, "loopback with relay disabled must be direct");
|
||||
assert!(
|
||||
s1.remote_addr.parse::<std::net::SocketAddr>().is_ok(),
|
||||
"direct path address should be ip:port, got {}",
|
||||
s1.remote_addr
|
||||
);
|
||||
|
||||
// Stream real audio so the path counters move.
|
||||
let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
|
||||
for seq in 0..25u32 {
|
||||
a.transport.broadcast(packet(&mut enc, seq));
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
|
||||
let s2 = snap(a.transport.connection_stats());
|
||||
assert!(s2.tx_bytes > s1.tx_bytes, "sent bytes should advance");
|
||||
assert!(
|
||||
s2.tx_datagrams > s1.tx_datagrams,
|
||||
"sent datagrams should advance"
|
||||
);
|
||||
|
||||
// The derivation seam turns the two snapshots into live badge info.
|
||||
let info = peerspeak::core::connstats::derive(Some(&s1), &s2, Duration::from_millis(200));
|
||||
assert!(!info.relay);
|
||||
assert_eq!(info.remote_addr, s2.remote_addr);
|
||||
assert!(info.rtt_ms < 1000, "localhost RTT should be sane");
|
||||
assert!(
|
||||
info.up_kbps.expect("same path + positive window has a rate") > 0.0,
|
||||
"audio was flowing, so the upstream rate must be non-zero"
|
||||
);
|
||||
}
|
||||
|
||||
/// Read datagrams off a raw connection until `target` arrive or the deadline
|
||||
/// passes, asserting each carries the 4-byte sequence header.
|
||||
async fn count_audio(conn: &Connection, target: u32, deadline: tokio::time::Instant) -> u32 {
|
||||
|
||||
Reference in New Issue
Block a user