-
-
Notifications
You must be signed in to change notification settings - Fork 213
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
gh-712: Improve peak jitter calculations
- 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
Showing
20 changed files
with
441 additions
and
179 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
/* | ||
* 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 JitterMeterConfig::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 JitterMeterConfig& 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 leaky peak detector. | ||
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 model of a leaky peak detector. | ||
// | ||
// 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). | ||
// | ||
// A leaky peak detector takes immediate peaks and mimicking a leakage process when | ||
// immediate values of jitter are lower than stored one. 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 (exponentially). | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 JitterMeterConfig { | ||
//! 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 resistance 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 resistance 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; | ||
|
||
JitterMeterConfig() | ||
: 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 JitterMeterConfig& 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 JitterMeterConfig 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_ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.