Skip to content

Commit a1771d6

Browse files
committed
Add NADA implementation
This change adds the NADA congestion control implementation. The binding to cc is left TODO.
1 parent 09051cd commit a1771d6

File tree

10 files changed

+611
-0
lines changed

10 files changed

+611
-0
lines changed

pkg/nada/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# [RFC 8698] NADA: A Unified Congestion Control Scheme for Real-Time Media
2+
3+
Notes:
4+
5+
* The receiver in this implementation assumes a monotonically ordered sequence of packets.

pkg/nada/config.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package rfc8698
2+
3+
import "time"
4+
5+
type Bits uint32
6+
7+
type BitsPerSecond float64
8+
9+
const Kbps = BitsPerSecond(1_000)
10+
const Mbps = BitsPerSecond(1_000_000)
11+
12+
type Config struct {
13+
// Weight of priority of the flow
14+
Priority float64
15+
// Minimum rate of the application supported by the media encoder
16+
MinimumRate BitsPerSecond // RMIN
17+
// Maximum rate of the application supported by media encoder
18+
MaximumRate BitsPerSecond // RMAX
19+
// Reference congestion level
20+
ReferenceCongestionLevel time.Duration // XREF
21+
// Scaling parameter for gradual rate update calculation
22+
κ float64
23+
// Scaling parameter for gradual rate update calculation
24+
η float64
25+
// Upper bound of RTT in gradual rate update calculation
26+
τ time.Duration
27+
// Target feedback interval
28+
δ time.Duration
29+
30+
// Observation window in time for calculating packet summary statistics at receiver
31+
LogWindow time.Duration // LOGWIN
32+
// Threshold for determining queuing delay build up at receiver
33+
QueueingDelayThreshold time.Duration
34+
// Bound on filtering delay
35+
FilteringDelay time.Duration // DFILT
36+
// Upper bound on rate increase ratio for accelerated ramp-up
37+
γ_max float64
38+
// Upper bound on self-inflicted queueing delay during ramp up
39+
QueueBound time.Duration // QBOUND
40+
41+
// Multiplier for self-scaling the expiration threshold of the last observed loss
42+
// (loss_exp) based on measured average loss interval (loss_int)
43+
LossMultiplier float64 // MULTILOSS
44+
// Delay threshold for invoking non-linear warping
45+
DelayThreshold time.Duration // QTH
46+
// Scaling parameter in the exponent of non-linear warping
47+
λ float64
48+
49+
// Reference packet loss ratio
50+
ReferencePacketLossRatio float64 // PLRREF
51+
// Reference packet marking ratio
52+
ReferencePacketMarkingRatio float64 // PMRREF
53+
// Reference delay penalty for loss when lacket loss ratio is at least PLRREF
54+
ReferenceDelayLoss time.Duration // DLOSS
55+
// Reference delay penalty for ECN marking when packet marking is at PMRREF
56+
ReferenceDelayMarking time.Duration // DMARK
57+
58+
// Frame rate of incoming video
59+
FrameRate float64 // FRAMERATE
60+
// Scaling parameter for modulating outgoing sending rate
61+
β_s float64
62+
// Scaling parameter for modulating video encoder target rate
63+
β_v float64
64+
// Smoothing factor in exponential smoothing of packet loss and marking rate
65+
α float64
66+
}
67+
68+
var DefaultConfig = Config{
69+
Priority: 1.0,
70+
MinimumRate: 150 * Kbps,
71+
MaximumRate: 1500 * Kbps,
72+
ReferenceCongestionLevel: 10 * time.Millisecond,
73+
κ: 0.5,
74+
η: 2.0,
75+
τ: 500 * time.Millisecond,
76+
δ: 100 * time.Millisecond,
77+
78+
LogWindow: 500 * time.Millisecond,
79+
QueueingDelayThreshold: 10 * time.Millisecond,
80+
FilteringDelay: 120 * time.Millisecond,
81+
γ_max: 0.5,
82+
QueueBound: 50 * time.Millisecond,
83+
84+
LossMultiplier: 7.0,
85+
DelayThreshold: 50 * time.Millisecond,
86+
λ: 0.5,
87+
88+
ReferencePacketLossRatio: 0.01,
89+
ReferencePacketMarkingRatio: 0.01,
90+
ReferenceDelayLoss: 10 * time.Millisecond,
91+
ReferenceDelayMarking: 2 * time.Millisecond,
92+
93+
FrameRate: 30.0,
94+
β_s: 0.1,
95+
β_v: 0.1,
96+
α: 0.1,
97+
}

pkg/nada/ecn/ecn.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package ecn
2+
3+
import (
4+
"errors"
5+
"syscall"
6+
)
7+
8+
// CheckExplicitCongestionNotification checks if the given oob data includes an ECN bit set.
9+
func CheckExplicitCongestionNotification(oob []byte) (uint8, error) {
10+
ctrlMsgs, err := syscall.ParseSocketControlMessage(oob)
11+
if err != nil {
12+
return 0, err
13+
}
14+
for _, ctrlMsg := range ctrlMsgs {
15+
if ctrlMsg.Header.Type == syscall.IP_TOS {
16+
return (ctrlMsg.Data[0] & 0x3), nil
17+
}
18+
}
19+
return 0, errors.New("no ECN control message")
20+
}

pkg/nada/ecn/ecn_darwin.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package ecn
2+
3+
import (
4+
"net"
5+
)
6+
7+
// EnableExplicitCongestionNotification enables ECN on the given connection.
8+
func EnableExplicitCongestionNotification(conn *net.UDPConn) {
9+
// noop.
10+
}

pkg/nada/ecn/ecn_linux.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package ecn
2+
3+
import (
4+
"net"
5+
"reflect"
6+
"syscall"
7+
)
8+
9+
// EnableExplicitCongestionNotification enables ECN on the given connection.
10+
func EnableExplicitCongestionNotification(conn *net.UDPConn) {
11+
ptrVal := reflect.ValueOf(*conn)
12+
fdmember := reflect.Indirect(ptrVal).FieldByName("fd")
13+
pfdmember := reflect.Indirect(fdmember).FieldByName("pfd")
14+
netfdmember := reflect.Indirect(pfdmember).FieldByName("Sysfd")
15+
fd := int(netfdmember.Int())
16+
syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_RECVTOS, 1)
17+
}

pkg/nada/packet_stream.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package rfc8698
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"sync"
7+
"time"
8+
9+
"github.com/muxable/rtptools/pkg/x_range"
10+
)
11+
12+
type packet struct {
13+
ts time.Time
14+
seq uint16
15+
ecn bool
16+
size Bits
17+
queueingDelay bool
18+
}
19+
20+
// String returns a string representation of the packet.
21+
func (p *packet) String() string {
22+
return fmt.Sprintf("%v@%v", p.seq, p.ts.Nanosecond()%1000)
23+
}
24+
25+
type packetStream struct {
26+
sync.Mutex
27+
28+
window time.Duration
29+
packets []*packet
30+
markCount uint16
31+
totalSize Bits
32+
queueingDelayCount uint16
33+
34+
tail uint16
35+
}
36+
37+
func newPacketStream(window time.Duration) *packetStream {
38+
return &packetStream{
39+
window: window,
40+
}
41+
}
42+
43+
var errTimeOrder = errors.New("invalid packet timestamp ordering")
44+
45+
// add writes a packet to the underlying stream.
46+
func (ps *packetStream) add(ts time.Time, seq uint16, ecn bool, size Bits, queueingDelay bool) error {
47+
ps.Lock()
48+
defer ps.Unlock()
49+
50+
if len(ps.packets) > 0 && ps.packets[len(ps.packets)-1].ts.After(ts) {
51+
return errTimeOrder
52+
}
53+
// check if the packet seq already exists.
54+
for _, p := range ps.packets {
55+
if p.seq == seq {
56+
return errTimeOrder
57+
}
58+
}
59+
ps.packets = append(ps.packets, &packet{
60+
ts: ts,
61+
seq: seq,
62+
ecn: ecn,
63+
size: size,
64+
queueingDelay: queueingDelay,
65+
})
66+
if ecn {
67+
ps.markCount++
68+
}
69+
ps.totalSize += size
70+
if queueingDelay {
71+
ps.queueingDelayCount++
72+
}
73+
return nil
74+
}
75+
76+
// prune removes packets that are older than the window and returns the loss and marking rate.
77+
func (ps *packetStream) prune(now time.Time) (loss float64, marking float64, receivingRate BitsPerSecond, hasQueueingDelay bool) {
78+
ps.Lock()
79+
defer ps.Unlock()
80+
81+
startTs := now.Add(-ps.window)
82+
start := 0
83+
for ; start < len(ps.packets) && ps.packets[start].ts.Before(startTs); start++ {
84+
// decrement mark count if ecn.
85+
if ps.packets[start].ecn {
86+
ps.markCount--
87+
}
88+
ps.totalSize -= ps.packets[start].size
89+
if ps.packets[start].queueingDelay {
90+
ps.queueingDelayCount--
91+
}
92+
}
93+
if start > 0 {
94+
ps.packets = ps.packets[start:]
95+
}
96+
seqs := make([]uint16, len(ps.packets))
97+
for i, p := range ps.packets {
98+
seqs[i] = p.seq
99+
}
100+
begin, end := x_range.GetSeqRange(seqs)
101+
loss = 1 - float64(len(ps.packets))/float64(end-begin+1)
102+
marking = float64(ps.markCount) / float64(end-begin+1)
103+
return loss, marking, BitsPerSecond(float64(ps.totalSize) / ps.window.Seconds()), ps.queueingDelayCount > 0
104+
}

pkg/nada/receiver.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package rfc8698
2+
3+
import (
4+
"math"
5+
"time"
6+
)
7+
8+
type Receiver struct {
9+
config Config
10+
BaselineDelay time.Duration // d_base
11+
EstimatedQueuingDelay time.Duration // d_queue
12+
EstimatedPacketLossRatio float64
13+
EstimatedPacketECNMarkingRatio float64
14+
ReceivingRate BitsPerSecond
15+
LastTimestamp time.Time
16+
CurrentTimestamp time.Time
17+
RecommendedRateAdaptionMode RateAdaptionMode
18+
19+
packetStream *packetStream
20+
}
21+
22+
func NewReceiver(now time.Time, config Config) *Receiver {
23+
return &Receiver{
24+
config: config,
25+
BaselineDelay: time.Duration(1<<63 - 1),
26+
EstimatedPacketLossRatio: 0.0,
27+
EstimatedPacketECNMarkingRatio: 0.0,
28+
ReceivingRate: 0.0,
29+
LastTimestamp: now,
30+
CurrentTimestamp: now,
31+
packetStream: newPacketStream(config.LogWindow),
32+
}
33+
}
34+
35+
// OnReceiveMediaPacket implements the media receive algorithm.
36+
func (r *Receiver) OnReceiveMediaPacket(now time.Time, sent time.Time, seq uint16, ecn bool, size Bits) error {
37+
// obtain current timestamp t_curr from system clock
38+
r.CurrentTimestamp = now
39+
40+
// obtain from packet header sending time stamp t_sent
41+
t_sent := sent
42+
43+
// obtain one-way delay measurement: d_fwd = t_curr - t_sent
44+
d_fwd := r.CurrentTimestamp.Sub(t_sent)
45+
46+
// update baseline delay: d_base = min(d_base, d_fwd)
47+
if d_fwd < r.BaselineDelay {
48+
r.BaselineDelay = d_fwd
49+
}
50+
51+
// update queuing delay: d_queue = d_fwd - d_base
52+
r.EstimatedQueuingDelay = d_fwd - r.BaselineDelay
53+
54+
if err := r.packetStream.add(now, seq, ecn, size, r.EstimatedQueuingDelay > r.config.QueueingDelayThreshold); err != nil {
55+
return err
56+
}
57+
58+
p_loss_inst, p_mark_inst, r_recv_inst, hasQueueingDelay := r.packetStream.prune(now)
59+
60+
// update packet loss ratio estimate p_loss
61+
// r.config.α*p_loss_inst + (1-r.config.α)*r.EstimatedPacketLossRatio
62+
r.EstimatedPacketLossRatio = r.config.α*(p_loss_inst-r.EstimatedPacketLossRatio) + r.EstimatedPacketLossRatio
63+
64+
// update packet marking ratio estimate p_mark
65+
// r.config.α*p_mark_inst + (1-r.config.α)*r.EstimatedPacketECNMarkingRatio
66+
r.EstimatedPacketECNMarkingRatio = r.config.α*(p_mark_inst-r.EstimatedPacketECNMarkingRatio) + r.EstimatedPacketECNMarkingRatio
67+
68+
// update measurement of receiving rate r_recv
69+
r.ReceivingRate = r_recv_inst
70+
71+
// update recommended rate adaption mode.
72+
if p_loss_inst == 0 && !hasQueueingDelay {
73+
r.RecommendedRateAdaptionMode = RateAdaptionModeAcceleratedRampUp
74+
} else {
75+
r.RecommendedRateAdaptionMode = RateAdaptionModeGradualUpdate
76+
}
77+
78+
return nil
79+
}
80+
81+
// BuildFeedbackPacket creates a new feedback packet.
82+
func (r *Receiver) BuildFeedbackReport() *FeedbackReport {
83+
// calculate non-linear warping of delay d_tilde if packet loss exists
84+
equivalentDelay := r.equivalentDelay()
85+
86+
// calculate current aggregate congestion signal x_curr
87+
aggregatedCongestionSignal := equivalentDelay +
88+
scale(r.config.ReferenceDelayMarking, math.Pow(r.EstimatedPacketECNMarkingRatio/r.config.ReferencePacketMarkingRatio, 2)) +
89+
scale(r.config.ReferenceDelayLoss, math.Pow(r.EstimatedPacketLossRatio/r.config.ReferencePacketLossRatio, 2))
90+
91+
// determine mode of rate adaptation for sender: rmode
92+
rmode := r.RecommendedRateAdaptionMode
93+
94+
// update t_last = t_curr
95+
r.LastTimestamp = r.CurrentTimestamp
96+
97+
// send feedback containing values of: rmode, x_curr, and r_recv
98+
return &FeedbackReport{
99+
RecommendedRateAdaptionMode: rmode,
100+
AggregatedCongestionSignal: aggregatedCongestionSignal,
101+
ReceivingRate: r.ReceivingRate,
102+
}
103+
}
104+
105+
func scale(t time.Duration, x float64) time.Duration {
106+
return time.Duration(float64(t) * x)
107+
}
108+
109+
// d_tilde computes d_tilde as described by
110+
//
111+
// / d_queue, if d_queue<QTH;
112+
// |
113+
// d_tilde = < (1)
114+
// | (d_queue-QTH)
115+
// \ QTH exp(-LAMBDA ---------------) , otherwise.
116+
// QTH
117+
//
118+
func (r *Receiver) equivalentDelay() time.Duration {
119+
if r.EstimatedQueuingDelay < r.config.DelayThreshold {
120+
return r.EstimatedQueuingDelay
121+
}
122+
scaling := math.Exp(-r.config.λ * float64((r.EstimatedQueuingDelay-r.config.DelayThreshold)/r.config.DelayThreshold))
123+
return scale(r.config.DelayThreshold, scaling)
124+
}

0 commit comments

Comments
 (0)