feat: jitter buffer instrumentation — drift test, telemetry, parameter sweep
WZP-P2-T1-S1: Automated drift measurement - New drift_test.rs: DriftTestConfig, DriftResult, run_drift_test() - CLI --drift-test <secs>: sends tone, measures actual vs expected duration - Interpretation tiers: EXCELLENT (<50ms) / GOOD / FAIR / POOR - 2 unit tests: drift math verification, config defaults WZP-P2-T1-S2: Jitter buffer telemetry - JitterStats gains: total_decoded, underruns, overruns, max_depth_seen - JitterBuffer: record_underrun(), record_decode(), reset_stats() - CallDecoder: stats() getter, reset_stats() - JitterTelemetry: periodic tracing::info! logger with configurable interval - 4 unit tests: ingestion tracking, underrun tracking, reset, interval WZP-P2-T1-S3: Parameter sweep - New sweep.rs: SweepConfig, SweepResult, run_local_sweep() - Tests 20 jitter buffer configs (5 target × 4 max depths) locally - CLI --sweep: runs sweep, prints ASCII comparison table - No network needed — pure encoder→decoder pipeline test - 3 unit tests: config defaults, local sweep runs, table formatting 216 tests passing across all crates. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,14 @@ pub struct JitterStats {
|
||||
pub packets_late: u64,
|
||||
pub packets_duplicate: u64,
|
||||
pub current_depth: usize,
|
||||
/// Total frames decoded by the consumer (tracked externally via `record_decode`).
|
||||
pub total_decoded: u64,
|
||||
/// Number of times the consumer tried to decode but the buffer was empty/not-ready.
|
||||
pub underruns: u64,
|
||||
/// Number of packets dropped because the buffer exceeded max depth.
|
||||
pub overruns: u64,
|
||||
/// High water mark — maximum buffer depth observed.
|
||||
pub max_depth_seen: usize,
|
||||
}
|
||||
|
||||
/// Result of attempting to get the next packet for playout.
|
||||
@@ -105,6 +113,7 @@ impl JitterBuffer {
|
||||
while self.buffer.len() > self.max_depth {
|
||||
if let Some((&oldest_seq, _)) = self.buffer.first_key_value() {
|
||||
self.buffer.remove(&oldest_seq);
|
||||
self.stats.overruns += 1;
|
||||
// Advance playout seq past evicted packet
|
||||
if seq_before(self.next_playout_seq, oldest_seq.wrapping_add(1)) {
|
||||
self.next_playout_seq = oldest_seq.wrapping_add(1);
|
||||
@@ -114,6 +123,9 @@ impl JitterBuffer {
|
||||
}
|
||||
|
||||
self.stats.current_depth = self.buffer.len();
|
||||
if self.stats.current_depth > self.stats.max_depth_seen {
|
||||
self.stats.max_depth_seen = self.stats.current_depth;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the next packet for playout.
|
||||
@@ -163,6 +175,24 @@ impl JitterBuffer {
|
||||
self.stats = JitterStats::default();
|
||||
}
|
||||
|
||||
/// Record that the consumer attempted to decode but the buffer was empty/not-ready.
|
||||
pub fn record_underrun(&mut self) {
|
||||
self.stats.underruns += 1;
|
||||
}
|
||||
|
||||
/// Record a successful frame decode by the consumer.
|
||||
pub fn record_decode(&mut self) {
|
||||
self.stats.total_decoded += 1;
|
||||
}
|
||||
|
||||
/// Reset statistics counters (preserves buffer contents and playout state).
|
||||
pub fn reset_stats(&mut self) {
|
||||
self.stats = JitterStats {
|
||||
current_depth: self.buffer.len(),
|
||||
..JitterStats::default()
|
||||
};
|
||||
}
|
||||
|
||||
/// Adjust target depth based on observed jitter.
|
||||
pub fn set_target_depth(&mut self, depth: usize) {
|
||||
self.target_depth = depth.min(self.max_depth);
|
||||
|
||||
Reference in New Issue
Block a user