Skip to content

Commit

Permalink
Add Req/Res count/time to candidate stats
Browse files Browse the repository at this point in the history
These details will provide information for
connectivity issue.
  • Loading branch information
cnderrauber committed Feb 26, 2025
1 parent d21ae5e commit 8f0b151
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 10 deletions.
11 changes: 11 additions & 0 deletions agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,11 @@ func (a *Agent) sendBindingRequest(m *stun.Message, local, remote Candidate) {
isUseCandidate: m.Contains(stun.AttrUseCandidate),
})

if pair := a.findPair(local, remote); pair != nil {
pair.UpdateRequestSent()
} else {
a.log.Warnf("Failed to find pair for add binding request from %s to %s", local, remote)
}

Check warning on line 998 in agent.go

View check run for this annotation

Codecov / codecov/patch

agent.go#L997-L998

Added lines #L997 - L998 were not covered by tests
a.sendSTUN(m, local, remote)
}

Expand All @@ -1014,6 +1019,12 @@ func (a *Agent) sendBindingSuccess(m *stun.Message, local, remote Candidate) {
); err != nil {
a.log.Warnf("Failed to handle inbound ICE from: %s to: %s error: %s", local, remote, err)
} else {

if pair := a.findPair(local, remote); pair != nil {
pair.UpdateResponseSent()
} else {
a.log.Warnf("Failed to find pair for add binding response from %s to %s", local, remote)
}
a.sendSTUN(out, local, remote)
}
}
Expand Down
16 changes: 10 additions & 6 deletions agent_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,22 @@ func (a *Agent) GetCandidatePairsStats() []CandidatePairStats {
// BytesReceived uint64
// LastPacketSentTimestamp time.Time
// LastPacketReceivedTimestamp time.Time
// FirstRequestTimestamp time.Time
// LastRequestTimestamp time.Time
// LastResponseTimestamp time.Time
FirstRequestTimestamp: cp.FirstRequestSentAt(),
LastRequestTimestamp: cp.LastRequestSentAt(),
FirstResponseTimestamp: cp.FirstReponseReceivedAt(),
LastResponseTimestamp: cp.LastResponseReceivedAt(),
FirstRequestReceivedTimestamp: cp.FirstRequestReceivedAt(),
LastRequestReceivedTimestamp: cp.LastRequestReceivedAt(),

TotalRoundTripTime: cp.TotalRoundTripTime(),
CurrentRoundTripTime: cp.CurrentRoundTripTime(),
// AvailableOutgoingBitrate float64
// AvailableIncomingBitrate float64
// CircuitBreakerTriggerCount uint32
// RequestsReceived uint64
// RequestsSent uint64
RequestsReceived: cp.RequestsReceived(),
RequestsSent: cp.RequestsSent(),
ResponsesReceived: cp.ResponsesReceived(),
// ResponsesSent uint64
ResponsesSent: cp.ResponsesSent(),
// RetransmissionsReceived uint64
// RetransmissionsSent uint64
// ConsentRequestsSent uint64
Expand Down
48 changes: 46 additions & 2 deletions agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -715,14 +715,18 @@ func TestCandidatePairsStats(t *testing.T) { //nolint:cyclop
p := agent.findPair(hostLocal, remote)

if p == nil {
agent.addPair(hostLocal, remote)
p = agent.addPair(hostLocal, remote)
}
p.UpdateRequestReceived()
p.UpdateRequestSent()
p.UpdateResponseSent()
p.UpdateRoundTripTime(time.Second)
}

p := agent.findPair(hostLocal, prflxRemote)
p.state = CandidatePairStateFailed

for i := 0; i < 10; i++ {
for i := 1; i < 10; i++ {
p.UpdateRoundTripTime(time.Duration(i+1) * time.Second)
}

Expand All @@ -749,6 +753,46 @@ func TestCandidatePairsStats(t *testing.T) { //nolint:cyclop
default:
t.Fatal("invalid remote candidate ID")
}

if cps.FirstRequestTimestamp.IsZero() {
t.Fatal("missing first request timestamp")
}

if cps.LastRequestTimestamp.IsZero() {
t.Fatal("missing last request timestamp")
}

if cps.FirstResponseTimestamp.IsZero() {
t.Fatal("missing first response timestamp")
}

if cps.LastResponseTimestamp.IsZero() {
t.Fatal("missing last response timestamp")
}

if cps.FirstRequestReceivedTimestamp.IsZero() {
t.Fatal("missing first request received timestamp")
}

if cps.LastRequestReceivedTimestamp.IsZero() {
t.Fatal("missing last packet sent timestamp")
}

if cps.RequestsReceived == 0 {
t.Fatal("missing requests received")
}

if cps.RequestsSent == 0 {
t.Fatal("missing requests sent")
}

if cps.ResponsesSent == 0 {
t.Fatal("missing responses sent")
}

if cps.ResponsesReceived == 0 {
t.Fatal("missing responses received")
}
}

if relayPairStat.RemoteCandidateID != relayRemote.ID() {
Expand Down
101 changes: 100 additions & 1 deletion candidatepair.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,18 @@ type CandidatePair struct {
// stats
currentRoundTripTime int64 // in ns
totalRoundTripTime int64 // in ns
responsesReceived uint64

requestsReceived uint64
requestsSent uint64
responsesReceived uint64
responsesSent uint64

firstRequestSentAt atomic.Value // time.Time
lastRequestSentAt atomic.Value //time.Time

Check failure on line 43 in candidatepair.go

View workflow job for this annotation

GitHub Actions / lint / Go

commentFormatting: put a space between `//` and comment text (gocritic)
firstReponseReceivedAt atomic.Value //time.Time

Check failure on line 44 in candidatepair.go

View workflow job for this annotation

GitHub Actions / lint / Go

commentFormatting: put a space between `//` and comment text (gocritic)
lastResponseReceivedAt atomic.Value //time.Time

Check failure on line 45 in candidatepair.go

View workflow job for this annotation

GitHub Actions / lint / Go

commentFormatting: put a space between `//` and comment text (gocritic)
firstRequestReceivedAt atomic.Value //time.Time
lastRequestReceivedAt atomic.Value //time.Time
}

func (p *CandidatePair) String() string {
Expand Down Expand Up @@ -127,6 +138,10 @@ func (p *CandidatePair) UpdateRoundTripTime(rtt time.Duration) {
atomic.StoreInt64(&p.currentRoundTripTime, rttNs)
atomic.AddInt64(&p.totalRoundTripTime, rttNs)
atomic.AddUint64(&p.responsesReceived, 1)

now := time.Now()
p.firstReponseReceivedAt.CompareAndSwap(nil, now)
p.lastResponseReceivedAt.Store(now)
}

// CurrentRoundTripTime returns the current round trip time in seconds
Expand All @@ -141,8 +156,92 @@ func (p *CandidatePair) TotalRoundTripTime() float64 {
return time.Duration(atomic.LoadInt64(&p.totalRoundTripTime)).Seconds()
}

// RequestsReceived returns the total number of connectivity checks received
// https://www.w3.org/TR/webrtc-stats/#dom-rtcicecandidatepairstats-requestsreceived
func (p *CandidatePair) RequestsReceived() uint64 {
return atomic.LoadUint64(&p.requestsReceived)
}

// RequestsSent returns the total number of connectivity checks sent
// https://www.w3.org/TR/webrtc-stats/#dom-rtcicecandidatepairstats-requestssent
func (p *CandidatePair) RequestsSent() uint64 {
return atomic.LoadUint64(&p.requestsSent)
}

// ResponsesReceived returns the total number of connectivity responses received
// https://www.w3.org/TR/webrtc-stats/#dom-rtcicecandidatepairstats-responsesreceived
func (p *CandidatePair) ResponsesReceived() uint64 {
return atomic.LoadUint64(&p.responsesReceived)
}

// ResponsesSent returns the total number of connectivity responses sent
// https://www.w3.org/TR/webrtc-stats/#dom-rtcicecandidatepairstats-responsessent
func (p *CandidatePair) ResponsesSent() uint64 {
return atomic.LoadUint64(&p.responsesSent)
}

// FirstRequestSentAt returns the timestamp of the first connectivity check sent
func (p *CandidatePair) FirstRequestSentAt() time.Time {
if v := p.firstRequestSentAt.Load(); v != nil {
return v.(time.Time)

Check failure on line 186 in candidatepair.go

View workflow job for this annotation

GitHub Actions / lint / Go

type assertion must be checked (forcetypeassert)
}
return time.Time{}

Check warning on line 188 in candidatepair.go

View check run for this annotation

Codecov / codecov/patch

candidatepair.go#L188

Added line #L188 was not covered by tests
}

// LastRequestSentAt returns the timestamp of the last connectivity check sent
func (p *CandidatePair) LastRequestSentAt() time.Time {
if v := p.lastRequestSentAt.Load(); v != nil {
return v.(time.Time)

Check failure on line 194 in candidatepair.go

View workflow job for this annotation

GitHub Actions / lint / Go

type assertion must be checked (forcetypeassert)
}
return time.Time{}

Check warning on line 196 in candidatepair.go

View check run for this annotation

Codecov / codecov/patch

candidatepair.go#L196

Added line #L196 was not covered by tests
}

// FirstReponseReceivedAt returns the timestamp of the first connectivity response received
func (p *CandidatePair) FirstReponseReceivedAt() time.Time {
if v := p.firstReponseReceivedAt.Load(); v != nil {
return v.(time.Time)

Check failure on line 202 in candidatepair.go

View workflow job for this annotation

GitHub Actions / lint / Go

type assertion must be checked (forcetypeassert)
}
return time.Time{}

Check warning on line 204 in candidatepair.go

View check run for this annotation

Codecov / codecov/patch

candidatepair.go#L204

Added line #L204 was not covered by tests
}

// LastResponseReceivedAt returns the timestamp of the last connectivity response received
func (p *CandidatePair) LastResponseReceivedAt() time.Time {
if v := p.lastResponseReceivedAt.Load(); v != nil {
return v.(time.Time)
}
return time.Time{}

Check warning on line 212 in candidatepair.go

View check run for this annotation

Codecov / codecov/patch

candidatepair.go#L212

Added line #L212 was not covered by tests
}

// FirstRequestReceivedAt returns the timestamp of the first connectivity check received
func (p *CandidatePair) FirstRequestReceivedAt() time.Time {
if v := p.firstRequestReceivedAt.Load(); v != nil {
return v.(time.Time)
}
return time.Time{}

Check warning on line 220 in candidatepair.go

View check run for this annotation

Codecov / codecov/patch

candidatepair.go#L220

Added line #L220 was not covered by tests
}

// LastRequestReceivedAt returns the timestamp of the last connectivity check received
func (p *CandidatePair) LastRequestReceivedAt() time.Time {
if v := p.lastRequestReceivedAt.Load(); v != nil {
return v.(time.Time)
}
return time.Time{}

Check warning on line 228 in candidatepair.go

View check run for this annotation

Codecov / codecov/patch

candidatepair.go#L228

Added line #L228 was not covered by tests
}

func (p *CandidatePair) UpdateRequestSent() {

Check failure on line 231 in candidatepair.go

View workflow job for this annotation

GitHub Actions / lint / Go

exported: exported method CandidatePair.UpdateRequestSent should have comment or be unexported (revive)
atomic.AddUint64(&p.requestsSent, 1)
now := time.Now()
p.firstRequestSentAt.CompareAndSwap(nil, now)
p.lastRequestSentAt.Store(now)
}

func (p *CandidatePair) UpdateResponseSent() {

Check failure on line 238 in candidatepair.go

View workflow job for this annotation

GitHub Actions / lint / Go

exported: exported method CandidatePair.UpdateResponseSent should have comment or be unexported (revive)
atomic.AddUint64(&p.responsesSent, 1)
}

func (p *CandidatePair) UpdateRequestReceived() {

Check failure on line 242 in candidatepair.go

View workflow job for this annotation

GitHub Actions / lint / Go

exported: exported method CandidatePair.UpdateRequestReceived should have comment or be unexported (revive)
atomic.AddUint64(&p.requestsReceived, 1)
now := time.Now()
p.firstRequestReceivedAt.CompareAndSwap(nil, now)
p.lastRequestReceivedAt.Store(now)
}
5 changes: 4 additions & 1 deletion selection.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,12 @@ func (s *controllingSelector) HandleBindingRequest(message *stun.Message, local,
pair := s.agent.findPair(local, remote)

if pair == nil {
s.agent.addPair(local, remote)
pair = s.agent.addPair(local, remote)
pair.UpdateRequestReceived()

return
}
pair.UpdateRequestReceived()

if pair.state == CandidatePairStateSucceeded && s.nominatedPair == nil && s.agent.getSelectedPair() == nil {
bestPair := s.agent.getBestAvailableCandidatePair()
Expand Down Expand Up @@ -281,6 +283,7 @@ func (s *controlledSelector) HandleBindingRequest(message *stun.Message, local,
if pair == nil {
pair = s.agent.addPair(local, remote)
}
pair.UpdateRequestReceived()

if message.Contains(stun.AttrUseCandidate) { //nolint:nestif
// https://tools.ietf.org/html/rfc8445#section-7.3.1.5
Expand Down
12 changes: 12 additions & 0 deletions stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,22 @@ type CandidatePairStats struct {
// (LastRequestTimestamp - FirstRequestTimestamp) / RequestsSent.
LastRequestTimestamp time.Time

// FirstResponseTimestamp represents the timestamp at which the first STUN response
// was received on this particular candidate pair.
FirstResponseTimestamp time.Time

// LastResponseTimestamp represents the timestamp at which the last STUN response
// was received on this particular candidate pair.
LastResponseTimestamp time.Time

// FirstRequestReceivedTimestamp represents the timestamp at which the first
// connectivity check request was received.
FirstRequestReceivedTimestamp time.Time

// LastRequestReceivedTimestamp represents the timestamp at which the last
// connectivity check request was received.
LastRequestReceivedTimestamp time.Time

// TotalRoundTripTime represents the sum of all round trip time measurements
// in seconds since the beginning of the session, based on STUN connectivity
// check responses (ResponsesReceived), including those that reply to requests
Expand Down

0 comments on commit 8f0b151

Please sign in to comment.