open-standards/joule-code/joule-code-rs/crates/joule-moq-ingest/tests/contrib_bench.rs
Transaction Science 650ed33653 joule-moq-relay: a stream's flow-control window of 16 MiB, configurable
A door holds each object of a group until the group's receipts arrive, and
it holds the object while reading its stream; the receipts follow the
group's last object. A group larger than the stream's receive window (QUIC's
default, about 1.25 MB) therefore stops its stream: the publisher cannot
finish the group, its receipts never come, and the door refuses it after its
hold. Contribution at 20 Mbit/s with a one-second GOP sends groups of 2.5 MB.
limits.quic_stream_window_bytes sets the window, 16 MiB by default (64 Mbit/s
for two seconds). contrib_srt's test of a 4.5 MB group shows it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 02:37:34 -04:00

116 lines
5.9 KiB
Rust

//! The gateway's throughput and energy on a contribution link: a sender in another
//! process (`joule-contrib-send`, named by `WAI_CONTRIB_SEND`) sends a synthetic program at
//! `WAI_BENCH_KBPS` (20 000 by default) for `WAI_BENCH_SECONDS` (20) over SRT, encrypted, into
//! the gateway, which publishes every object with its receipts to a relay whose door checks
//! every receipt; both are this process. It prints one JSON line: the payload rate carried,
//! the objects published, the process's CPU time and its energy by the counter's profile,
//! class and scope (the gateway and the relay; the sender's process is not counted).
//!
//! ```text
//! WAI_CONTRIB_SEND=…/release/joule-contrib-send cargo test --release -p joule-moq-ingest \
//! --test contrib_bench -- --ignored --nocapture
//! ```
mod common;
use common::*;
use joule_contrib::energy;
use joule_moq_ingest::linkrec;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
fn env_u64(k: &str, d: u64) -> u64 {
std::env::var(k).ok().and_then(|v| v.parse().ok()).unwrap_or(d)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 8)]
#[ignore = "a measurement: set WAI_CONTRIB_SEND, run with --release"]
async fn contribution_throughput_and_energy() {
// What the relay refuses at its door, and why, goes to the test's output.
let _ = tracing_subscriber::fmt().with_max_level(tracing::Level::WARN).with_test_writer().try_init();
let send = std::env::var("WAI_CONTRIB_SEND").expect("WAI_CONTRIB_SEND names joule-contrib-send");
let kbps = env_u64("WAI_BENCH_KBPS", 20_000);
let seconds = env_u64("WAI_BENCH_SECONDS", 20);
let w = World::start(|c| {
c.contrib.srt.listen = Some("127.0.0.1:0".parse().unwrap());
c.contrib.srt.latency_ms = 200;
c.contrib.max_bitrate_kbps = 2_000_000;
c.limits.max_frame_bytes = 16 << 20;
c.limits.max_queue_bytes = 512 << 20;
})
.await;
let auth = w.auth(&w.publisher, 0);
let r = auth.send("POST", &format!("{}/srt", w.base()), &[], b"").await;
assert_eq!(r.status, 201, "{}", r.text());
let s: serde_json::Value = serde_json::from_slice(&r.body).unwrap();
let url = format!(
"srt://127.0.0.1:{}?latency=200&streamid={}&passphrase={}&pbkeylen=16",
w.gateway.srt_addr.unwrap().port(),
s["stream_id"].as_str().unwrap().replace('#', "%23").replace(',', "%2C").replace('=', "%3D").replace('/', "%2F"),
s["passphrase"].as_str().unwrap()
);
// On a counter every user of the package shares (linux-rapl), the figure is a bracket:
// the run's reading less an idle reading taken just before it, scaled to the run's length.
let idle = match energy::read() {
Some(r) if r.scope == "bracket" => {
let t = Instant::now();
tokio::time::sleep(Duration::from_secs(seconds)).await;
Some((r.to(&energy::read().unwrap()), t.elapsed().as_secs_f64()))
}
_ => None,
};
let e0 = energy::read();
let c0 = energy::cpu_us();
let t0 = Instant::now();
let mut child = Command::new(&send).args([&url, "--synthetic", &seconds.to_string(), "--kbps", &kbps.to_string(), "--linger", "1000"]).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().expect("the sender runs");
let out = tokio::task::spawn_blocking(move || {
let mut o = String::new();
std::io::Read::read_to_string(child.stdout.as_mut().unwrap(), &mut o).unwrap();
let st = child.wait().unwrap();
(st, o)
})
.await
.unwrap();
assert!(out.0.success(), "the sender finished: {}", out.1);
assert!(until(Duration::from_secs(30), || w.gateway.gateway.link_records().iter().any(|b| linkrec::LinkRecord::read(b).is_ok_and(|r| r.last))).await, "the session ended");
let wall = t0.elapsed().as_secs_f64();
let c1 = energy::cpu_us();
let e1 = energy::read();
let parts = linkrec::verify_series(&w.gateway.gateway.link_records()).expect("the link record verifies");
let bytes: u64 = parts.iter().map(|p| p.account.bytes).sum();
let packets: u64 = parts.iter().map(|p| p.account.delivered).sum();
// The session's record is signed once its tracks have ended at the relay, after its
// last link part.
assert!(until(Duration::from_secs(30), || w.gateway.gateway.records().iter().any(|r| r.record.protocol == "srt")).await, "the session record");
let rec = w.gateway.gateway.records().into_iter().find(|r| r.record.protocol == "srt").unwrap();
let objects: u64 = rec.record.tracks.iter().filter(|t| t.dir == 2).map(|t| t.objects).sum();
let raw = match (&e0, &e1) {
(Some(a), Some(b)) => Some(a.to(b) as f64 / 1e9),
_ => None,
};
let idle_j = idle.map(|(nj, s)| nj as f64 / 1e9 * wall / s);
let joules = match (raw, idle_j) {
(Some(r), Some(i)) => Some(r - i),
(r, _) => r,
};
let gb = bytes as f64 / 1e9;
println!(
"{}",
serde_json::json!({
"bench": "gateway-srt-aes128",
"kbps_target": kbps,
"seconds": seconds,
"wall_s": wall,
"payload_bytes": bytes,
"packets": packets,
"objects_published": objects,
"payload_mbps": bytes as f64 * 8.0 / 1e6 / seconds as f64,
"cpu_s": (c1 - c0) as f64 / 1e6,
"refused_at_door": w.relay.relay().stats().refused_at_door,
"sender": serde_json::from_str::<serde_json::Value>(out.1.trim()).unwrap_or(serde_json::Value::Null),
"energy": e1.map(|e| serde_json::json!({"profile": e.profile, "class": e.class, "scope": e.scope, "joules": joules, "raw_joules": raw, "idle_joules": idle_j, "j_per_gb": joules.map(|j| j / gb.max(1e-9)), "covers": if e.scope == "task" { "the gateway and the relay, one process; not the sender" } else { "the package less an idle bracket: the gateway, the relay and the sender together" }})),
})
);
assert_eq!(w.relay.relay().stats().refused_at_door, 0);
w.stop().await;
}