Skip to content

Commit

Permalink
gh-712: Improve peak jitter calculations
Browse files Browse the repository at this point in the history
- Extract JitterMeter class.
- Remove `min_jitter` metric (it is almost always zero or very
  close to it).
- Replace `max_jitter` with `peak_jitter`, which is similar to
  maximum, but tries to exclude harmless spikes to reduce latency.

See comments in JitterMeter for details on the algorithm.
  • Loading branch information
gavv committed Aug 11, 2024
1 parent b26f9f0 commit 28e74a0
Show file tree
Hide file tree
Showing 20 changed files with 439 additions and 179 deletions.
151 changes: 151 additions & 0 deletions src/internal_modules/roc_audio/jitter_meter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright (c) 2024 Roc Streaming authors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

#include "roc_audio/jitter_meter.h"
#include "roc_core/panic.h"

namespace roc {
namespace audio {

bool JitterConfig::deduce_defaults(audio::LatencyTunerProfile latency_profile) {
if (jitter_window == 0) {
if (latency_profile == audio::LatencyTunerProfile_Responsive) {
jitter_window = 10000;
} else {
jitter_window = 30000;
}
}

if (peak_quantile_window == 0) {
peak_quantile_window = jitter_window / 5;
}

if (envelope_resistance_coeff == 0) {
if (latency_profile == audio::LatencyTunerProfile_Responsive) {
envelope_resistance_coeff = 0.05;
} else {
envelope_resistance_coeff = 0.1;
}
}

return true;
}

JitterMeter::JitterMeter(const JitterConfig& config, core::IArena& arena)
: config_(config)
, jitter_window_(arena, config.jitter_window)
, smooth_jitter_window_(arena, config.envelope_smoothing_window_len)
, envelope_window_(arena, config.peak_quantile_window, config.peak_quantile_coeff)
, peak_window_(arena, config.jitter_window)
, capacitor_charge_(0)
, capacitor_discharge_resistance_(0)
, capacitor_discharge_iteration_(0) {
}

const JitterMetrics& JitterMeter::metrics() const {
return metrics_;
}

void JitterMeter::update_jitter(const core::nanoseconds_t jitter) {
// Moving average of jitter.
jitter_window_.add(jitter);

// Update current value of jitter envelope based on current value of jitter.
// Envelope is computed based on smoothed jitter + a peak detector with capacitor.
smooth_jitter_window_.add(jitter);
const core::nanoseconds_t jitter_envelope =
update_envelope_(smooth_jitter_window_.mov_max(), jitter_window_.mov_avg());

// Quantile of envelope.
envelope_window_.add(jitter_envelope);
// Moving maximum of quantile of envelope.
peak_window_.add(envelope_window_.mov_quantile());

metrics_.mean_jitter = jitter_window_.mov_avg();
metrics_.peak_jitter = peak_window_.mov_max();
metrics_.curr_jitter = jitter;
metrics_.curr_envelope = jitter_envelope;
}

// This function calculates jitter envelope using a peak detector with capacitor.
//
// The quantile of jitter envelope is used as the value for `peak_jitter` metric.
// LatencyTuner selects target latency based on its value. We want find lowest
// possible peak jitter and target latency that are safe (don't cause disruptions).
//
// The function tries to achieve two goals:
//
// - The quantile of envelope (e.g. 90% of values) should be above regular repeating
// spikes, typical for wireless networks, and should ignore occasional exceptions
// if they're not too high and not too frequent.
//
// - The quantile of envelope should be however increased if occasional spike is
// really high, which is often a predictor of increasing network load
// (i.e. if spike is abnormally high, chances are that more high spikes follows).
//
// The role of the capacitor is to amplify the impact of jitter spikes of certain
// kinds. Without it, spikes would be too thin to be reliably detected by quantile.
//
// Typical jitter envelope before applying capacitor:
//
// ------------------------------------- maximum (too high)
// |╲
// || |╲ |╲
// --||----------||--------||----------- quantile (too low)
// __||______|╲__||__|╲____||__|╲____
//
// And after applying capacitor:
//
// |╲_
// --| |_-------|╲_-------|╲----------- quantile (good)
// | ╲ | ╲_ | ╲_
// __| ╲_|╲__| ╲____| ╲____
//
core::nanoseconds_t JitterMeter::update_envelope_(const core::nanoseconds_t cur_jitter,
const core::nanoseconds_t avg_jitter) {
// `capacitor_charge_` represents current envelope value.
// Each step we either instantly re-charge capacitor if we see a peak, or slowly
// discharge it until it reaches zero or we see next peek.

if (capacitor_charge_ < cur_jitter) {
// If current jitter is higher than capacitor charge, instantly re-charge
// capacitor. The charge is set to the jitter value, and the resistance to
// discharging is proportional to the value of the jitter related to average.
//
// Peaks that are significantly higher than average cause very slow discharging,
// and hence have bigger impact on the envelope's quantile.
//
// Peaks that are not so high discharge quicker, but if they are frequent enough,
// capacitor value is constantly re-charged and keeps high. Hence, frequent peeks
// also have bigger impact on the envelope's quantile.
//
// Peaks that are neither high nor frequent have small impact on the quantile.
capacitor_charge_ = cur_jitter;
capacitor_discharge_resistance_ = std::pow((double)cur_jitter / avg_jitter,
config_.envelope_resistance_exponent)
* config_.envelope_resistance_coeff;
capacitor_discharge_iteration_ = 0;
} else if (capacitor_charge_ > 0) {
// No peak detected, continue discharging (exponentionally).
capacitor_charge_ =
core::nanoseconds_t(capacitor_charge_
* std::exp(-capacitor_discharge_iteration_
/ capacitor_discharge_resistance_));
capacitor_discharge_iteration_++;
}

if (capacitor_charge_ < 0) {
// Fully discharged. Normally doesn't happen.
capacitor_charge_ = 0;
}

return capacitor_charge_;
}

} // namespace audio
} // namespace roc
168 changes: 168 additions & 0 deletions src/internal_modules/roc_audio/jitter_meter.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* Copyright (c) 2024 Roc Streaming authors
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

//! @file roc_audio/jitter_meter.h
//! @brief Jitter metrics calculator.

#ifndef ROC_AUDIO_JITTER_METER_H_
#define ROC_AUDIO_JITTER_METER_H_

#include "roc_audio/latency_config.h"
#include "roc_core/iarena.h"
#include "roc_core/noncopyable.h"
#include "roc_core/time.h"
#include "roc_stat/mov_aggregate.h"
#include "roc_stat/mov_quantile.h"

namespace roc {
namespace audio {

//! Jitter meter parameters.
//!
//! Mean jitter is calculated as moving average of last `jitter_window` packets.
//!
//! Peak jitter calculation is performed in several steps:
//!
//! 1. Calculate jitter envelope - a curve that outlines jitter extremes.
//! Envelope calculation is based on a smoothing window
//! (`envelope_smoothing_window_len`) and a peak detector with capacitor
//! (`envelope_resistance_exponent`, `envelope_resistance_coeff`).
//!
//! 2. Calculate moving quantile of the envelope - a line above certain percentage
//! of the envelope values across moving window (`peak_quantile_coeff`,
//! `peak_quantile_window`).
//!
//! 3. Calculate moving maximum of the envelope's quantile across last `jitter_window`
//! samples. This is the resulting peak jitter.
struct JitterConfig {
//! Number of packets for calculating long-term jitter sliding statistics.
//! @remarks
//! Increase this value if you want slower and smoother reaction.
//! Peak jitter is not decreased until jitter envelope is low enough
//! during this window.
//! @note
//! Default value is about a few minutes.
size_t jitter_window;

//! Number of packets in small smoothing window to calculate jitter envelope.
//! @remarks
//! The larger is this value, the rougher is jitter envelope.
//! @note
//! Default value is a few packets.
size_t envelope_smoothing_window_len;

//! Exponent coefficient of capacitor resistance used in jitter envelope.
//! @note
//! Capacitor discharge speed is (peak ^ exp) * coeff, where `peak` is
//! the jitter peak size relative to the average jitter, `exp` is
//! `envelope_resistance_exponent`, and `coeff` is `envelope_resistance_coeff`.
//! @remarks
//! Increase this value to make impact to the peak jitter of high spikes much
//! stronger than impact of low spikes.
double envelope_resistance_exponent;

//! Linear coefficient of capacitor resistance used in jitter envelope.
//! @note
//! Capacitor discharge speed is (peak ^ exp) * coeff, where `peak` is
//! the jitter peak size relative to the average jitter, `exp` is
//! `envelope_resistance_exponent`, and `coeff` is `envelope_resistance_coeff`.
//! @remarks
//! Increase this value to make impact to the peak jitter of frequent spikes
//! stronger than impact of rare spikes.
double envelope_resistance_coeff;

//! Number of packets for calculating envelope quantile.
//! @remarks
//! This window size is used to calculate moving quantile of the envelope.
//! @note
//! This value is the compromise between reaction speed to the increased
//! jitter and ability to distinguish rare spikes from frequent ones.
//! If you increase this value, we can detect and cut out more spikes that
//! are harmless, but we react to the relevant spikes a bit slower.
size_t peak_quantile_window;

//! Coefficient of envelope quantile from 0 to 1.
//! @remarks
//! Defines percentage of the envelope that we want to cut out.
//! @note
//! E.g. value 0.9 means that we want to draw a line that is above 90%
//! of all envelope values across the quantile window.
double peak_quantile_coeff;

JitterConfig()
: jitter_window(0)
, envelope_smoothing_window_len(10)
, envelope_resistance_exponent(6)
, envelope_resistance_coeff(0)
, peak_quantile_window(0)
, peak_quantile_coeff(0.90) {
}

//! Automatically fill missing settings.
ROC_ATTR_NODISCARD bool deduce_defaults(audio::LatencyTunerProfile latency_profile);
};

//! Jitter metrics.
struct JitterMetrics {
//! Moving average of the jitter.
core::nanoseconds_t mean_jitter;

//! Moving peak value of the jitter.
//! @remarks
//! This metric is similar to moving maximum, but excludes short rate spikes
//! that are considered harmless.
core::nanoseconds_t peak_jitter;

//! Last jitter value.
core::nanoseconds_t curr_jitter;

//! Last jitter envelope value.
core::nanoseconds_t curr_envelope;

JitterMetrics()
: mean_jitter(0)
, peak_jitter(0)
, curr_jitter(0)
, curr_envelope(0) {
}
};

//! Jitter metrics calculator.
class JitterMeter : public core::NonCopyable<JitterMeter> {
public:
//! Initialize.
JitterMeter(const JitterConfig& config, core::IArena& arena);

//! Get updated jitter metrics.
const JitterMetrics& metrics() const;

//! Update jitter metrics based on the jitter value for newly received packet.
void update_jitter(core::nanoseconds_t jitter);

private:
core::nanoseconds_t update_envelope_(core::nanoseconds_t cur_jitter,
core::nanoseconds_t avg_jitter);

const JitterConfig config_;

JitterMetrics metrics_;

stat::MovAggregate<core::nanoseconds_t> jitter_window_;
stat::MovAggregate<core::nanoseconds_t> smooth_jitter_window_;
stat::MovQuantile<core::nanoseconds_t> envelope_window_;
stat::MovAggregate<core::nanoseconds_t> peak_window_;

core::nanoseconds_t capacitor_charge_;
double capacitor_discharge_resistance_;
double capacitor_discharge_iteration_;
};

} // namespace audio
} // namespace roc

#endif // ROC_AUDIO_JITTER_METER_H_
13 changes: 6 additions & 7 deletions src/internal_modules/roc_audio/latency_tuner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ bool LatencyTuner::update_stream() {
roc_panic_if(init_status_ != status::StatusOK);

if (enable_latency_adjustment_ && latency_is_adaptive_ && has_metrics_) {
update_target_latency_(link_metrics_.max_jitter, link_metrics_.jitter,
update_target_latency_(link_metrics_.peak_jitter, link_metrics_.mean_jitter,
latency_metrics_.fec_block_duration);
}

Expand Down Expand Up @@ -350,7 +350,7 @@ void LatencyTuner::compute_scaling_(packet::stream_timestamp_diff_t actual_laten
// NB: After the increasement the new latency target value must not be greater than
// upper threshold in any circumstances.
//
void LatencyTuner::update_target_latency_(const core::nanoseconds_t max_jitter_ns,
void LatencyTuner::update_target_latency_(const core::nanoseconds_t peak_jitter_ns,
const core::nanoseconds_t mean_jitter_ns,
const core::nanoseconds_t fec_block_ns) {
// If there is no active timeout, check if evaluated target latency is
Expand All @@ -360,7 +360,7 @@ void LatencyTuner::update_target_latency_(const core::nanoseconds_t max_jitter_n
// jitter statistics. Later we'll use this value only for decision making if it
// worth changing or we rather keep the current latency target untouched.
const core::nanoseconds_t estimate = std::max(
std::max(core::nanoseconds_t(max_jitter_ns * max_jitter_overhead_),
std::max(core::nanoseconds_t(peak_jitter_ns * max_jitter_overhead_),
core::nanoseconds_t(mean_jitter_ns * mean_jitter_overhead_)),
fec_block_ns);

Expand Down Expand Up @@ -527,7 +527,7 @@ void LatencyTuner::periodic_report_() {
"latency tuner:"
" e2e_latency=%ld(%.3fms) niq_latency=%ld(%.3fms) target_latency=%ld(%.3fms)"
" stale=%ld(%.3fms)"
" packets(lost/exp)=%ld/%ld jitter(avg/min/max)=%.3fms/%.3fms/%.3fms"
" packets(lost/exp)=%ld/%ld jitter(mean/peak)=%.3fms/%.3fms"
" fec_blk=%.3fms"
" fe=%.6f eff_fe=%.6f fe_stbl=%s",
// e2e_latency, niq_latency, target_latency
Expand All @@ -540,9 +540,8 @@ void LatencyTuner::periodic_report_() {
// packets
(long)link_metrics_.lost_packets, (long)link_metrics_.expected_packets,
// jitter
(double)link_metrics_.jitter / core::Millisecond,
(double)link_metrics_.min_jitter / core::Millisecond,
(double)link_metrics_.max_jitter / core::Millisecond,
(double)link_metrics_.mean_jitter / core::Millisecond,
(double)link_metrics_.peak_jitter / core::Millisecond,
// fec_blk
(double)latency_metrics_.fec_block_duration / core::Millisecond,
// fe, eff_fe, fe_stbl
Expand Down
2 changes: 1 addition & 1 deletion src/internal_modules/roc_audio/latency_tuner.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class LatencyTuner : public core::NonCopyable<> {
bool check_actual_latency_(packet::stream_timestamp_diff_t latency);
void compute_scaling_(packet::stream_timestamp_diff_t latency);

void update_target_latency_(core::nanoseconds_t max_jitter_ns,
void update_target_latency_(core::nanoseconds_t peak_jitter_ns,
core::nanoseconds_t mean_jitter_ns,
core::nanoseconds_t fec_block_ns);
void try_decrease_latency_(core::nanoseconds_t estimate,
Expand Down
Loading

0 comments on commit 28e74a0

Please sign in to comment.