From 7de076215c2865914bfdf4166ee2aa6a2d206ab2 Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Mon, 7 Oct 2024 12:38:39 +0530 Subject: [PATCH] Add ability to get selected candidate pair stats (#735) It is useful to have stats from just the selected pair as a lightweight option where a lot of agents are running, for example, an SFU. lint Switch udp_mux_test to use sha256 instead of sha1 (#733) Minor change to this test to stop using sha1 and remove the linter exceptions. Co-authored-by: Daniel Kessler Update module golang.org/x/net to v0.29.0 Generated by renovateBot Update module github.com/pion/dtls/v3 to v3.0.3 Generated by renovateBot --- agent_stats.go | 50 ++++++++++++++++++++++++++++ agent_test.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/agent_stats.go b/agent_stats.go index 4366beeb..9344ec3a 100644 --- a/agent_stats.go +++ b/agent_stats.go @@ -54,6 +54,56 @@ func (a *Agent) GetCandidatePairsStats() []CandidatePairStats { return res } +// GetSelectedCandidatePairStats returns a candidate pair stats for selected candidate pair. +// Returns false if there is no selected pair +func (a *Agent) GetSelectedCandidatePairStats() (CandidatePairStats, bool) { + isAvailable := false + var res CandidatePairStats + err := a.run(a.context(), func(ctx context.Context, agent *Agent) { + sp := agent.getSelectedPair() + if sp == nil { + return + } + + isAvailable = true + res = CandidatePairStats{ + Timestamp: time.Now(), + LocalCandidateID: sp.Local.ID(), + RemoteCandidateID: sp.Remote.ID(), + State: sp.state, + Nominated: sp.nominated, + // PacketsSent uint32 + // PacketsReceived uint32 + // BytesSent uint64 + // BytesReceived uint64 + // LastPacketSentTimestamp time.Time + // LastPacketReceivedTimestamp time.Time + // FirstRequestTimestamp time.Time + // LastRequestTimestamp time.Time + // LastResponseTimestamp time.Time + TotalRoundTripTime: sp.TotalRoundTripTime(), + CurrentRoundTripTime: sp.CurrentRoundTripTime(), + // AvailableOutgoingBitrate float64 + // AvailableIncomingBitrate float64 + // CircuitBreakerTriggerCount uint32 + // RequestsReceived uint64 + // RequestsSent uint64 + ResponsesReceived: sp.ResponsesReceived(), + // ResponsesSent uint64 + // RetransmissionsReceived uint64 + // RetransmissionsSent uint64 + // ConsentRequestsSent uint64 + // ConsentExpiredTimestamp time.Time + } + }) + if err != nil { + a.log.Errorf("Failed to get selected candidate pair stats: %v", err) + return CandidatePairStats{}, false + } + + return res, isAvailable +} + // GetLocalCandidatesStats returns a list of local candidates stats func (a *Agent) GetLocalCandidatesStats() []CandidateStats { var res []CandidateStats diff --git a/agent_test.go b/agent_test.go index bbee7f9a..5941773e 100644 --- a/agent_test.go +++ b/agent_test.go @@ -621,9 +621,8 @@ func TestInvalidGather(t *testing.T) { }) } -func TestCandidatePairStats(t *testing.T) { - report := test.CheckRoutines(t) - defer report() +func TestCandidatePairsStats(t *testing.T) { + defer test.CheckRoutines(t)() // Avoid deadlocks? defer test.TimeOut(1 * time.Second).Stop() @@ -775,6 +774,89 @@ func TestCandidatePairStats(t *testing.T) { assert.NoError(t, a.Close()) } +func TestSelectedCandidatePairStats(t *testing.T) { + defer test.CheckRoutines(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) + } + defer func() { + require.NoError(t, a.Close()) + }() + + hostConfig := &CandidateHostConfig{ + Network: "udp", + Address: "192.168.1.1", + Port: 19216, + Component: 1, + } + hostLocal, err := NewCandidateHost(hostConfig) + if err != nil { + t.Fatalf("Failed to construct local host candidate: %s", err) + } + + srflxConfig := &CandidateServerReflexiveConfig{ + Network: "udp", + Address: "10.10.10.2", + Port: 19218, + Component: 1, + RelAddr: "4.3.2.1", + RelPort: 43212, + } + srflxRemote, err := NewCandidateServerReflexive(srflxConfig) + if err != nil { + t.Fatalf("Failed to construct remote srflx candidate: %s", err) + } + + // no selected pair, should return not available + _, ok := a.GetSelectedCandidatePairStats() + require.False(t, ok) + + // add pair and populate some RTT stats + p := a.findPair(hostLocal, srflxRemote) + if p == nil { + a.addPair(hostLocal, srflxRemote) + p = a.findPair(hostLocal, srflxRemote) + } + for i := 0; i < 10; i++ { + p.UpdateRoundTripTime(time.Duration(i+1) * time.Second) + } + + // set the pair as selected + a.setSelectedPair(p) + + stats, ok := a.GetSelectedCandidatePairStats() + require.True(t, ok) + + if stats.LocalCandidateID != hostLocal.ID() { + t.Fatal("invalid local candidate id") + } + if stats.RemoteCandidateID != srflxRemote.ID() { + t.Fatal("invalid remote candidate id") + } + + expectedCurrentRoundTripTime := time.Duration(10) * time.Second + if stats.CurrentRoundTripTime != expectedCurrentRoundTripTime.Seconds() { + t.Fatalf("expected current round trip time to be %f, it is %f instead", + expectedCurrentRoundTripTime.Seconds(), stats.CurrentRoundTripTime) + } + + expectedTotalRoundTripTime := time.Duration(55) * time.Second + if stats.TotalRoundTripTime != expectedTotalRoundTripTime.Seconds() { + t.Fatalf("expected total round trip time to be %f, it is %f instead", + expectedTotalRoundTripTime.Seconds(), stats.TotalRoundTripTime) + } + + if stats.ResponsesReceived != 10 { + t.Fatalf("expected responses received to be 10, it is %d instead", + stats.ResponsesReceived) + } +} + func TestLocalCandidateStats(t *testing.T) { report := test.CheckRoutines(t) defer report()