e18e6c8399
Adds `AudioLatencyComparisonTest` to the hang-interop suite — runs the
Amethyst Kotlin speaker AND the upstream `hang-publish` Rust sidecar
into the same local moq-relay, with one Kotlin listener subscribing to
both. Same room, distinct broadcast suffixes (per-publisher pubkey),
same Opus shape (440 Hz mono, 32 kbps, 20 ms frames), 10 s window.
What it measures (per publisher):
- delivered frame count (sanity floor: 80 % of theoretical max)
- inter-arrival p50 / p95 / p99 / max at the listener — the part
the publisher stack actually controls (mic -> encoder ->
publisher.send -> uni-stream open -> QUIC writer -> wire), since
relay + receive path are identical
- time-to-first-frame (informational; Rust pays process-spawn that
JVM doesn't, so not asserted on)
What it does NOT measure: end-to-end glass-to-ear latency (would need
synchronised send-side timestamps from hang-publish, which means
modifying the Rust sidecar) and loss / congestion (no simulator on
the localhost link). For loss behaviour, see `quic/interop/`.
Asserts: each side delivers >= 80 % of expected frames and each
side's median sits in [15, 35] ms — catches a stalled publisher or
a no-pacing burst-mode publisher without depending on per-host tail
percentiles. Tail numbers are printed for human inspection.
Observed on this run:
Kotlin speaker frames=571 ttf=143.8 ms p50=20.21 p95=20.63 p99=21.45 max=25.88
Rust hang-publish frames=495 ttf=326.0 ms p50=19.99 p95=21.41 p99=22.18 max=26.72
Gated by `-DnestsHangInterop=true`; needs the cargo-built
`hang-publish` sidecar already produced by `interopBuildSidecars`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>