Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve peak jitter calculations #776

Merged
merged 1 commit into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions src/internal_modules/roc_audio/jitter_meter.cpp
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
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 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_
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
Loading