diff --git a/api/stats/rtcstatsreport.h b/api/stats/rtcstatsreport.h index ee56b35d06..f2267d6c68 100644 --- a/api/stats/rtcstatsreport.h +++ b/api/stats/rtcstatsreport.h @@ -63,6 +63,9 @@ class RTCStatsReport : public rtc::RefCountInterface { const RTCStats* Get(const std::string& id) const; size_t size() const { return stats_.size(); } + // Removes the stats object from the report, returning ownership of it or null + // if there is no object with |id|. + std::unique_ptr Take(const std::string& id); // Takes ownership of all the stats in |victim|, leaving it empty. void TakeMembersFrom(rtc::scoped_refptr victim); diff --git a/pc/BUILD.gn b/pc/BUILD.gn index 334d1af01d..54bf0c7070 100644 --- a/pc/BUILD.gn +++ b/pc/BUILD.gn @@ -150,6 +150,8 @@ rtc_static_library("peerconnection") { "remoteaudiosource.h", "rtcstatscollector.cc", "rtcstatscollector.h", + "rtcstatstraversal.cc", + "rtcstatstraversal.h", "rtpreceiver.cc", "rtpreceiver.h", "rtpsender.cc", @@ -436,6 +438,7 @@ if (rtc_include_tests) { "proxy_unittest.cc", "rtcstats_integrationtest.cc", "rtcstatscollector_unittest.cc", + "rtcstatstraversal_unittest.cc", "rtpmediautils_unittest.cc", "rtpsenderreceiver_unittest.cc", "sctputils_unittest.cc", diff --git a/pc/rtcstats_integrationtest.cc b/pc/rtcstats_integrationtest.cc index 374c048b1f..40d07f7607 100644 --- a/pc/rtcstats_integrationtest.cc +++ b/pc/rtcstats_integrationtest.cc @@ -8,6 +8,7 @@ * be found in the AUTHORS file in the root of the source tree. */ +#include #include #include @@ -17,6 +18,7 @@ #include "api/peerconnectioninterface.h" #include "api/stats/rtcstats_objects.h" #include "api/stats/rtcstatsreport.h" +#include "pc/rtcstatstraversal.h" #include "pc/test/peerconnectiontestwrapper.h" #include "pc/test/rtcstatsobtainer.h" #include "rtc_base/checks.h" @@ -24,6 +26,7 @@ #include "rtc_base/gunit.h" #include "rtc_base/refcountedobject.h" #include "rtc_base/scoped_ref_ptr.h" +#include "rtc_base/stringutils.h" #include "rtc_base/trace_event.h" #include "rtc_base/virtualsocketserver.h" @@ -741,6 +744,52 @@ TEST_F(RTCStatsIntegrationTest, GetsStatsWhileDestroyingPeerConnections) { EXPECT_EQ(stats_obtainer->report()->ToJson(), RTCStatsReportTraceListener::last_trace()); } + +// GetStatsReferencedIds() is optimized to recognize what is or isn't a +// referenced ID based on dictionary type information and knowing what members +// are used as references, as opposed to iterating all members to find the ones +// with the "Id" or "Ids" suffix. As such, GetStatsReferencedIds() is tested as +// an integration test instead of a unit test in order to guard against adding +// new references and forgetting to update GetStatsReferencedIds(). +TEST_F(RTCStatsIntegrationTest, GetStatsReferencedIds) { + StartCall(); + + rtc::scoped_refptr report = GetStatsFromCallee(); + for (const RTCStats& stats : *report) { + // Find all references by looking at all string members with the "Id" or + // "Ids" suffix. + std::set expected_ids; + for (const auto* member : stats.Members()) { + if (!member->is_defined()) + continue; + if (member->type() == RTCStatsMemberInterface::kString) { + if (rtc::ends_with(member->name(), "Id")) { + const auto& id = member->cast_to>(); + expected_ids.insert(&(*id)); + } + } else if (member->type() == RTCStatsMemberInterface::kSequenceString) { + if (rtc::ends_with(member->name(), "Ids")) { + const auto& ids = + member->cast_to>>(); + for (const std::string& id : *ids) + expected_ids.insert(&id); + } + } + } + + std::vector neighbor_ids = GetStatsReferencedIds(stats); + EXPECT_EQ(neighbor_ids.size(), expected_ids.size()); + for (const std::string* neighbor_id : neighbor_ids) { + EXPECT_TRUE(expected_ids.find(neighbor_id) != expected_ids.end()) + << "Unexpected neighbor ID: " << *neighbor_id; + } + for (const std::string* expected_id : expected_ids) { + EXPECT_TRUE(std::find(neighbor_ids.begin(), neighbor_ids.end(), + expected_id) != neighbor_ids.end()) + << "Missing expected neighbor ID: " << *expected_id; + } + } +} #endif // HAVE_SCTP } // namespace diff --git a/pc/rtcstatstraversal.cc b/pc/rtcstatstraversal.cc new file mode 100644 index 0000000000..2b74beed6f --- /dev/null +++ b/pc/rtcstatstraversal.cc @@ -0,0 +1,119 @@ +/* + * Copyright 2018 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "pc/rtcstatstraversal.h" + +#include +#include +#include +#include + +#include "api/stats/rtcstats_objects.h" +#include "rtc_base/checks.h" + +namespace webrtc { + +namespace { + +void TraverseAndTakeVisitedStats(RTCStatsReport* report, + RTCStatsReport* visited_report, + const std::string& current_id) { + // Mark current stats object as visited by moving it |report| to + // |visited_report|. + std::unique_ptr current = report->Take(current_id); + if (!current) { + // This node has already been visited (or it is an invalid id). + return; + } + std::vector neighbor_ids = + GetStatsReferencedIds(*current); + visited_report->AddStats(std::move(current)); + + // Recursively traverse all neighbors. + for (const auto* neighbor_id : neighbor_ids) { + TraverseAndTakeVisitedStats(report, visited_report, *neighbor_id); + } +} + +void AddIdIfDefined(const RTCStatsMember& id, + std::vector* neighbor_ids) { + if (id.is_defined()) + neighbor_ids->push_back(&(*id)); +} + +void AddIdsIfDefined(const RTCStatsMember>& ids, + std::vector* neighbor_ids) { + if (ids.is_defined()) { + for (const std::string& id : *ids) + neighbor_ids->push_back(&id); + } +} + +} // namespace + +rtc::scoped_refptr TakeReferencedStats( + rtc::scoped_refptr report, + const std::vector& ids) { + rtc::scoped_refptr result = RTCStatsReport::Create(); + for (const auto& id : ids) { + TraverseAndTakeVisitedStats(report.get(), result.get(), id); + } + return result; +} + +std::vector GetStatsReferencedIds(const RTCStats& stats) { + std::vector neighbor_ids; + const char* type = stats.type(); + if (type == RTCCertificateStats::kType) { + const auto& certificate = static_cast(stats); + AddIdIfDefined(certificate.issuer_certificate_id, &neighbor_ids); + } else if (type == RTCCodecStats::kType) { + // RTCCodecStats does not have any neighbor references. + } else if (type == RTCDataChannelStats::kType) { + // RTCDataChannelStats does not have any neighbor references. + } else if (type == RTCIceCandidatePairStats::kType) { + const auto& candidate_pair = + static_cast(stats); + AddIdIfDefined(candidate_pair.transport_id, &neighbor_ids); + AddIdIfDefined(candidate_pair.local_candidate_id, &neighbor_ids); + AddIdIfDefined(candidate_pair.remote_candidate_id, &neighbor_ids); + } else if (type == RTCLocalIceCandidateStats::kType || + type == RTCRemoteIceCandidateStats::kType) { + const auto& local_or_remote_candidate = + static_cast(stats); + AddIdIfDefined(local_or_remote_candidate.transport_id, &neighbor_ids); + } else if (type == RTCMediaStreamStats::kType) { + const auto& stream = static_cast(stats); + AddIdsIfDefined(stream.track_ids, &neighbor_ids); + } else if (type == RTCMediaStreamTrackStats::kType) { + // RTCMediaStreamTrackStats does not have any neighbor references. + } else if (type == RTCPeerConnectionStats::kType) { + // RTCPeerConnectionStats does not have any neighbor references. + } else if (type == RTCInboundRTPStreamStats::kType || + type == RTCOutboundRTPStreamStats::kType) { + const auto& rtp = static_cast(stats); + AddIdIfDefined(rtp.associate_stats_id, &neighbor_ids); + AddIdIfDefined(rtp.track_id, &neighbor_ids); + AddIdIfDefined(rtp.transport_id, &neighbor_ids); + AddIdIfDefined(rtp.codec_id, &neighbor_ids); + } else if (type == RTCTransportStats::kType) { + // RTCTransportStats does not have any neighbor references. + const auto& transport = static_cast(stats); + AddIdIfDefined(transport.rtcp_transport_stats_id, &neighbor_ids); + AddIdIfDefined(transport.selected_candidate_pair_id, &neighbor_ids); + AddIdIfDefined(transport.local_certificate_id, &neighbor_ids); + AddIdIfDefined(transport.remote_certificate_id, &neighbor_ids); + } else { + RTC_NOTREACHED() << "Unrecognized type: " << type; + } + return neighbor_ids; +} + +} // namespace webrtc diff --git a/pc/rtcstatstraversal.h b/pc/rtcstatstraversal.h new file mode 100644 index 0000000000..1c50ae46ac --- /dev/null +++ b/pc/rtcstatstraversal.h @@ -0,0 +1,43 @@ +/* + * Copyright 2018 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef PC_RTCSTATSTRAVERSAL_H_ +#define PC_RTCSTATSTRAVERSAL_H_ + +#include +#include + +#include "api/stats/rtcstatsreport.h" +#include "rtc_base/scoped_ref_ptr.h" + +namespace webrtc { + +// Traverses the stats graph, taking all stats objects that are directly or +// indirectly accessible from and including the stats objects identified by +// |ids|, returning them as a new stats report. +// This is meant to be used to implement the stats selection algorithm. +// https://w3c.github.io/webrtc-pc/#dfn-stats-selection-algorithm +rtc::scoped_refptr TakeReferencedStats( + rtc::scoped_refptr report, + const std::vector& ids); + +// Gets pointers to the string values of any members in |stats| that are used as +// references for looking up other stats objects in the same report by ID. The +// pointers are valid for the lifetime of |stats| assumings its members are not +// modified. +// +// For example, RTCCodecStats contains "transportId" +// (RTCCodecStats::transport_id) referencing an RTCTransportStats. +// https://w3c.github.io/webrtc-stats/#dom-rtccodecstats-transportid +std::vector GetStatsReferencedIds(const RTCStats& stats); + +} // namespace webrtc + +#endif // PC_RTCSTATSTRAVERSAL_H_ diff --git a/pc/rtcstatstraversal_unittest.cc b/pc/rtcstatstraversal_unittest.cc new file mode 100644 index 0000000000..0552964023 --- /dev/null +++ b/pc/rtcstatstraversal_unittest.cc @@ -0,0 +1,208 @@ +/* + * Copyright 2018 The WebRTC Project Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "pc/rtcstatstraversal.h" + +#include +#include +#include + +#include "api/stats/rtcstats_objects.h" +#include "rtc_base/checks.h" +#include "rtc_base/gunit.h" + +// This file contains tests for TakeReferencedStats(). +// GetStatsNeighborIds() is tested in rtcstats_integrationtest.cc. + +namespace webrtc { + +class RTCStatsTraversalTest : public testing::Test { + public: + RTCStatsTraversalTest() { + transport_ = new RTCTransportStats("transport", 0); + candidate_pair_ = new RTCIceCandidatePairStats("candidate-pair", 0); + local_candidate_ = new RTCLocalIceCandidateStats("local-candidate", 0); + remote_candidate_ = new RTCRemoteIceCandidateStats("remote-candidate", 0); + initial_report_ = RTCStatsReport::Create(0); + initial_report_->AddStats(std::unique_ptr(transport_)); + initial_report_->AddStats(std::unique_ptr(candidate_pair_)); + initial_report_->AddStats( + std::unique_ptr(local_candidate_)); + initial_report_->AddStats( + std::unique_ptr(remote_candidate_)); + result_ = RTCStatsReport::Create(0); + } + + void TakeReferencedStats(std::vector start_nodes) { + std::vector start_ids; + for (const RTCStats* start_node : start_nodes) { + start_ids.push_back(start_node->id()); + } + result_ = webrtc::TakeReferencedStats(initial_report_, start_ids); + } + + void EXPECT_VISITED(const RTCStats* stats) { + EXPECT_FALSE(initial_report_->Get(stats->id())) + << '"' << stats->id() + << "\" should be visited but it was not removed from initial report."; + EXPECT_TRUE(result_->Get(stats->id())) + << '"' << stats->id() + << "\" should be visited but it was not added to the resulting report."; + } + + void EXPECT_UNVISITED(const RTCStats* stats) { + EXPECT_TRUE(initial_report_->Get(stats->id())) + << '"' << stats->id() + << "\" should not be visited but it was removed from initial report."; + EXPECT_FALSE(result_->Get(stats->id())) + << '"' << stats->id() + << "\" should not be visited but it was added to the resulting report."; + } + + protected: + rtc::scoped_refptr initial_report_; + rtc::scoped_refptr result_; + // Raw pointers to stats owned by the reports. + RTCTransportStats* transport_; + RTCIceCandidatePairStats* candidate_pair_; + RTCIceCandidateStats* local_candidate_; + RTCIceCandidateStats* remote_candidate_; +}; + +TEST_F(RTCStatsTraversalTest, NoReachableConnections) { + // Everything references transport but transport doesn't reference anything. + // + // candidate-pair + // | | | + // v | v + // local-candidate | remote-candidate + // | | | + // v v v + // start:transport + candidate_pair_->transport_id = "transport"; + candidate_pair_->local_candidate_id = "local-candidate"; + candidate_pair_->remote_candidate_id = "remote-candidate"; + local_candidate_->transport_id = "transport"; + remote_candidate_->transport_id = "transport"; + TakeReferencedStats({transport_}); + EXPECT_VISITED(transport_); + EXPECT_UNVISITED(candidate_pair_); + EXPECT_UNVISITED(local_candidate_); + EXPECT_UNVISITED(remote_candidate_); +} + +TEST_F(RTCStatsTraversalTest, SelfReference) { + transport_->rtcp_transport_stats_id = "transport"; + TakeReferencedStats({transport_}); + EXPECT_VISITED(transport_); + EXPECT_UNVISITED(candidate_pair_); + EXPECT_UNVISITED(local_candidate_); + EXPECT_UNVISITED(remote_candidate_); +} + +TEST_F(RTCStatsTraversalTest, BogusReference) { + transport_->rtcp_transport_stats_id = "bogus-reference"; + TakeReferencedStats({transport_}); + EXPECT_VISITED(transport_); + EXPECT_UNVISITED(candidate_pair_); + EXPECT_UNVISITED(local_candidate_); + EXPECT_UNVISITED(remote_candidate_); +} + +TEST_F(RTCStatsTraversalTest, Tree) { + // start:candidate-pair + // | | + // v v + // local-candidate remote-candidate + // | + // v + // transport + candidate_pair_->local_candidate_id = "local-candidate"; + candidate_pair_->remote_candidate_id = "remote-candidate"; + local_candidate_->transport_id = "transport"; + TakeReferencedStats({candidate_pair_}); + EXPECT_VISITED(transport_); + EXPECT_VISITED(candidate_pair_); + EXPECT_VISITED(local_candidate_); + EXPECT_VISITED(remote_candidate_); +} + +TEST_F(RTCStatsTraversalTest, MultiplePathsToSameNode) { + // start:candidate-pair + // | | + // v v + // local-candidate remote-candidate + // | | + // v v + // transport + candidate_pair_->local_candidate_id = "local-candidate"; + candidate_pair_->remote_candidate_id = "remote-candidate"; + local_candidate_->transport_id = "transport"; + remote_candidate_->transport_id = "transport"; + TakeReferencedStats({candidate_pair_}); + EXPECT_VISITED(transport_); + EXPECT_VISITED(candidate_pair_); + EXPECT_VISITED(local_candidate_); + EXPECT_VISITED(remote_candidate_); +} + +TEST_F(RTCStatsTraversalTest, CyclicGraph) { + // candidate-pair + // | ^ + // v | + // start:local-candidate | remote-candidate + // | | + // v | + // transport + local_candidate_->transport_id = "transport"; + transport_->selected_candidate_pair_id = "candidate-pair"; + candidate_pair_->local_candidate_id = "local-candidate"; + TakeReferencedStats({local_candidate_}); + EXPECT_VISITED(transport_); + EXPECT_VISITED(candidate_pair_); + EXPECT_VISITED(local_candidate_); + EXPECT_UNVISITED(remote_candidate_); +} + +TEST_F(RTCStatsTraversalTest, MultipleStarts) { + // start:candidate-pair + // | + // v + // local-candidate remote-candidate + // | + // v + // start:transport + candidate_pair_->remote_candidate_id = "remote-candidate"; + local_candidate_->transport_id = "transport"; + TakeReferencedStats({candidate_pair_, transport_}); + EXPECT_VISITED(transport_); + EXPECT_VISITED(candidate_pair_); + EXPECT_UNVISITED(local_candidate_); + EXPECT_VISITED(remote_candidate_); +} + +TEST_F(RTCStatsTraversalTest, MultipleStartsLeadingToSameNode) { + // candidate-pair + // + // + // start:local-candidate start:remote-candidate + // | | + // v v + // transport + local_candidate_->transport_id = "transport"; + remote_candidate_->transport_id = "transport"; + TakeReferencedStats({local_candidate_, remote_candidate_}); + EXPECT_VISITED(transport_); + EXPECT_UNVISITED(candidate_pair_); + EXPECT_VISITED(local_candidate_); + EXPECT_VISITED(remote_candidate_); +} + +} // namespace webrtc diff --git a/stats/rtcstatsreport.cc b/stats/rtcstatsreport.cc index 90025f7120..55e4d3d4af 100644 --- a/stats/rtcstatsreport.cc +++ b/stats/rtcstatsreport.cc @@ -84,6 +84,15 @@ const RTCStats* RTCStatsReport::Get(const std::string& id) const { return nullptr; } +std::unique_ptr RTCStatsReport::Take(const std::string& id) { + StatsMap::iterator it = stats_.find(id); + if (it == stats_.end()) + return nullptr; + std::unique_ptr stats = std::move(it->second); + stats_.erase(it); + return stats; +} + void RTCStatsReport::TakeMembersFrom( rtc::scoped_refptr victim) { for (StatsMap::iterator it = victim->stats_.begin(); diff --git a/stats/rtcstatsreport_unittest.cc b/stats/rtcstatsreport_unittest.cc index 5483ee3083..809cae1d24 100644 --- a/stats/rtcstatsreport_unittest.cc +++ b/stats/rtcstatsreport_unittest.cc @@ -110,6 +110,19 @@ TEST(RTCStatsReport, StatsOrder) { EXPECT_EQ(i, static_cast(7)); } +TEST(RTCStatsReport, Take) { + rtc::scoped_refptr report = RTCStatsReport::Create(0); + report->AddStats(std::unique_ptr(new RTCTestStats1("A", 1))); + report->AddStats(std::unique_ptr(new RTCTestStats1("B", 2))); + EXPECT_TRUE(report->Get("A")); + EXPECT_EQ(report->size(), 2u); + auto a = report->Take("A"); + EXPECT_TRUE(a); + EXPECT_EQ(report->size(), 1u); + EXPECT_FALSE(report->Get("A")); + EXPECT_FALSE(report->Take("A")); +} + TEST(RTCStatsReport, TakeMembersFrom) { rtc::scoped_refptr a = RTCStatsReport::Create(1337); EXPECT_EQ(a->timestamp_us(), 1337u);