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:
+21
-2
@@ -39,6 +39,12 @@ pub struct PipelineStats {
|
||||
sent_frames: u64,
|
||||
pipewire_dropped: 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 ---
|
||||
capture_queue_depth: usize,
|
||||
@@ -75,6 +81,8 @@ impl PipelineStats {
|
||||
sent_frames: 0,
|
||||
pipewire_dropped: 0,
|
||||
over_budget_count: 0,
|
||||
duplicate_frames_skipped: 0,
|
||||
prev_duplicate_frames_skipped: 0,
|
||||
capture_queue_depth: 0,
|
||||
encoded_queue_depth: 0,
|
||||
capture_gaps_ms: Vec::new(),
|
||||
@@ -186,6 +194,13 @@ impl PipelineStats {
|
||||
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.
|
||||
pub fn set_queue_depths(&mut self, capture: usize, encoded: usize) {
|
||||
self.capture_queue_depth = capture;
|
||||
@@ -216,6 +231,7 @@ impl PipelineStats {
|
||||
sent_frames: self.sent_frames,
|
||||
pipewire_dropped: self.pipewire_dropped,
|
||||
over_budget_count: self.over_budget_count,
|
||||
duplicate_frames_skipped: self.duplicate_frames_skipped,
|
||||
capture_queue_depth: self.capture_queue_depth,
|
||||
encoded_queue_depth: self.encoded_queue_depth,
|
||||
capture_gap_avg_ms: avg_f64(&self.capture_gaps_ms),
|
||||
@@ -254,6 +270,7 @@ impl PipelineStats {
|
||||
self.sent_frames = 0;
|
||||
self.pipewire_dropped = 0;
|
||||
self.over_budget_count = 0;
|
||||
self.duplicate_frames_skipped = 0;
|
||||
self.capture_queue_depth = 0;
|
||||
self.encoded_queue_depth = 0;
|
||||
self.capture_gaps_ms.clear();
|
||||
@@ -288,6 +305,7 @@ pub struct StatsSnapshot {
|
||||
pub sent_frames: u64,
|
||||
pub pipewire_dropped: u64,
|
||||
pub over_budget_count: u64,
|
||||
pub duplicate_frames_skipped: u64,
|
||||
// Queue depths
|
||||
pub capture_queue_depth: usize,
|
||||
pub encoded_queue_depth: usize,
|
||||
@@ -330,8 +348,8 @@ impl std::fmt::Display for StatsSnapshot {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"capture_fps={:.1} encoded_fps={:.1} sent_fps={:.1} \
|
||||
pw_dropped={} over_budget={} \
|
||||
"capture_fps={:.1} encoded_fps={:.1} sent_fps={:.1} \
|
||||
pw_dropped={} over_budget={} duplicate_frames_skipped={} \
|
||||
cap_q={} enc_q={} \
|
||||
cap_gap_p95={:.1}ms cap_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.pipewire_dropped,
|
||||
self.over_budget_count,
|
||||
self.duplicate_frames_skipped,
|
||||
self.capture_queue_depth,
|
||||
self.encoded_queue_depth,
|
||||
self.capture_gap_p95_ms,
|
||||
|
||||
Reference in New Issue
Block a user