feat(stats): expose duplicate_frames_skipped counter (closes #20)
Final piece of #20. The EncodeOutcome::SkippedDuplicate variant was introduced in #19 but its count was invisible — silent Ok(_) arm in encode_thread_loop. Now exposed as a stat. Changes: - stats.rs: PipelineStats gains duplicate_frames_skipped (window delta) and prev_duplicate_frames_skipped (running total). Snapshot field added. Display format places it after over_budget (both are counters). Reset clears window delta but preserves running total (same pattern as pipewire_dropped). - state_portal.rs: EncodeThread struct gains duplicate_count: Arc<AtomicU64>. Cloned for encode_thread_loop, stored for main-thread reads. encode_thread_loop now explicitly matches SkippedDuplicate and increments with Ordering::Relaxed. Stats snapshot code reads atomic and calls set_duplicate_frames_skipped after timing drain. What this enables: Diagnosing encoded_fps health. Examples: - capture_fps=60 encoded_fps=30 duplicate_frames_skipped=30 → healthy: encoder at 30fps target, 30 frames were true duplicates - capture_fps=60 encoded_fps=5 duplicate_frames_skipped=0 → problem: frames not being dedup'd but encoder can't keep up - capture_fps=1.7 encoded_fps=1.7 duplicate_frames_skipped=0 → healthy static: low fps because KWin damage-driven delivery Original #20 issues status: - 'encoded_fps stuck at ~30': FIXED via #19 (EncodeOutcome enum), now tracks capture_fps when below 30 - 'filler masking real fps': FIXED via #15/#18 (filler deleted) - 'duplicate count invisible': FIXED via this commit - 'unique_encoded_fps / delivered_fps': not implemented, deemed unnecessary now that the core metrics are trustworthy Tests: - cargo build --release: 0 new warnings (19 baseline preserved) - cargo test: 96 lib + 3 integration, 0 failed - SAFETY comments preserved verbatim - 2 files changed, +45/-6 lines
This commit is contained in:
+24
-4
@@ -34,6 +34,7 @@ struct EncodeThread {
|
|||||||
handle: Option<std::thread::JoinHandle<()>>,
|
handle: Option<std::thread::JoinHandle<()>>,
|
||||||
input_tx: crossbeam_channel::Sender<CpuNv12Frame>,
|
input_tx: crossbeam_channel::Sender<CpuNv12Frame>,
|
||||||
timing_rx: crossbeam_channel::Receiver<EncodeThreadTiming>,
|
timing_rx: crossbeam_channel::Receiver<EncodeThreadTiming>,
|
||||||
|
duplicate_count: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WebrtcThread {
|
struct WebrtcThread {
|
||||||
@@ -244,14 +245,26 @@ impl StatePortal {
|
|||||||
bitrate_rx,
|
bitrate_rx,
|
||||||
encoder_resolution_rx,
|
encoder_resolution_rx,
|
||||||
)?;
|
)?;
|
||||||
|
let duplicate_count = std::sync::Arc::new(
|
||||||
|
std::sync::atomic::AtomicU64::new(0),
|
||||||
|
);
|
||||||
|
let duplicate_count_for_thread = duplicate_count.clone();
|
||||||
let handle = std::thread::Builder::new()
|
let handle = std::thread::Builder::new()
|
||||||
.name("wl-webrtc-encode".into())
|
.name("wl-webrtc-encode".into())
|
||||||
.spawn(move || encode_thread_loop(encode, input_rx, timing_tx))?;
|
.spawn(move || {
|
||||||
|
encode_thread_loop(
|
||||||
|
encode,
|
||||||
|
input_rx,
|
||||||
|
timing_tx,
|
||||||
|
duplicate_count_for_thread,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
self.enc_import = Some(import);
|
self.enc_import = Some(import);
|
||||||
self.enc_thread = Some(EncodeThread {
|
self.enc_thread = Some(EncodeThread {
|
||||||
handle: Some(handle),
|
handle: Some(handle),
|
||||||
input_tx,
|
input_tx,
|
||||||
timing_rx,
|
timing_rx,
|
||||||
|
duplicate_count,
|
||||||
});
|
});
|
||||||
|
|
||||||
let wrtc = self.webrtc.take().ok_or_else(|| {
|
let wrtc = self.webrtc.take().ok_or_else(|| {
|
||||||
@@ -331,6 +344,11 @@ impl StatePortal {
|
|||||||
timing.output_bytes,
|
timing.output_bytes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Read duplicate counter (delta computed in setter)
|
||||||
|
let total = enc_thread
|
||||||
|
.duplicate_count
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
self.stats.set_duplicate_frames_skipped(total);
|
||||||
}
|
}
|
||||||
if let Some(ref webrtc_thread) = self.webrtc_thread {
|
if let Some(ref webrtc_thread) = self.webrtc_thread {
|
||||||
while let Ok((gap_ms, age_ms)) = webrtc_thread.sent_gap_rx.try_recv() {
|
while let Ok((gap_ms, age_ms)) = webrtc_thread.sent_gap_rx.try_recv() {
|
||||||
@@ -634,6 +652,7 @@ fn encode_thread_loop(
|
|||||||
mut encode: SwEncEncode,
|
mut encode: SwEncEncode,
|
||||||
input_rx: crossbeam_channel::Receiver<CpuNv12Frame>,
|
input_rx: crossbeam_channel::Receiver<CpuNv12Frame>,
|
||||||
timing_tx: crossbeam_channel::Sender<EncodeThreadTiming>,
|
timing_tx: crossbeam_channel::Sender<EncodeThreadTiming>,
|
||||||
|
duplicate_count: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||||
) {
|
) {
|
||||||
loop {
|
loop {
|
||||||
match input_rx.recv() {
|
match input_rx.recv() {
|
||||||
@@ -647,10 +666,11 @@ fn encode_thread_loop(
|
|||||||
output_bytes: t.output_bytes,
|
output_bytes: t.output_bytes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Ok(EncodeOutcome::SkippedDuplicate) => {
|
||||||
|
duplicate_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// SkippedPaused / SkippedDisconnected / SkippedDuplicate
|
// SkippedPaused / SkippedDisconnected — no counter needed
|
||||||
// Do not report timing; do not tick encoded_fps.
|
|
||||||
// take_timing() intentionally NOT called — last_timing stays default.
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Encode thread error: {e}");
|
tracing::error!("Encode thread error: {e}");
|
||||||
|
|||||||
+20
-1
@@ -39,6 +39,12 @@ pub struct PipelineStats {
|
|||||||
sent_frames: u64,
|
sent_frames: u64,
|
||||||
pipewire_dropped: u64,
|
pipewire_dropped: u64,
|
||||||
over_budget_count: u64,
|
over_budget_count: u64,
|
||||||
|
/// Count of frames dropped by encode thread due to Y-plane hash dedup
|
||||||
|
/// (EncodeOutcome::SkippedDuplicate). Read from atomic counter set by
|
||||||
|
/// encode thread, computed as delta since previous snapshot.
|
||||||
|
duplicate_frames_skipped: u64,
|
||||||
|
// Running total from the encode-thread atomic; NOT reset between windows.
|
||||||
|
prev_duplicate_frames_skipped: u64,
|
||||||
|
|
||||||
// --- queue depth at last observation ---
|
// --- queue depth at last observation ---
|
||||||
capture_queue_depth: usize,
|
capture_queue_depth: usize,
|
||||||
@@ -75,6 +81,8 @@ impl PipelineStats {
|
|||||||
sent_frames: 0,
|
sent_frames: 0,
|
||||||
pipewire_dropped: 0,
|
pipewire_dropped: 0,
|
||||||
over_budget_count: 0,
|
over_budget_count: 0,
|
||||||
|
duplicate_frames_skipped: 0,
|
||||||
|
prev_duplicate_frames_skipped: 0,
|
||||||
capture_queue_depth: 0,
|
capture_queue_depth: 0,
|
||||||
encoded_queue_depth: 0,
|
encoded_queue_depth: 0,
|
||||||
capture_gaps_ms: Vec::new(),
|
capture_gaps_ms: Vec::new(),
|
||||||
@@ -186,6 +194,13 @@ impl PipelineStats {
|
|||||||
self.pipewire_dropped = total_dropped.saturating_sub(prev_dropped);
|
self.pipewire_dropped = total_dropped.saturating_sub(prev_dropped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update duplicate frames skipped counter (absolute value from atomic).
|
||||||
|
/// Computes delta from previous value, like set_pipewire_dropped.
|
||||||
|
pub fn set_duplicate_frames_skipped(&mut self, total_skipped: u64) {
|
||||||
|
self.duplicate_frames_skipped = total_skipped.saturating_sub(self.prev_duplicate_frames_skipped);
|
||||||
|
self.prev_duplicate_frames_skipped = total_skipped;
|
||||||
|
}
|
||||||
|
|
||||||
/// Update queue depth observations.
|
/// Update queue depth observations.
|
||||||
pub fn set_queue_depths(&mut self, capture: usize, encoded: usize) {
|
pub fn set_queue_depths(&mut self, capture: usize, encoded: usize) {
|
||||||
self.capture_queue_depth = capture;
|
self.capture_queue_depth = capture;
|
||||||
@@ -216,6 +231,7 @@ impl PipelineStats {
|
|||||||
sent_frames: self.sent_frames,
|
sent_frames: self.sent_frames,
|
||||||
pipewire_dropped: self.pipewire_dropped,
|
pipewire_dropped: self.pipewire_dropped,
|
||||||
over_budget_count: self.over_budget_count,
|
over_budget_count: self.over_budget_count,
|
||||||
|
duplicate_frames_skipped: self.duplicate_frames_skipped,
|
||||||
capture_queue_depth: self.capture_queue_depth,
|
capture_queue_depth: self.capture_queue_depth,
|
||||||
encoded_queue_depth: self.encoded_queue_depth,
|
encoded_queue_depth: self.encoded_queue_depth,
|
||||||
capture_gap_avg_ms: avg_f64(&self.capture_gaps_ms),
|
capture_gap_avg_ms: avg_f64(&self.capture_gaps_ms),
|
||||||
@@ -254,6 +270,7 @@ impl PipelineStats {
|
|||||||
self.sent_frames = 0;
|
self.sent_frames = 0;
|
||||||
self.pipewire_dropped = 0;
|
self.pipewire_dropped = 0;
|
||||||
self.over_budget_count = 0;
|
self.over_budget_count = 0;
|
||||||
|
self.duplicate_frames_skipped = 0;
|
||||||
self.capture_queue_depth = 0;
|
self.capture_queue_depth = 0;
|
||||||
self.encoded_queue_depth = 0;
|
self.encoded_queue_depth = 0;
|
||||||
self.capture_gaps_ms.clear();
|
self.capture_gaps_ms.clear();
|
||||||
@@ -288,6 +305,7 @@ pub struct StatsSnapshot {
|
|||||||
pub sent_frames: u64,
|
pub sent_frames: u64,
|
||||||
pub pipewire_dropped: u64,
|
pub pipewire_dropped: u64,
|
||||||
pub over_budget_count: u64,
|
pub over_budget_count: u64,
|
||||||
|
pub duplicate_frames_skipped: u64,
|
||||||
// Queue depths
|
// Queue depths
|
||||||
pub capture_queue_depth: usize,
|
pub capture_queue_depth: usize,
|
||||||
pub encoded_queue_depth: usize,
|
pub encoded_queue_depth: usize,
|
||||||
@@ -331,7 +349,7 @@ impl std::fmt::Display for StatsSnapshot {
|
|||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"capture_fps={:.1} encoded_fps={:.1} sent_fps={:.1} \
|
"capture_fps={:.1} encoded_fps={:.1} sent_fps={:.1} \
|
||||||
pw_dropped={} over_budget={} \
|
pw_dropped={} over_budget={} duplicate_frames_skipped={} \
|
||||||
cap_q={} enc_q={} \
|
cap_q={} enc_q={} \
|
||||||
cap_gap_p95={:.1}ms cap_gap_max={:.1}ms \
|
cap_gap_p95={:.1}ms cap_gap_max={:.1}ms \
|
||||||
enc_gap_p95={:.1}ms enc_gap_max={:.1}ms \
|
enc_gap_p95={:.1}ms enc_gap_max={:.1}ms \
|
||||||
@@ -346,6 +364,7 @@ impl std::fmt::Display for StatsSnapshot {
|
|||||||
self.sent_fps,
|
self.sent_fps,
|
||||||
self.pipewire_dropped,
|
self.pipewire_dropped,
|
||||||
self.over_budget_count,
|
self.over_budget_count,
|
||||||
|
self.duplicate_frames_skipped,
|
||||||
self.capture_queue_depth,
|
self.capture_queue_depth,
|
||||||
self.encoded_queue_depth,
|
self.encoded_queue_depth,
|
||||||
self.capture_gap_p95_ms,
|
self.capture_gap_p95_ms,
|
||||||
|
|||||||
Reference in New Issue
Block a user