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:
Siavash Sameni
2026-03-28 10:26:40 +04:00
parent 524d1145bb
commit 59a00d371b
7 changed files with 776 additions and 7 deletions

View File

@@ -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);