Skip to content

[Core] JB: fixes and adjustments - #2337

Closed
jchavanton wants to merge 4 commits into
signalwire:masterfrom
jchavanton:ejb
Closed

[Core] JB: fixes and adjustments#2337
jchavanton wants to merge 4 commits into
signalwire:masterfrom
jchavanton:ejb

Conversation

@jchavanton

@jchavanton jchavanton commented Dec 18, 2023

Copy link
Copy Markdown
Contributor
  • [Core] JB: if the jitter buffer size is above the its max size, we force accelerate

  • [Core] JB: proper handling of seq roll-over

  • [Core] JB: fix seq decrement

  • [Core] JB: add buffering skip counter

  • [Core] JB: reset when expanding above the maximum size

  • [Core] JB: disable skip buffering when acceleration is active

These fixes and the adjustment were thoroughly tested, they were found while testing the packet relay metrics. #2292

A/B testing with 100 sessions and no artificial jitter, to make sure everything is working well under normal conditions.

  • less packets are replaced by PLC or simply skipped with the default JB
  • less delays is introduced

EJB : more elastic jitter buffer
image

The packet relay metrics are still not ready for merge but I am able to use them to precisely monitor the jitter buffer in production.


Here is one report of real traffic facing jitter, I selected it randomly from production traffic.
We can use it as a source of validation evidence.
Configuration:

<action application="set" data="rtp_jitter_buffer_accelerate=true"/>
<action application="set" data="jitterbuffer_msec=20:1200"/>

End of call report:

{
  "rp": {
    "call_id_in": "rt342cma1dgelvphc5pb",
    "call_id_out": "c454bbd8-4e26-4049-85bb-20569a0b2990",
    "report": {
      "in": {
        "ssrc": "0x95262C82",
        "remote_socket": "172.11.61.245:18550",
        "local_socket": "172.11.61.245:35930",
        "codec": "opus",
        "count": 43181,   // ~15 minutes call
        "plc": 732   //  1.6% PLC
      },
      "out": {
        "ssrc": "0xA95A90EE",
        "remote_socket": "172.11.50.19:34402",
        "local_socket": "172.11.61.245:13842",
        "codec": "PCMU",
        "count": 43181,
        "max_ms": 1060,   // this is the maximum time spent by any packet inside FS, we can see close to matching the JB max size.
        "avg_ms": 74.57   // this is the average time spent by any packet inside FS
      }
    }
  },
  "jb": {
    "in_call_id": "rt342cma1dgelvphc5pb",
    "out_call_id": "c454bbd8-4e26-4049-85bb-20569a0b2990",
    "jb": {
      "size_max_ms": 1220,   // we can see that the maximum jitter buffer size was respected
      "size_est_ms": 20,
      "acceleration_ms": 13680,
      "expand_ms": 13580,
      "fast_acceleration_ms": 0,
      "forced_acceleration_ms": 60,   // at one point the jitter buffer was above its max size
      "jitter_max_ms": 140,   // very high jitter was seen during that call
      "jitter_est_ms": 8,   // near the end there was not that much jitter
      "reset": 7,
      "reset_too_big": 0
    }
  }
}

@signalwire-ci

signalwire-ci Bot commented Dec 18, 2023

Copy link
Copy Markdown

@signalwire-ci

signalwire-ci Bot commented Dec 18, 2023

Copy link
Copy Markdown

@dragos-oancea

Copy link
Copy Markdown
Contributor
src/switch_jitterbuffer.c:1741:27: error: 'switch_jb_stats_t' {aka 'struct switch_jb_stats_s'} has no member named 'expand_frame_len'
 1741 |       if (jb->jitter.stats.expand_frame_len < 0) jb->jitter.stats.expand_frame_len = 0;
      |                           ^
src/switch_jitterbuffer.c:1741:66: error: 'switch_jb_stats_t' {aka 'struct switch_jb_stats_s'} has no member named 'expand_frame_len'
 1741 |       if (jb->jitter.stats.expand_frame_len < 0) jb->jitter.stats.expand_frame_len = 0;

@jchavanton , you should fix the build.

@jchavanton

Copy link
Copy Markdown
Contributor Author
src/switch_jitterbuffer.c:1741:27: error: 'switch_jb_stats_t' {aka 'struct switch_jb_stats_s'} has no member named 'expand_frame_len'
 1741 |       if (jb->jitter.stats.expand_frame_len < 0) jb->jitter.stats.expand_frame_len = 0;
      |                           ^
src/switch_jitterbuffer.c:1741:66: error: 'switch_jb_stats_t' {aka 'struct switch_jb_stats_s'} has no member named 'expand_frame_len'
 1741 |       if (jb->jitter.stats.expand_frame_len < 0) jb->jitter.stats.expand_frame_len = 0;

@jchavanton , you should fix the build.

Thanks !
Yes will test this exact version soon, I am working on a fork to have more metrics.
But I will make sure to test it properly.
I will convert it to a draft for now to avoid confusion.

@jchavanton
jchavanton marked this pull request as draft December 19, 2023 13:58
@jchavanton

jchavanton commented Dec 21, 2023

Copy link
Copy Markdown
Contributor Author

A/B testing on real traffic.

So far I have been to rigorous on not accepting to reset unless I know exactly why but in some case the remote RTP endpoint may behave unexpectedly and I added a reset to deal with exceptional cases. I am expecting that the reset will take good care of them
image

Lets hope I will have the visit of santa 🎅 I think I deserve it 🙂

Edit : 3 hours
image
image

@jchavanton

jchavanton commented Dec 29, 2023

Copy link
Copy Markdown
Contributor Author

running A/B testing to make sure I am not missing anything
image

@jchavanton

jchavanton commented Jan 9, 2024

Copy link
Copy Markdown
Contributor Author

After several days of A/B testing of the elastic jitter buffer
Measurements have confirmed that this will :

  • Reduce the delay introduced by the jitter buffer by half (on average 30ms instead of 70ms), while still reducing the amount of packets dropped or missing significantly.

One of the reason why we faced difficulties (that were identified during the ramp-up A/B testing) was that the default jitter buffer is resetting under various conditions some of which are a bit obscure (expecting the unexpected), however even if these resets are damaging the flow of packets they tend to restore the conditions to normal.

Adjustments were made to fine tune the reset on the elastic jitter buffer as well.

image

@jchavanton

Copy link
Copy Markdown
Contributor Author

image

@jchavanton
jchavanton force-pushed the ejb branch 2 times, most recently from 4de890e to e0775c2 Compare January 11, 2024 15:14
@signalwire-ci

signalwire-ci Bot commented Jan 11, 2024

Copy link
Copy Markdown

@signalwire-ci

signalwire-ci Bot commented Jan 11, 2024

Copy link
Copy Markdown

@signalwire-ci

signalwire-ci Bot commented Jan 11, 2024

Copy link
Copy Markdown

@signalwire-ci

signalwire-ci Bot commented Jan 11, 2024

Copy link
Copy Markdown

@jchavanton

jchavanton commented Jan 11, 2024

Copy link
Copy Markdown
Contributor Author

@dragos-oancea I made a rebase and commit clean up, looks like it is time to review.

My take away on the testing is that this is statistically significant, enough to convince my that the elastic jitter buffer is indeed reducing the impact of degradation caused by jitter without introducing new problems.
As we may have expected, but due to all the little thing that can go wrong, I wanted to make sure.

I think, we can already observe the sample distribution is not that random because A and B are always not that far from each other.

Anyway I can still dig on poor performing calls and investigate the buffer behavior that was seen, to corner case even more unexpected conditions.

But it seems we are in a very good spot !
image

@jchavanton
jchavanton marked this pull request as ready for review January 17, 2024 21:41
@jchavanton

Copy link
Copy Markdown
Contributor Author

@andywolk Hi Andrey, will you or someone else be able to help with the review.

Should I update FS documentation to reflect the new feature and make sure users are aware about it ?

@jchavanton

jchavanton commented Feb 13, 2024

Copy link
Copy Markdown
Contributor Author

image

Today I am completing the A/B testing, it lasted 30 days and 10M minutes of very long calls (real users) were compared, we have built enough confidence in the value of this improvement.

Considering that the calls have less latency, less missing packets and higher average call duration.
@andywolk hopefully, we can review, document and merge.

@jchavanton

Copy link
Copy Markdown
Contributor Author

@andywolk I can not make it to cluecon this year, but maybe we could look at merging that MR ?
What do you think is missing.

dev-ryanc pushed a commit to team-telnyx/freeswitch that referenced this pull request Mar 13, 2026
…alwire#2337

Cherry-picked fixes from signalwire#2337 (Julien Chavanton):

1. Sequence rollover bug in check_jb_size() - add MAX_DROPOUT guard
   to distinguish old packets from seq 65535->0 wraparound
2. decrement_seq() ordering - set last_target_seq after decrement
3. Buffering skip gate - do not block elastic mode with the
   complete_frames < frame_len check
4. Consecutive miss safety valve - after MAX_CONSECUTIVE_MISS (100)
   consecutive missed frames, disable elastic mode and reset JB
dev-ryanc pushed a commit to team-telnyx/freeswitch that referenced this pull request Mar 13, 2026
Track net buffer expansion via expand_frame_len (incremented on PLC
expand, decremented on acceleration). Reset the jitter buffer when
expansion exceeds max_frame_len to prevent unbounded growth during
sustained packet loss.

Cherry-picked from signalwire#2337 (Julien Chavanton).
@jchavanton jchavanton changed the title [Core] JB: fixes and adjusments [Core] JB: fixes and adjustments Mar 18, 2026
@jchavanton
jchavanton force-pushed the ejb branch 2 times, most recently from d700712 to 0388398 Compare June 9, 2026 17:23
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.
dev-ryanc pushed a commit to team-telnyx/freeswitch that referenced this pull request Jul 1, 2026
…alwire#2337

Cherry-picked fixes from signalwire#2337 (Julien Chavanton):

1. Sequence rollover bug in check_jb_size() - add MAX_DROPOUT guard
   to distinguish old packets from seq 65535->0 wraparound
2. decrement_seq() ordering - set last_target_seq after decrement
3. Buffering skip gate - do not block elastic mode with the
   complete_frames < frame_len check
4. Consecutive miss safety valve - after MAX_CONSECUTIVE_MISS (100)
   consecutive missed frames, disable elastic mode and reset JB
dev-ryanc pushed a commit to team-telnyx/freeswitch that referenced this pull request Jul 1, 2026
Track net buffer expansion via expand_frame_len (incremented on PLC
expand, decremented on acceleration). Reset the jitter buffer when
expansion exceeds max_frame_len to prevent unbounded growth during
sustained packet loss.

Cherry-picked from signalwire#2337 (Julien Chavanton).
dev-ryanc added a commit to team-telnyx/freeswitch that referenced this pull request Jul 1, 2026
…e PR signalwire#2337

Consolidate inline channel variable exports from the jitter buffer hot
path into switch_jb_export_stats(), called at hangup before hangup
hooks run (via switch_core_media_export_jb_stats) and as a safety net
before every switch_jb_destroy() in both switch_rtp_destroy() and the
KILL_JB path in read_rtp_packet().

This prevents a race condition where hangup hooks read stale/missing
JB stats because the jitter buffer was already destroyed or the inline
exports hadn't run recently enough.

Adapted from upstream commit 0388398 — excluded
fast_acceleration, forced_acceleration, and packets_in_buffer fields
which don't exist in our switch_jb struct.

Cherry-picked from signalwire#2337 (Julien Chavanton).
dev-ryanc added a commit to team-telnyx/freeswitch that referenced this pull request Jul 1, 2026
…signalwire#2337

1. Add switch_rtp_ready() guard in the JB poll loop to prevent
   polling after the session is being destroyed.

2. Add rtp_jitter_buffer_accelerate channel variable check to the
   tries > 20 condition, allowing JB reset to be skipped when
   acceleration is explicitly enabled.
dev-ryanc added a commit to team-telnyx/freeswitch that referenced this pull request Jul 1, 2026


Add elastic algorithm activity tracking and enhanced statistics
export from upstream commit af83338:

- New stats fields: grew, shrunk, grow_to_max, at_min_ms, at_max_ms,
  edge_since, reset_setup, reset_runtime
- New jb fields: media_started, packets_total
- Track frame_len edge transitions in jb_frame_inc_line()
- Split reset count into setup vs runtime in switch_jb_reset()
- Flush at-edge timer on set_frames()
- Initialize edge_since on JB creation
- Export all new metrics in switch_jb_export_stats() with a
  comprehensive summary log line
- Set media_started and increment packets_total on each put_packet

Cherry-picked from signalwire#2337 (Julien Chavanton).
tajamulTelnyx pushed a commit to team-telnyx/freeswitch that referenced this pull request Jul 16, 2026
…SignalWire PR signalwire#2337 (#539)

* TEL-6872: Cherry-pick jitter buffer bug fixes from SignalWire PR signalwire#2337

Cherry-picked fixes from signalwire#2337 (Julien Chavanton):

1. Sequence rollover bug in check_jb_size() - add MAX_DROPOUT guard
   to distinguish old packets from seq 65535->0 wraparound
2. decrement_seq() ordering - set last_target_seq after decrement
3. Buffering skip gate - do not block elastic mode with the
   complete_frames < frame_len check
4. Consecutive miss safety valve - after MAX_CONSECUTIVE_MISS (100)
   consecutive missed frames, disable elastic mode and reset JB

* TEL-6872: Add unbounded expand protection for elastic jitter buffer

Track net buffer expansion via expand_frame_len (incremented on PLC
expand, decremented on acceleration). Reset the jitter buffer when
expansion exceeds max_frame_len to prevent unbounded growth during
sustained packet loss.

Cherry-picked from signalwire#2337 (Julien Chavanton).

* TEL-6872: Cherry-pick JB statistics race condition fix from SignalWire PR signalwire#2337

Consolidate inline channel variable exports from the jitter buffer hot
path into switch_jb_export_stats(), called at hangup before hangup
hooks run (via switch_core_media_export_jb_stats) and as a safety net
before every switch_jb_destroy() in both switch_rtp_destroy() and the
KILL_JB path in read_rtp_packet().

This prevents a race condition where hangup hooks read stale/missing
JB stats because the jitter buffer was already destroyed or the inline
exports hadn't run recently enough.

Adapted from upstream commit 0388398 — excluded
fast_acceleration, forced_acceleration, and packets_in_buffer fields
which don't exist in our switch_jb struct.

Cherry-picked from signalwire#2337 (Julien Chavanton).

* TEL-6872: Fix JB review findings - frame_len reset, missing log, expand boundary

[1] Reset jb->frame_len to jb->min_frame_len when disabling elastic mode
    after consecutive miss safety valve triggers. Without this, the JB
    stays at expanded size after elastic mode is disabled.

[2] Add SWITCH_LOG_ALERT debug log when elastic mode is disabled after
    MAX_CONSECUTIVE_MISS, matching upstream.

[5] Add expand_frame_len >= max_frame_len boundary check (third else-if
    branch) to catch the exact boundary case and reset via reset_error.

* TEL-6872: Add missing switch_rtp.c functional changes from upstream PR signalwire#2337

1. Add switch_rtp_ready() guard in the JB poll loop to prevent
   polling after the session is being destroyed.

2. Add rtp_jitter_buffer_accelerate channel variable check to the
   tries > 20 condition, allowing JB reset to be skipped when
   acceleration is explicitly enabled.

* TEL-6872: Cherry-pick JB more metrics from SignalWire PR signalwire#2337

Add elastic algorithm activity tracking and enhanced statistics
export from upstream commit af83338:

- New stats fields: grew, shrunk, grow_to_max, at_min_ms, at_max_ms,
  edge_since, reset_setup, reset_runtime
- New jb fields: media_started, packets_total
- Track frame_len edge transitions in jb_frame_inc_line()
- Split reset count into setup vs runtime in switch_jb_reset()
- Flush at-edge timer on set_frames()
- Initialize edge_since on JB creation
- Export all new metrics in switch_jb_export_stats() with a
  comprehensive summary log line
- Set media_started and increment packets_total on each put_packet

Cherry-picked from signalwire#2337 (Julien Chavanton).

* TELCORE-248: address review findings on elastic JB cherry-pick

Fix double-decrement in decrement_seq() — last_target_seq was
over-decremented by one, shifting PLC sequence numbers an extra
packet behind.

Fix lock-order inversion in switch_jb_export_stats() — snapshot
JB fields under mutex, unlock before channel writes to avoid
holding JB mutex while acquiring channel-side locks from
hangup/destroy/KILL_JB contexts.

Cache rtp_jitter_buffer_accelerate channel var at function entry
instead of per-iteration in the retry loop, and restore an
unconditional hard retry bound (tries > 100) so the read loop
cannot spin indefinitely in bad-jitter conditions.

---------

Co-authored-by: Ryan Colobong <ryanc@76xl6d3.office.telnyx.com>
jchavanton and others added 2 commits August 9, 2026 09:50
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>
@jchavanton jchavanton changed the title [Core] JB: fixes and adjustments [Core] EJB overhaul + damage-model plumbing (packet stats, opus PLC, RTCP loss export) Aug 9, 2026
@jchavanton jchavanton changed the title [Core] EJB overhaul + damage-model plumbing (packet stats, opus PLC, RTCP loss export) [Core] EJB: revised elastic jitter buffer — reduces per-call max delay ~2× in canary A/B Aug 9, 2026
@jchavanton jchavanton changed the title [Core] EJB: revised elastic jitter buffer — reduces per-call max delay ~2× in canary A/B [Core] JB: fixes and adjustments Aug 9, 2026
@jchavanton

Copy link
Copy Markdown
Contributor Author

Superseded by #3118 — reorganized into 4 logical commits, revised description, canary-validated A/B.

@jchavanton jchavanton closed this Aug 9, 2026
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.

2 participants