Skip to content

Report locally measured loss and jitter per ingress SSRC without RTCP - #2201

Open
darwvin-dev wants to merge 1 commit into
sipwise:masterfrom
darwvin-dev:fix/ng-ssrc-loss-without-rtcp-934
Open

darwvin-dev wants to merge 1 commit into
sipwise:masterfrom
darwvin-dev:fix/ng-ssrc-loss-without-rtcp-934

Conversation

@darwvin-dev

Copy link
Copy Markdown
Contributor

Closes #934

Problem

#934 asks for per-leg QoS stats (loss, interarrival jitter) for the RTP that
rtpengine receives, even when the endpoints send no RTCP.

When the issue was filed, rtpengine did not track sequence numbers or timing in
passthrough. That has changed since:

  • userspace computes sequence-gap loss and RFC 3550 jitter per ingress SSRC in
    the passthrough handlers (codec_calc_lost() / codec_calc_jitter()) and in
    the transcoding sequencer, and
  • the kernel module computes the same values into the shared-memory
    struct ssrc_stats (total_lost, jitter) when it measures RTP stats.

Three gaps remained:

  1. The NG query/delete output hid the loss value without RTCP. It only
    printed cumulative loss for an SSRC that also had MOS stats blocks, which
    come from RTCP RRs. Jitter measured from RTP was never printed.
  2. Kernel-measured loss/jitter never reached userspace. Since
    af96fc7 ("track SSRC stats in kernel via shm") the kernel's per-SSRC
    total_lost and jitter are written to shm, but nothing read them. Before
    that commit they were merged into ssrc_entry_call. So for kernel-forwarded
    streams, MQTT lost, NG and self-generated RTCP RRs only covered packets
    seen in userspace.
  3. Two small bugs made those values wrong:
    • The shm ssrc_stats.lost_bits started at 0, while the userspace tracker
      starts at -1. Userspace already advances ext_seq, so on kernel takeover
      the kernel counted up to 32 received packets as lost (in the per-SSRC
      counter and in the interface/global packets_lost stats).
    • With measure-rtp, ssrc_collect_metrics() converted the running jitter
      estimator to ms in place on every call timer run. That corrupted the
      estimator, which is kept in RTP timestamp units and also used for generated
      RTCP RRs.

Changes

  • ssrc_packets_lost() / ssrc_jitter() helpers combine the userspace and
    kernel measurements. Loss is the sum: each side counts the packets it
    handled. Jitter is the kernel's estimate if it has one, otherwise the
    userspace one. They are used for NG output, MQTT lost, and RTCP RR
    generation/substitution.
  • NG: every ingress SSRC entry (inline lists and the top-level SSRC dict)
    now always contains cumulative loss and jitter (ms, omitted if the clock
    rate is unknown), whether or not RTCP was received. Egress entries are
    unchanged. Their only data source would be RTCP from the far end, which the
    existing MOS fields already cover.
  • Initialise the shm lost_bits to -1 (userspace only, no kernel code
    change).
  • ssrc_collect_metrics() converts a copy of the jitter value instead of
    overwriting the estimator.
  • get_rtp_payload_type() takes a const struct codec_store *, so that the
    const call_media in ng_stats_media() can be used.
  • Docs: ingress SSRCs / egress SSRCs are now described in
    docs/ng_control_protocol.md.

The existing cumulative loss key keeps its meaning (local sequence-gap loss,
since 90f4f8e) and its position, so consumers that read it alongside MOS see no
change.

Testing

  • Built in a Debian trixie container with the build dependencies from
    debian/control. make builds without warnings.
  • New test in t/auto-daemon-tests-ssrc.pl: one RTP packet is dropped out of 41
    with no RTCP. It checks cumulative loss == 1, jitter > 0, that there are
    no MOS keys, the top-level SSRC dict, and that egress entries have neither
    key. It fails on unmodified master (3 failures) and passes with this change.
  • make -C t unit-tests: all pass.
  • Every target in daemon-tests passes (0 not ok), including
    daemon-tests-ssrc (117), -rtcp (35), -measure-rtp, -mos-legacy,
    -mos-fullband, -main (3035), -redis* and -bundle. I also ran
    daemon-tests-kernel-api (343) and daemon-tests-stats (479) with
    RTPENGINE_EXTENDED_TESTS=1, and both pass. I did not run the other
    extended-only suites (DTX, EVS, audio player and so on); they don't touch
    this code.
  • Each commit builds on its own.
  • One existing test expectation changed: t/auto-daemon-tests-rtcp.pl
    expected a jitter of exactly 0 in rtpengine-generated RRs. That 0 came from
    the ssrc_collect_metrics() bug, which divided the estimator by 8 every
    second under --measure-rtp. The real value for its 3 back-to-back packets is
    about 0x10–0x11 timestamp units, so the regex now accepts 1..127.

Limitations / not covered

  • The kernel path is not runtime-tested. No kernel module could be loaded
    in the test environment. The kernel code is unchanged: this PR only reads
    fields the kernel already maintains, and initialises one shm field.
  • The kernel measures loss/jitter only when reti->rtp_stats is set, which
    means measure-rtp, RTCP generation, or MQTT. Without one of those, packets
    forwarded by the kernel are not included in the values. This is documented.
  • The loss estimator is the existing one, shared by userspace and kernel. Gaps
    of 32 or more packets are approximated, and jumps over 256 are treated as a
    sequence reset. Small inaccuracies are possible around userspace/kernel
    handovers, because each side keeps its own reordering window. This PR does
    not change the estimator.
  • Duplicates are still counted in userspace only.

🤖 Generated with Claude Code

Comment thread daemon/ssrc.c
Comment on lines +42 to 43
ent->stats->lost_bits = -1; // kernel tracker, so it doesn't count its first 32 packets as lost
ent->lost_bits = -1;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably eliminate ent->lost_bits altogether and just use the shared data from both kernel and user space

Comment thread daemon/ssrc.c
Comment on lines +703 to +715
// sequence-gap based loss, counted in userspace and in the kernel for packets each one handled
uint32_t ssrc_packets_lost(struct ssrc_entry_call *se) {
return se->packets_lost + atomic_get_na(&se->stats->total_lost);
}

// RFC 3550 interarrival jitter (scaled by 16, in RTP timestamp units). The kernel keeps
// its own estimate if it measures RTP stats, which is the current one once it has taken
// over forwarding this SSRC.
uint32_t ssrc_jitter(struct ssrc_entry_call *se) {
uint32_t kernel_jitter = atomic_get_na(&se->stats->jitter);
return kernel_jitter ? kernel_jitter : se->jitter;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment here, would make more sense to just have a single value used by both code paths, no?

Comment thread daemon/ssrc.c Outdated
// Mirrors kernel-module/nft_rtpengine.c:rtp_stats(). During offload handover,
// the kernel and userspace must not update the same SSRC simultaneously.
void ssrc_track_rtp(struct ssrc_entry_call *se, uint16_t seq, uint32_t timestamp) {
LOCK(&se->h.lock);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that's gonna fly, because the loss calculation is non-atomic and relies on the mutex/lock, which is handled differently between kernel and user space. So kernel and user space would race each other. It would require a lock that can be used from both contexts.

I'm ok with leaving it as it is for now, because that would probably be a larger rework.

Please don't keep adding new commits and please avoid merge commits. Instead rebase everything on the latest master if needed.

Expose locally measured RTP loss and jitter in NG query/delete output
even without RTCP, merge kernel-measured per-SSRC loss/jitter from shm
into userspace consumers (NG, MQTT, RTCP RRs, call summary), start the
kernel loss tracker at -1 like userspace, and skip jitter metrics when
no RTP clock rate is known.

Closes sipwise#934
@darwvin-dev
darwvin-dev force-pushed the fix/ng-ssrc-loss-without-rtcp-934 branch from 42273df to 15e630d Compare October 16, 2026 13:01
@darwvin-dev

Copy link
Copy Markdown
Contributor Author

Squashed into a single commit rebased on master, and dropped the shared kernel/userspace tracker given the locking race you pointed out.

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.

QoS stats without RTCP, QoS stats per call leg

2 participants