From b619936dee2807ca99ae2ed76956997bd2b43d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Bostr=C3=B6m?= Date: Mon, 12 Mar 2018 10:27:55 +0100 Subject: [PATCH] Stats traversal algorithm added. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is part of the work to add a selector argument to getStats(). Changes: - TakeReferencedStats() added, which traverses the stats graph and takes any stats from the report that are directly or indirectly accessible from the starting stats objects in the stats graph. The result is returned as a stats report. - GetStatsReferencedIds(), an efficient helper function for getting neighbor stats object IDs. - RTCStatsReport::Take(), removed the stats object with the given ID and returns ownership of it (so that it can be added to another report). TakeReferencedStats() is tested with a bunch of sample stats graphs. GetStatsReferencedIds() is tested in the rtcstats_integrationttest.cc, making sure the expected IDs are returned. The expected IDs are the values of the stats object members with the "Id" or "Ids" suffix. Design doc: https://docs.google.com/document/d/18BywbtXgHCjsbR5nWBedpzqDjAfXrFSTJNiADnzoK0w/edit?usp=sharing Bug: chromium:680172 Change-Id: I5da9da8250da0cb05adb864015901393a4290776 Reviewed-on: https://webrtc-review.googlesource.com/60869 Commit-Queue: Henrik Boström Reviewed-by: Taylor Brandstetter Cr-Commit-Position: refs/heads/master@{#22381} --- api/stats/rtcstatsreport.h | 3 + pc/BUILD.gn | 3 + pc/rtcstats_integrationtest.cc | 49 ++++++++ pc/rtcstatstraversal.cc | 119 ++++++++++++++++++ pc/rtcstatstraversal.h | 43 +++++++ pc/rtcstatstraversal_unittest.cc | 208 +++++++++++++++++++++++++++++++ stats/rtcstatsreport.cc | 9 ++ stats/rtcstatsreport_unittest.cc | 13 ++ 8 files changed, 447 insertions(+) create mode 100644 pc/rtcstatstraversal.cc create mode 100644 pc/rtcstatstraversal.h create mode 100644 pc/rtcstatstraversal_unittest.cc 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);