Initial stats

Add basic candidate and candidate pair stats
This commit is contained in:
Hugo Arregui
2019-06-22 11:55:06 +00:00
committed by Hugo Arregui
parent da599f54ff
commit 1ff70e0ee1
11 changed files with 635 additions and 3 deletions
+107
View File
@@ -973,6 +973,113 @@ func (a *Agent) closeMulticastConn() {
}
}
// GetCandidatePairsStats returns a list of candidate pair stats
func (a *Agent) GetCandidatePairsStats() []CandidatePairStats {
resultChan := make(chan []CandidatePairStats)
err := a.run(func(agent *Agent) {
result := make([]CandidatePairStats, 0, len(agent.checklist))
for _, cp := range agent.checklist {
stat := CandidatePairStats{
Timestamp: time.Now(),
LocalCandidateID: cp.local.ID(),
RemoteCandidateID: cp.remote.ID(),
State: cp.state,
// Nominated bool
// PacketsSent uint32
// PacketsReceived uint32
// BytesSent uint64
// BytesReceived uint64
// LastPacketSentTimestamp time.Time
// LastPacketReceivedTimestamp time.Time
// FirstRequestTimestamp time.Time
// LastRequestTimestamp time.Time
// LastResponseTimestamp time.Time
// TotalRoundTripTime float64
// CurrentRoundTripTime float64
// AvailableOutgoingBitrate float64
// AvailableIncomingBitrate float64
// CircuitBreakerTriggerCount uint32
// RequestsReceived uint64
// RequestsSent uint64
// ResponsesReceived uint64
// ResponsesSent uint64
// RetransmissionsReceived uint64
// RetransmissionsSent uint64
// ConsentRequestsSent uint64
// ConsentExpiredTimestamp time.Time
}
result = append(result, stat)
}
resultChan <- result
})
if err != nil {
a.log.Errorf("error getting candidate pairs stats %v", err)
return []CandidatePairStats{}
}
return <-resultChan
}
// GetLocalCandidatesStats returns a list of local candidates stats
func (a *Agent) GetLocalCandidatesStats() []CandidateStats {
resultChan := make(chan []CandidateStats)
err := a.run(func(agent *Agent) {
result := make([]CandidateStats, 0, len(agent.localCandidates))
for networkType, localCandidates := range agent.localCandidates {
for _, c := range localCandidates {
stat := CandidateStats{
Timestamp: time.Now(),
ID: c.ID(),
NetworkType: networkType,
IP: c.Address(),
Port: c.Port(),
CandidateType: c.Type(),
Priority: c.Priority(),
// URL string
RelayProtocol: "udp",
// Deleted bool
}
result = append(result, stat)
}
}
resultChan <- result
})
if err != nil {
a.log.Errorf("error getting candidate pairs stats %v", err)
return []CandidateStats{}
}
return <-resultChan
}
// GetRemoteCandidatesStats returns a list of remote candidates stats
func (a *Agent) GetRemoteCandidatesStats() []CandidateStats {
resultChan := make(chan []CandidateStats)
err := a.run(func(agent *Agent) {
result := make([]CandidateStats, 0, len(agent.remoteCandidates))
for networkType, localCandidates := range agent.remoteCandidates {
for _, c := range localCandidates {
stat := CandidateStats{
Timestamp: time.Now(),
ID: c.ID(),
NetworkType: networkType,
IP: c.Address(),
Port: c.Port(),
CandidateType: c.Type(),
Priority: c.Priority(),
// URL string
RelayProtocol: "udp",
}
result = append(result, stat)
}
}
resultChan <- result
})
if err != nil {
a.log.Errorf("error getting candidate pairs stats %v", err)
return []CandidateStats{}
}
return <-resultChan
}
// Role represents ICE agent role, which can be controlling or controlled.
type Role byte
+303 -1
View File
@@ -116,7 +116,7 @@ func TestPairPriority(t *testing.T) {
p = a.addPair(hostLocal, remote)
}
p.state = candidatePairStateValid
p.state = CandidatePairStateSucceeded
bestPair := a.getBestValidCandidatePair()
if bestPair.String() != (&candidatePair{remote: remote, local: hostLocal}).String() {
t.Fatalf("Unexpected bestPair %s (expected remote: %s)", bestPair, remote)
@@ -617,3 +617,305 @@ func TestInvalidGather(t *testing.T) {
}
})
}
func TestCandidatePairStats(t *testing.T) {
// avoid deadlocks?
defer test.TimeOut(1 * time.Second).Stop()
a, err := NewAgent(&AgentConfig{})
if err != nil {
t.Fatalf("Failed to create agent: %s", err)
}
hostLocal, err := NewCandidateHost(
"udp",
"192.168.1.1", 19216,
1,
)
if err != nil {
t.Fatalf("Failed to construct local host candidate: %s", err)
}
relayRemote, err := NewCandidateRelay(
"udp",
"1.2.3.4", 2340,
1,
"4.3.2.1", 43210,
)
if err != nil {
t.Fatalf("Failed to construct remote relay candidate: %s", err)
}
srflxRemote, err := NewCandidateServerReflexive(
"udp",
"10.10.10.2", 19218,
1,
"4.3.2.1", 43212,
)
if err != nil {
t.Fatalf("Failed to construct remote srflx candidate: %s", err)
}
prflxRemote, err := NewCandidatePeerReflexive(
"udp",
"10.10.10.2", 19217,
1,
"4.3.2.1", 43211,
)
if err != nil {
t.Fatalf("Failed to construct remote prflx candidate: %s", err)
}
hostRemote, err := NewCandidateHost(
"udp",
"1.2.3.5", 12350,
1,
)
if err != nil {
t.Fatalf("Failed to construct remote host candidate: %s", err)
}
for _, remote := range []Candidate{relayRemote, srflxRemote, prflxRemote, hostRemote} {
p := a.findPair(hostLocal, remote)
if p == nil {
a.addPair(hostLocal, remote)
}
}
p := a.findPair(hostLocal, prflxRemote)
p.state = CandidatePairStateFailed
stats := a.GetCandidatePairsStats()
if len(stats) != 4 {
t.Fatal("expected 4 candidate pairs stats")
}
var relayPairStat, srflxPairStat, prflxPairStat, hostPairStat CandidatePairStats
for _, cps := range stats {
if cps.LocalCandidateID != hostLocal.ID() {
t.Fatal("invalid local candidate id")
}
switch cps.RemoteCandidateID {
case relayRemote.ID():
relayPairStat = cps
case srflxRemote.ID():
srflxPairStat = cps
case prflxRemote.ID():
prflxPairStat = cps
case hostRemote.ID():
hostPairStat = cps
default:
t.Fatal("invalid remote candidate ID")
}
}
if relayPairStat.RemoteCandidateID != relayRemote.ID() {
t.Fatal("missing host-relay pair stat")
}
if srflxPairStat.RemoteCandidateID != srflxRemote.ID() {
t.Fatal("missing host-srflx pair stat")
}
if prflxPairStat.RemoteCandidateID != prflxRemote.ID() {
t.Fatal("missing host-prflx pair stat")
}
if hostPairStat.RemoteCandidateID != hostRemote.ID() {
t.Fatal("missing host-host pair stat")
}
if prflxPairStat.State != CandidatePairStateFailed {
t.Fatalf("expected host-prfflx pair to have state failed, it has state %s instead",
prflxPairStat.State.String())
}
if err := a.Close(); err != nil {
t.Fatalf("Error on agent.Close(): %s", err)
}
}
func TestLocalCandidateStats(t *testing.T) {
// avoid deadlocks?
defer test.TimeOut(1 * time.Second).Stop()
a, err := NewAgent(&AgentConfig{})
if err != nil {
t.Fatalf("Failed to create agent: %s", err)
}
hostLocal, err := NewCandidateHost(
"udp",
"192.168.1.1", 19216,
1,
)
if err != nil {
t.Fatalf("Failed to construct local host candidate: %s", err)
}
srflxLocal, err := NewCandidateServerReflexive(
"udp",
"192.168.1.1", 19217,
1,
"4.3.2.1", 43212,
)
if err != nil {
t.Fatalf("Failed to construct local srflx candidate: %s", err)
}
a.localCandidates[NetworkTypeUDP4] = []Candidate{hostLocal, srflxLocal}
localStats := a.GetLocalCandidatesStats()
if len(localStats) != 2 {
t.Fatalf("expected 2 local candidates stats, got %d instead", len(localStats))
}
var hostLocalStat, srflxLocalStat CandidateStats
for _, stats := range localStats {
var candidate Candidate
switch stats.ID {
case hostLocal.ID():
hostLocalStat = stats
candidate = hostLocal
case srflxLocal.ID():
srflxLocalStat = stats
candidate = srflxLocal
default:
t.Fatal("invalid local candidate ID")
}
if stats.CandidateType != candidate.Type() {
t.Fatal("invalid stats CandidateType")
}
if stats.Priority != candidate.Priority() {
t.Fatal("invalid stats CandidateType")
}
if stats.IP != candidate.Address() {
t.Fatal("invalid stats IP")
}
}
if hostLocalStat.ID != hostLocal.ID() {
t.Fatal("missing host local stat")
}
if srflxLocalStat.ID != srflxLocal.ID() {
t.Fatal("missing srflx local stat")
}
if err := a.Close(); err != nil {
t.Fatalf("Error on agent.Close(): %s", err)
}
}
func TestRemoteCandidateStats(t *testing.T) {
// avoid deadlocks?
defer test.TimeOut(1 * time.Second).Stop()
a, err := NewAgent(&AgentConfig{})
if err != nil {
t.Fatalf("Failed to create agent: %s", err)
}
relayRemote, err := NewCandidateRelay(
"udp",
"1.2.3.4", 12340,
1,
"4.3.2.1", 43210,
)
if err != nil {
t.Fatalf("Failed to construct remote relay candidate: %s", err)
}
srflxRemote, err := NewCandidateServerReflexive(
"udp",
"10.10.10.2", 19218,
1,
"4.3.2.1", 43212,
)
if err != nil {
t.Fatalf("Failed to construct remote srflx candidate: %s", err)
}
prflxRemote, err := NewCandidatePeerReflexive(
"udp",
"10.10.10.2", 19217,
1,
"4.3.2.1", 43211,
)
if err != nil {
t.Fatalf("Failed to construct remote prflx candidate: %s", err)
}
hostRemote, err := NewCandidateHost(
"udp",
"1.2.3.5", 12350,
1,
)
if err != nil {
t.Fatalf("Failed to construct remote host candidate: %s", err)
}
a.remoteCandidates[NetworkTypeUDP4] = []Candidate{relayRemote, srflxRemote, prflxRemote, hostRemote}
remoteStats := a.GetRemoteCandidatesStats()
if len(remoteStats) != 4 {
t.Fatalf("expected 4 remote candidates stats, got %d instead", len(remoteStats))
}
var relayRemoteStat, srflxRemoteStat, prflxRemoteStat, hostRemoteStat CandidateStats
for _, stats := range remoteStats {
var candidate Candidate
switch stats.ID {
case relayRemote.ID():
relayRemoteStat = stats
candidate = relayRemote
case srflxRemote.ID():
srflxRemoteStat = stats
candidate = srflxRemote
case prflxRemote.ID():
prflxRemoteStat = stats
candidate = prflxRemote
case hostRemote.ID():
hostRemoteStat = stats
candidate = hostRemote
default:
t.Fatal("invalid remote candidate ID")
}
if stats.CandidateType != candidate.Type() {
t.Fatal("invalid stats CandidateType")
}
if stats.Priority != candidate.Priority() {
t.Fatal("invalid stats CandidateType")
}
if stats.IP != candidate.Address() {
t.Fatal("invalid stats IP")
}
}
if relayRemoteStat.ID != relayRemote.ID() {
t.Fatal("missing relay remote stat")
}
if srflxRemoteStat.ID != srflxRemote.ID() {
t.Fatal("missing srflx remote stat")
}
if prflxRemoteStat.ID != prflxRemote.ID() {
t.Fatal("missing prflx remote stat")
}
if hostRemoteStat.ID != hostRemote.ID() {
t.Fatal("missing host remote stat")
}
if err := a.Close(); err != nil {
t.Fatalf("Error on agent.Close(): %s", err)
}
}
+1
View File
@@ -17,6 +17,7 @@ const (
// Candidate represents an ICE candidate
type Candidate interface {
ID() string
Component() uint16
Address() string
LastReceived() time.Time
+6
View File
@@ -11,6 +11,7 @@ import (
)
type candidateBase struct {
id string
networkType NetworkType
candidateType CandidateType
@@ -31,6 +32,11 @@ type candidateBase struct {
closedCh chan struct{}
}
// ID returns Candidate ID
func (c *candidateBase) ID() string {
return c.id
}
// Address returns Candidate Address
func (c *candidateBase) Address() string {
return c.address
+6
View File
@@ -14,8 +14,14 @@ type CandidateHost struct {
// NewCandidateHost creates a new host candidate
func NewCandidateHost(network string, address string, port int, component uint16) (*CandidateHost, error) {
candidateID, err := generateCandidateID()
if err != nil {
return nil, err
}
c := &CandidateHost{
candidateBase: candidateBase{
id: candidateID,
address: address,
candidateType: CandidateTypeHost,
component: component,
+6
View File
@@ -19,8 +19,14 @@ func NewCandidatePeerReflexive(network string, address string, port int, compone
return nil, err
}
candidateID, err := generateCandidateID()
if err != nil {
return nil, err
}
return &CandidatePeerReflexive{
candidateBase: candidateBase{
id: candidateID,
networkType: networkType,
candidateType: CandidateTypePeerReflexive,
address: address,
+6
View File
@@ -29,8 +29,14 @@ func NewCandidateRelay(network string, address string, port int, component uint1
return nil, err
}
candidateID, err := generateCandidateID()
if err != nil {
return nil, err
}
return &CandidateRelay{
candidateBase: candidateBase{
id: candidateID,
networkType: networkType,
candidateType: CandidateTypeRelay,
address: address,
+6
View File
@@ -19,8 +19,14 @@ func NewCandidateServerReflexive(network string, address string, port int, compo
return nil, err
}
candidateID, err := generateCandidateID()
if err != nil {
return nil, err
}
return &CandidateServerReflexive{
candidateBase: candidateBase{
id: candidateID,
networkType: networkType,
candidateType: CandidateTypeServerReflexive,
address: address,
+1 -2
View File
@@ -2,7 +2,6 @@ package ice
import (
"crypto/rand"
"fmt"
)
// MulticastDNSMode represents the different Multicast modes ICE can run in
@@ -28,5 +27,5 @@ func generateMulticastDNSName() (string, error) {
return "", err
}
return fmt.Sprintf("%X-%X-%X-%X-%X.local", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), nil
return generateRandString("", ".local")
}
+177
View File
@@ -0,0 +1,177 @@
package ice
import (
"time"
)
// CandidatePairStats contains ICE candidate pair statistics
type CandidatePairStats struct {
// Timestamp is the timestamp associated with this object.
Timestamp time.Time
// LocalCandidateID is the ID of the local candidate
LocalCandidateID string
// RemoteCandidateID is the ID of the remote candidate
RemoteCandidateID string
// State represents the state of the checklist for the local and remote
// candidates in a pair.
State CandidatePairState
// Nominated is true when this valid pair that should be used for media
// if it is the highest-priority one amongst those whose nominated flag is set
Nominated bool
// PacketsSent represents the total number of packets sent on this candidate pair.
PacketsSent uint32
// PacketsReceived represents the total number of packets received on this candidate pair.
PacketsReceived uint32
// BytesSent represents the total number of payload bytes sent on this candidate pair
// not including headers or padding.
BytesSent uint64
// BytesReceived represents the total number of payload bytes received on this candidate pair
// not including headers or padding.
BytesReceived uint64
// LastPacketSentTimestamp represents the timestamp at which the last packet was
// sent on this particular candidate pair, excluding STUN packets.
LastPacketSentTimestamp time.Time
// LastPacketReceivedTimestamp represents the timestamp at which the last packet
// was received on this particular candidate pair, excluding STUN packets.
LastPacketReceivedTimestamp time.Time
// FirstRequestTimestamp represents the timestamp at which the first STUN request
// was sent on this particular candidate pair.
FirstRequestTimestamp time.Time
// LastRequestTimestamp represents the timestamp at which the last STUN request
// was sent on this particular candidate pair. The average interval between two
// consecutive connectivity checks sent can be calculated with
// (LastRequestTimestamp - FirstRequestTimestamp) / RequestsSent.
LastRequestTimestamp time.Time
// LastResponseTimestamp represents the timestamp at which the last STUN response
// was received on this particular candidate pair.
LastResponseTimestamp 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
// that are sent in order to verify consent. The average round trip time can
// be computed from TotalRoundTripTime by dividing it by ResponsesReceived.
TotalRoundTripTime float64
// CurrentRoundTripTime represents the latest round trip time measured in seconds,
// computed from both STUN connectivity checks, including those that are sent
// for consent verification.
CurrentRoundTripTime float64
// AvailableOutgoingBitrate is calculated by the underlying congestion control
// by combining the available bitrate for all the outgoing RTP streams using
// this candidate pair. The bitrate measurement does not count the size of the
// IP or other transport layers like TCP or UDP. It is similar to the TIAS defined
// in RFC 3890, i.e., it is measured in bits per second and the bitrate is calculated
// over a 1 second window.
AvailableOutgoingBitrate float64
// AvailableIncomingBitrate is calculated by the underlying congestion control
// by combining the available bitrate for all the incoming RTP streams using
// this candidate pair. The bitrate measurement does not count the size of the
// IP or other transport layers like TCP or UDP. It is similar to the TIAS defined
// in RFC 3890, i.e., it is measured in bits per second and the bitrate is
// calculated over a 1 second window.
AvailableIncomingBitrate float64
// CircuitBreakerTriggerCount represents the number of times the circuit breaker
// is triggered for this particular 5-tuple, ceasing transmission.
CircuitBreakerTriggerCount uint32
// RequestsReceived represents the total number of connectivity check requests
// received (including retransmissions). It is impossible for the receiver to
// tell whether the request was sent in order to check connectivity or check
// consent, so all connectivity checks requests are counted here.
RequestsReceived uint64
// RequestsSent represents the total number of connectivity check requests
// sent (not including retransmissions).
RequestsSent uint64
// ResponsesReceived represents the total number of connectivity check responses received.
ResponsesReceived uint64
// ResponsesSent epresents the total number of connectivity check responses sent.
// Since we cannot distinguish connectivity check requests and consent requests,
// all responses are counted.
ResponsesSent uint64
// RetransmissionsReceived represents the total number of connectivity check
// request retransmissions received.
RetransmissionsReceived uint64
// RetransmissionsSent represents the total number of connectivity check
// request retransmissions sent.
RetransmissionsSent uint64
// ConsentRequestsSent represents the total number of consent requests sent.
ConsentRequestsSent uint64
// ConsentExpiredTimestamp represents the timestamp at which the latest valid
// STUN binding response expired.
ConsentExpiredTimestamp time.Time
}
// CandidateStats contains ICE candidate statistics related to the ICETransport objects.
type CandidateStats struct {
// Timestamp is the timestamp associated with this object.
Timestamp time.Time
// ID is the candidate ID
ID string
// NetworkType represents the type of network interface used by the base of a
// local candidate (the address the ICE agent sends from). Only present for
// local candidates; it's not possible to know what type of network interface
// a remote candidate is using.
//
// Note:
// This stat only tells you about the network interface used by the first "hop";
// it's possible that a connection will be bottlenecked by another type of network.
// For example, when using Wi-Fi tethering, the networkType of the relevant candidate
// would be "wifi", even when the next hop is over a cellular connection.
NetworkType NetworkType
// IP is the IP address of the candidate, allowing for IPv4 addresses and
// IPv6 addresses, but fully qualified domain names (FQDNs) are not allowed.
IP string
// Port is the port number of the candidate.
Port int
// CandidateType is the "Type" field of the ICECandidate.
CandidateType CandidateType
// Priority is the "Priority" field of the ICECandidate.
Priority uint32
// URL is the URL of the TURN or STUN server indicated in the that translated
// this IP address. It is the URL address surfaced in an PeerConnectionICEEvent.
URL string
// RelayProtocol is the protocol used by the endpoint to communicate with the
// TURN server. This is only present for local candidates. Valid values for
// the TURN URL protocol is one of udp, tcp, or tls.
RelayProtocol string
// Deleted is true if the candidate has been deleted/freed. For host candidates,
// this means that any network resources (typically a socket) associated with the
// candidate have been released. For TURN candidates, this means the TURN allocation
// is no longer active.
//
// Only defined for local candidates. For remote candidates, this property is not applicable.
Deleted bool
}
+16
View File
@@ -1,6 +1,7 @@
package ice
import (
"fmt"
"math/rand"
"net"
"sync/atomic"
@@ -73,3 +74,18 @@ func addrEqual(a, b net.Addr) bool {
return aType == bType && aIP.Equal(bIP) && aPort == bPort
}
func generateCandidateID() (string, error) {
return generateRandString("candidate:", "")
}
func generateRandString(prefix, sufix string) (string, error) {
b := make([]byte, 16)
_, err := rand.Read(b) //nolint
if err != nil {
return "", err
}
return fmt.Sprintf("%s%X-%X-%X-%X-%X%s", prefix, b[0:4], b[4:6], b[6:8], b[8:10], b[10:], sufix), nil
}