Skip to content

[Core] EJB: revised elastic jitter buffer — reduces per-call max delay ~2× in canary A/B - #3118

Open
jchavanton wants to merge 4 commits into
signalwire:masterfrom
jchavanton:ejb-v2
Open

[Core] EJB: revised elastic jitter buffer — reduces per-call max delay ~2× in canary A/B#3118
jchavanton wants to merge 4 commits into
signalwire:masterfrom
jchavanton:ejb-v2

Conversation

@jchavanton

Copy link
Copy Markdown
Contributor

Summary

Rework of the elastic jitter buffer and the surrounding instrumentation
needed to distinguish JB-induced audio loss from carrier-side loss.
Restructured from prior JB-only history into 4 logical commits:

  1. [Core] EJB: add per-leg RTP packet statistics
    packet_stats_t on the session plus counters wired through the RTP
    read path, bridge, and state machine. Adds the in_rx counter
    (packets received pre-JB) that the damage model joins against
    bridge/JB writes.

  2. [Codec] Opus: per-call PLC/FEC stats and run-length classification
    Codec-truth PLC/FEC counters exported as
    opus_decoder_frames/plc/fec. Gap-tolerant run-length
    classification splits PLC into plc_short (≤60ms, inaudible) and
    plc_long (>60ms, audible comfort noise).

  3. [Core] EJB: elastic jitter buffer overhaul — the core rework:

    • Two-tier EWMA+EWMV jitter estimator (fast ~320ms + slow ~10s),
      exported as jitter_mean_ms, sigma_ms, and _slow_ms variants.
    • Clock-drift tracker with sliding-min baseline (survives
      switch_jb_reset), logged as ppm.
    • Categorized reset accounting: reset_too_big, reset_too_expanded,
      reset_missing_frames, plus buffering_skip and expand_ms.
    • Expand floor and "keep elastic on miss" to avoid collapsing the
      EJB on brief gaps.
    • switch_jb_is_elastic() and switch_jb_export_stats() for
      hangup-time per-leg export.
  4. [Core] Media: export inbound sequence-loss counters
    rtp_audio_in_cum_lost and rtp_audio_in_expected_pkt channel
    variables from the RTCP receiver-report path, so cum_lost / expected_pkt gives the true inbound loss rate independent of JB
    behavior. Populated only when rtcp-audio-interval-msec is set.

Damage model

Together these expose enough state to separate three sources of audio
loss on a call:

  • Carrier-side lossrtp_audio_in_cum_lost / expected_pkt
  • JB-side silence lossmax(0, in_rx − jb_out − plc)
  • Concealment — split into inaudible (plc_short) and audible
    (plc_long) via opus run-length classification

Testing

Validated in canary via A/B testing (EJB on vs off), using the counters
introduced in commits 1–4 to compare per-leg audio quality on matched
call volumes. Key findings:

  • Non-elastic JB drifts under load. Per-packet delay is more likely
    to accumulate over the call, resulting in roughly 2× the per-call max
    delay compared with EJB (bucket-level p95: EJB-off ~1640ms vs EJB-on
    ~520ms).
  • PLC counts alone under-count audible damage. The honest metric
    is silence + plc_long, where silence = max(0, in_rx − jb_out − plc) captures JB-side loss and plc_long (>60ms runs) is the
    audible portion of concealment. plc_short is inaudible spectral
    interpolation and should not be counted as damage.
  • RTCP-derived cum_lost / expected_pkt cleanly separates
    carrier-side loss (upstream of FS) from JB-side loss, so
    regressions can be attributed correctly.
  • EJB reset baseline: ~5 setup + ~2 runtime resets per call are
    normal when the categorized reset_* counters are all zero; the
    categorized counters isolate pathological cases (reset_too_big,
    reset_too_expanded, reset_missing_frames).

jchavanton and others added 4 commits June 30, 2026 09:34
Introduce packet_stats_t (in_count, in_plc, in_rx, count) and
packet_stats_io_info_t (codec/ssrc/addr/callid per direction) on the
session. Wire counter increments from the RTP path, the bridge, and
the state machine; expose a per-leg JB stats export hook for v2
hangup metrics.

Exposes the in_rx counter (packets received from peer, pre-JB) that
the damage model joins against bridge/JB writes to compute silence
loss and concealment rates.

Touches: switch_core.h, switch_core_pvt.h, switch_core_media.h,
switch_frame.h (received_ts), switch_rtp.h, switch_core_media.c,
switch_core_session.c, switch_core_state_machine.c, switch_ivr_bridge.c,
switch_rtp.c.
Track FEC, PLC, and frame counters on the opus decoder. Export them
as channel variables (opus_decoder_frames/plc/fec) at decoder
teardown so v2 hangup metrics can record the codec-truth concealment
count, independent of any bridge-side SFF_PLC accounting.

Add gap-tolerant run-length classification: PLC frames in runs ≤3
(≤60ms) are inaudible spectral interpolation; runs >3 degenerate
into audible comfort noise. Split exported as plc_short / plc_long /
plc_long_starts. A run is gap-tolerant within 200ms so glitchy
PLC-real-PLC patterns still count as one audible event.
Comprehensive rework of the elastic jitter buffer covering tuning,
metrics, and a new arrival-time jitter estimator:

- Two-tier EWMA+EWMV jitter estimator: fast (~320ms) and slow (~10s)
  tiers exported as jitter_mean_ms/sigma_ms (+ _slow_ms variants).
  Slow tier and clock-drift state survive switch_jb_reset.
- Clock-drift detection from RTP timestamp sampling, logged as ppm.
- Categorized reset accounting: reset_too_big, reset_too_expanded
  (with dropped-packet count), reset_missing_frames, plus
  buffering_skip and expand_ms.
- Expand floor and "keep elastic on miss" so brief gaps don't
  collapse the EJB to non-elastic mode.
- Export switch_jb_is_elastic() and switch_jb_export_stats() so
  hangup metrics can pull JB state per leg.
- Headline counters surfaced via switch_core_media_export_jb_stats
  feed the v2 damage model (silence loss vs concealment).

Includes earlier statistics fixes, duplicate overflow-block removal,
small tuning adjustments, and follow-up EJB auto-disable fix.
…ed_pkt)

Expose the RTCP receiver-report loss counters as channel variables so
hangup metrics and applications can measure true inbound packet loss
(carrier→FS) independently of JB behavior:

  rtp_audio_in_cum_lost      - cumulative packets never received
  rtp_audio_in_expected_pkt  - packets expected (high_ext_seq - base + 1)

Ratio cum_lost / expected_pkt is the honest inbound loss rate. Prior
JB damage models could only observe silence and concealment; adding
these lets us separate carrier-side loss from JB-side loss and stop
misattributing the two.

Populated only when rtcp-audio-interval-msec is set on the sip profile
(without RTCP the sequence-tracking path in rtcp_stats early-returns).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant