feat(stats): populate frame_age metric for WebRTC path (partial #20)
Add quantitative capture-to-send latency measurement so we can diagnose remaining latency sources after #24/#25 PTS fixes. Previously frame_age_p95 was always 0.0ms because the WebRTC code path never propagated capture timestamps, even though stats.rs already supported the metric. The infrastructure existed but was disconnected. Changes: - avhw.rs: Add capture_time: Instant field to CpuNv12Frame (set when PipeWire delivers frame) and EncodedH264Frame (propagated through encode thread via new last_capture_time side-channel on SwEncEncode). - state_portal.rs: Change sent_gap channel type from Sender<f64> to Sender<(f64, Option<f64>)> so WebRTC thread can send pre-computed age_ms = capture_time.elapsed() at the exact send moment (not at stats drain time, which would inflate the measurement by ~1s). - stats.rs: record_send_from_thread now accepts Option<f64> age_ms and pushes to frame_age_ms Vec when Some. After this commit: - stats: log lines show real frame_age_p95 / frame_age_max in ms - Expected range: 5-30ms (import + scale + encode + channel send) - If much higher: server pipeline has queueing issue - If low but user still sees latency: confirms bottleneck is network or browser-side (jitter buffer, decode queue) Scope notes: - Only Portal/PipeWire path is instrumented. wlr-screencopy path uses different code path (EncState, not SwEncState) and will continue to report frame_age=0.0ms. Adding wlr instrumentation is separate scope. - This is diagnostic only — does NOT change user-visible behavior. No encoding, sending, or stats output format changes. Tests: - cargo build --release: 0 new warnings (19 baseline preserved) - cargo test: 96 lib + 3 integration, 0 failed - SAFETY comments preserved verbatim - 3 files changed, +39/-10 lines Refs #20.
This commit is contained in:
+14
-7
@@ -38,7 +38,7 @@ struct EncodeThread {
|
||||
|
||||
struct WebrtcThread {
|
||||
handle: Option<std::thread::JoinHandle<()>>,
|
||||
sent_gap_rx: crossbeam_channel::Receiver<f64>,
|
||||
sent_gap_rx: crossbeam_channel::Receiver<(f64, Option<f64>)>,
|
||||
}
|
||||
|
||||
/// 门户模式的主状态机
|
||||
@@ -281,7 +281,8 @@ impl StatePortal {
|
||||
.clone();
|
||||
let fps = self.args.fps;
|
||||
let max_bitrate = self.args.max_bitrate;
|
||||
let (sent_gap_tx, sent_gap_rx) = crossbeam_channel::bounded(64);
|
||||
let (sent_gap_tx, sent_gap_rx) =
|
||||
crossbeam_channel::bounded::<(f64, Option<f64>)>(64);
|
||||
let webrtc_handle = std::thread::Builder::new()
|
||||
.name("wl-webrtc-webrtc".into())
|
||||
.spawn(move || {
|
||||
@@ -349,8 +350,8 @@ impl StatePortal {
|
||||
}
|
||||
}
|
||||
if let Some(ref webrtc_thread) = self.webrtc_thread {
|
||||
while let Ok(gap_ms) = webrtc_thread.sent_gap_rx.try_recv() {
|
||||
self.stats.record_send_from_thread(gap_ms);
|
||||
while let Ok((gap_ms, age_ms)) = webrtc_thread.sent_gap_rx.try_recv() {
|
||||
self.stats.record_send_from_thread(gap_ms, age_ms);
|
||||
}
|
||||
}
|
||||
let snap = self.stats.snapshot_and_reset();
|
||||
@@ -694,7 +695,7 @@ fn webrtc_thread_loop(
|
||||
enc_height: u32,
|
||||
max_bitrate: u64,
|
||||
paused: Arc<AtomicBool>,
|
||||
sent_gap_tx: crossbeam_channel::Sender<f64>,
|
||||
sent_gap_tx: crossbeam_channel::Sender<(f64, Option<f64>)>,
|
||||
bitrate_tx: crossbeam_channel::Sender<BitrateCommand>,
|
||||
resolution_tx: crossbeam_channel::Sender<BitrateCommand>,
|
||||
) {
|
||||
@@ -799,8 +800,12 @@ fn webrtc_thread_loop(
|
||||
let gap_ms = last_send
|
||||
.map(|l| l.elapsed().as_secs_f64() * 1000.0)
|
||||
.unwrap_or(0.0);
|
||||
// Compute capture-to-send age on the sending thread so the
|
||||
// frame_age stat stays accurate when batch-drained later.
|
||||
let age_ms =
|
||||
Some(enc_frame.capture_time.elapsed().as_secs_f64() * 1000.0);
|
||||
last_send = Some(std::time::Instant::now());
|
||||
let _ = sent_gap_tx.try_send(gap_ms);
|
||||
let _ = sent_gap_tx.try_send((gap_ms, age_ms));
|
||||
}
|
||||
} else {
|
||||
while webrtc_rx.try_recv().is_ok() {}
|
||||
@@ -817,8 +822,10 @@ fn webrtc_thread_loop(
|
||||
let gap_ms = last_send
|
||||
.map(|l| l.elapsed().as_secs_f64() * 1000.0)
|
||||
.unwrap_or(0.0);
|
||||
let age_ms =
|
||||
Some(enc_frame.capture_time.elapsed().as_secs_f64() * 1000.0);
|
||||
last_send = Some(std::time::Instant::now());
|
||||
let _ = sent_gap_tx.try_send(gap_ms);
|
||||
let _ = sent_gap_tx.try_send((gap_ms, age_ms));
|
||||
}
|
||||
}
|
||||
Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
|
||||
|
||||
Reference in New Issue
Block a user