e490564316
Root-caused live on a phone at 100 Mbps (stream stuck seconds behind, then oscillating): a stack of transport defects, each amplifying the next. - MTU-safe shards: shard_payload 1452 overshot the IPv4/1500 budget (the old math forgot the 40 B header + 24 B crypto ride inside the UDP payload and counted IP+UDP as 8 B) — the kernel silently split EVERY video datagram into two IP fragments, doubling per-datagram loss on Wi-Fi. New config::mtu1500_shard_payload() = 1408 (1472 sealed = the exact ceiling), negotiated in the Welcome, pinned by a unit test. - Android batched I/O: recv/send batching was cfg(linux); Android is target_os="android" and silently fell back to a syscall per datagram. The libc crate binds neither recvmmsg/sendmmsg nor mmsghdr for Android, so a local bionic extern binding provides them (API 21+, floor is 28); cbindgen excludes them from the C header. The pump/runtime threads also get the Apple-QoS analogue on Android: nice −8 (below the decode thread's −10). - Latency-bounded receive: packets are consumed strictly in order at exactly the arrival rate, so a standing queue (Wi-Fi stall, power-save clumping) NEVER drains — observed as a stream permanently 6-7 s behind with both 32 MB socket buffers full. The pump now flushes the entire backlog (Session::flush_backlog: discard ring + kernel queue at memcpy speed, reset the reassembler) and requests a keyframe when frames keep completing > 400 ms behind the skew-corrected capture clock (30 consecutive, 2 s cooldown, logged). - Time-based loss window: the reassembler declared an incomplete frame lost a fixed 4 INDICES behind the newest — 33 ms at 120 fps, inside normal Wi-Fi retry/reorder timescales, so merely-late frames were pruned every few seconds, each costing a recovery-IDR burst + an inflated loss report. Now 120 ms of capture time (LOSS_WINDOW_NS), same fuse at every refresh rate, with a 64-index hard cap bounding memory against hostile pts. - Adaptive-FEC hysteresis: the controller was memoryless — one clean 750 ms report dropped FEC from 8 % straight back to the 1 % floor, so periodic burst loss (Wi-Fi scan / BT coexistence beats) always hit an unprotected stream and ping-ponged 1↔8 % with a frozen frame per cycle (observed in the host log as alternating loss_ppm=0/50000). Attack stays instant; decay is now one point per clean report. Verified: full core suite (incl. new flush + time-window tests) on macOS + Linux, host release build, arm64 cargo-ndk build, and a 30 s wired probe run at 2800x1260@120 — 3559/3559 frames, zero loss, capture→received p50 5.3 ms (host 5.1 + network 0.3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
108 lines
4.3 KiB
Rust
108 lines
4.3 KiB
Rust
//! Tier-1 microbenchmarks for the punktfunk/1 hot path — GPU-free, so they run in normal CI.
|
|
//!
|
|
//! Two layers:
|
|
//! - `crypto/*` — the isolated AES-128-GCM primitives on one ~MTU shard.
|
|
//! - `pipeline/*`— a whole frame through the real per-frame path end to end over the in-process
|
|
//! loopback transport: FEC encode → AES-GCM seal → packetize → (loopback) → reassemble →
|
|
//! FEC decode → open. This is what a throughput/latency regression in the core would show up in.
|
|
//!
|
|
//! The GPU capture/NVENC encode path is deliberately out of scope here (no GPU in CI) — that's the
|
|
//! Tier-3 stream benchmark on a self-hosted GPU runner. Run locally with `cargo bench -p punktfunk-core`.
|
|
|
|
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
|
use punktfunk_core::config::{Config, FecConfig, FecScheme, ProtocolPhase, Role};
|
|
use punktfunk_core::crypto::SessionCrypto;
|
|
use punktfunk_core::session::Session;
|
|
use punktfunk_core::transport::loopback_pair;
|
|
|
|
const TAG_LEN: usize = 16; // AES-GCM authentication tag
|
|
const SHARD: usize = punktfunk_core::config::mtu1500_shard_payload(); // one MTU-safe data shard
|
|
|
|
fn cfg(role: Role, scheme: FecScheme) -> Config {
|
|
Config {
|
|
role,
|
|
phase: match scheme {
|
|
FecScheme::Gf8 => ProtocolPhase::P1GameStream,
|
|
FecScheme::Gf16 => ProtocolPhase::P2Punktfunk,
|
|
},
|
|
fec: FecConfig {
|
|
scheme,
|
|
fec_percent: 25,
|
|
// GF(2^8) is capped at ≤255 shards/block (Moonlight-compatible); GF(2^16) Leopard goes
|
|
// far higher. Use a realistic, valid block size for each.
|
|
max_data_per_block: match scheme {
|
|
FecScheme::Gf8 => 128,
|
|
FecScheme::Gf16 => 4096,
|
|
},
|
|
},
|
|
shard_payload: SHARD,
|
|
max_frame_bytes: 8 * 1024 * 1024,
|
|
encrypt: true, // bench the real path — crypto is always on for punktfunk/1
|
|
key: [7u8; 16],
|
|
salt: [1, 2, 3, 4],
|
|
loopback_drop_period: 0, // throughput run: no induced loss (loss-harness covers recovery)
|
|
}
|
|
}
|
|
|
|
fn bench_crypto(c: &mut Criterion) {
|
|
let host = SessionCrypto::new(&[7u8; 16], [1, 2, 3, 4], Role::Host);
|
|
let client = SessionCrypto::new(&[7u8; 16], [1, 2, 3, 4], Role::Client);
|
|
let payload = vec![0xABu8; SHARD];
|
|
let sealed = host.seal(0, &payload).unwrap();
|
|
|
|
let mut g = c.benchmark_group("crypto");
|
|
g.throughput(Throughput::Bytes(SHARD as u64));
|
|
g.bench_function("seal", |b| {
|
|
let mut seq = 0u64;
|
|
b.iter(|| {
|
|
let ct = host.seal(seq, black_box(&payload)).unwrap();
|
|
seq += 1;
|
|
black_box(ct)
|
|
})
|
|
});
|
|
g.bench_function("seal_in_place", |b| {
|
|
let mut seq = 0u64;
|
|
let mut buf = vec![0xABu8; SHARD + TAG_LEN];
|
|
b.iter(|| {
|
|
host.seal_in_place(seq, black_box(&mut buf)).unwrap();
|
|
seq += 1;
|
|
})
|
|
});
|
|
g.bench_function("open", |b| {
|
|
b.iter(|| black_box(client.open(0, black_box(&sealed)).unwrap()))
|
|
});
|
|
g.finish();
|
|
}
|
|
|
|
fn bench_pipeline(c: &mut Criterion) {
|
|
let mut g = c.benchmark_group("pipeline");
|
|
// 64 KB ≈ a steady-state P-frame; 1 MB ≈ a keyframe/scene-cut. Both FEC schemes (GF(2^8)
|
|
// GameStream-compat vs GF(2^16) Leopard, the wall-breaker).
|
|
for scheme in [FecScheme::Gf8, FecScheme::Gf16] {
|
|
let label = match scheme {
|
|
FecScheme::Gf8 => "gf8",
|
|
FecScheme::Gf16 => "gf16",
|
|
};
|
|
for &size in &[64 * 1024usize, 1024 * 1024] {
|
|
g.throughput(Throughput::Bytes(size as u64));
|
|
g.bench_with_input(BenchmarkId::new(label, size), &size, |b, &size| {
|
|
let (h, cl) = loopback_pair(0, 0);
|
|
let mut host = Session::new(cfg(Role::Host, scheme), Box::new(h)).unwrap();
|
|
let mut client = Session::new(cfg(Role::Client, scheme), Box::new(cl)).unwrap();
|
|
let frame = vec![0x5Au8; size];
|
|
let mut seq = 0u64;
|
|
b.iter(|| {
|
|
host.submit_frame(black_box(&frame), seq, 0).unwrap();
|
|
let f = client.poll_frame().unwrap();
|
|
seq += 1;
|
|
black_box(f)
|
|
})
|
|
});
|
|
}
|
|
}
|
|
g.finish();
|
|
}
|
|
|
|
criterion_group!(benches, bench_crypto, bench_pipeline);
|
|
criterion_main!(benches);
|