diff --git a/api/stats/rtcstats_objects.h b/api/stats/rtcstats_objects.h index 64bfa190f6..ff99df53a3 100644 --- a/api/stats/rtcstats_objects.h +++ b/api/stats/rtcstats_objects.h @@ -74,6 +74,14 @@ struct RTCNetworkType { static const char* const kUnknown; }; +// https://w3c.github.io/webrtc-stats/#dom-rtcqualitylimitationreason +struct RTCQualityLimitationReason { + static const char* const kNone; + static const char* const kCpu; + static const char* const kBandwidth; + static const char* const kOther; +}; + // https://webrtc.org/experiments/rtp-hdrext/video-content-type/ struct RTCContentType { static const char* const kUnspecified; @@ -464,6 +472,11 @@ class RTC_EXPORT RTCOutboundRTPStreamStats final : public RTCRTPStreamStats { // TODO(https://crbug.com/webrtc/10635): This is only implemented for video; // implement it for audio as well. RTCStatsMember total_packet_send_delay; + // Enum type RTCQualityLimitationReason + // TODO(https://crbug.com/webrtc/10686): Also expose + // qualityLimitationDurations. Requires RTCStatsMember support for + // "record", see https://crbug.com/webrtc/10685. + RTCStatsMember quality_limitation_reason; // https://henbos.github.io/webrtc-provisional-stats/#dom-rtcoutboundrtpstreamstats-contenttype RTCStatsMember content_type; }; diff --git a/call/video_send_stream.h b/call/video_send_stream.h index 929aa88398..850996e858 100644 --- a/call/video_send_stream.h +++ b/call/video_send_stream.h @@ -28,6 +28,7 @@ #include "api/video/video_stream_encoder_settings.h" #include "api/video_codecs/video_encoder_config.h" #include "call/rtp_config.h" +#include "common_video/include/quality_limitation_reason.h" #include "modules/rtp_rtcp/include/report_block_data.h" #include "modules/rtp_rtcp/include/rtcp_statistics.h" #include "modules/rtp_rtcp/include/rtp_rtcp_defines.h" @@ -92,6 +93,11 @@ class VideoSendStream { bool cpu_limited_resolution = false; bool bw_limited_framerate = false; bool cpu_limited_framerate = false; + // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationreason + QualityLimitationReason quality_limitation_reason = + QualityLimitationReason::kNone; + // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationdurations + std::map quality_limitation_durations_ms; // Total number of times resolution as been requested to be changed due to // CPU/quality adaptation. int number_of_cpu_adapt_changes = 0; diff --git a/common_video/BUILD.gn b/common_video/BUILD.gn index 53bdea07f8..10a646fa87 100644 --- a/common_video/BUILD.gn +++ b/common_video/BUILD.gn @@ -28,6 +28,7 @@ rtc_static_library("common_video") { "include/bitrate_adjuster.h", "include/i420_buffer_pool.h", "include/incoming_video_stream.h", + "include/quality_limitation_reason.h", "include/video_frame.h", "include/video_frame_buffer.h", "incoming_video_stream.cc", diff --git a/common_video/include/quality_limitation_reason.h b/common_video/include/quality_limitation_reason.h new file mode 100644 index 0000000000..068136a4b2 --- /dev/null +++ b/common_video/include/quality_limitation_reason.h @@ -0,0 +1,26 @@ +/* + * Copyright 2019 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 COMMON_VIDEO_INCLUDE_QUALITY_LIMITATION_REASON_H_ +#define COMMON_VIDEO_INCLUDE_QUALITY_LIMITATION_REASON_H_ + +namespace webrtc { + +// https://w3c.github.io/webrtc-stats/#rtcqualitylimitationreason-enum +enum class QualityLimitationReason { + kNone, + kCpu, + kBandwidth, + kOther, +}; + +} // namespace webrtc + +#endif // COMMON_VIDEO_INCLUDE_QUALITY_LIMITATION_REASON_H_ diff --git a/media/base/media_channel.h b/media/base/media_channel.h index 3b9a54c17b..c991de39ea 100644 --- a/media/base/media_channel.h +++ b/media/base/media_channel.h @@ -31,6 +31,7 @@ #include "api/video/video_source_interface.h" #include "api/video/video_timing.h" #include "api/video_codecs/video_encoder_config.h" +#include "common_video/include/quality_limitation_reason.h" #include "media/base/codec.h" #include "media/base/delayable.h" #include "media/base/media_config.h" @@ -552,6 +553,12 @@ struct VideoSenderInfo : public MediaSenderInfo { int nominal_bitrate = 0; int adapt_reason = 0; int adapt_changes = 0; + // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationreason + webrtc::QualityLimitationReason quality_limitation_reason = + webrtc::QualityLimitationReason::kNone; + // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationdurations + std::map + quality_limitation_durations_ms; int avg_encode_ms = 0; int encode_usage_percent = 0; uint32_t frames_encoded = 0; diff --git a/media/engine/webrtc_video_engine.cc b/media/engine/webrtc_video_engine.cc index 5950204779..92ed38d4d3 100644 --- a/media/engine/webrtc_video_engine.cc +++ b/media/engine/webrtc_video_engine.cc @@ -2267,6 +2267,8 @@ VideoSenderInfo WebRtcVideoChannel::WebRtcVideoSendStream::GetVideoSenderInfo( if (stats.bw_limited_resolution) info.adapt_reason |= ADAPTREASON_BANDWIDTH; + info.quality_limitation_reason = stats.quality_limitation_reason; + info.quality_limitation_durations_ms = stats.quality_limitation_durations_ms; info.encoder_implementation_name = stats.encoder_implementation_name; info.ssrc_groups = ssrc_groups_; info.framerate_input = stats.input_frame_rate; diff --git a/pc/rtc_stats_collector.cc b/pc/rtc_stats_collector.cc index db7ca61874..4386579d75 100644 --- a/pc/rtc_stats_collector.cc +++ b/pc/rtc_stats_collector.cc @@ -199,6 +199,20 @@ const char* NetworkAdapterTypeToStatsType(rtc::AdapterType type) { return nullptr; } +const char* QualityLimitationReasonToRTCQualityLimitationReason( + QualityLimitationReason reason) { + switch (reason) { + case QualityLimitationReason::kNone: + return RTCQualityLimitationReason::kNone; + case QualityLimitationReason::kCpu: + return RTCQualityLimitationReason::kCpu; + case QualityLimitationReason::kBandwidth: + return RTCQualityLimitationReason::kBandwidth; + case QualityLimitationReason::kOther: + return RTCQualityLimitationReason::kOther; + } +} + double DoubleAudioLevelFromIntAudioLevel(int audio_level) { RTC_DCHECK_GE(audio_level, 0); RTC_DCHECK_LE(audio_level, 32767); @@ -375,6 +389,9 @@ void SetOutboundRTPStreamStatsFromVideoSenderInfo( outbound_video->total_packet_send_delay = static_cast(video_sender_info.total_packet_send_delay_ms) / rtc::kNumMillisecsPerSec; + outbound_video->quality_limitation_reason = + QualityLimitationReasonToRTCQualityLimitationReason( + video_sender_info.quality_limitation_reason); // TODO(https://crbug.com/webrtc/10529): When info's |content_info| is // optional, support the "unspecified" value. if (video_sender_info.content_type == VideoContentType::SCREENSHARE) diff --git a/pc/rtc_stats_collector_unittest.cc b/pc/rtc_stats_collector_unittest.cc index c8876bf216..51e6eb9283 100644 --- a/pc/rtc_stats_collector_unittest.cc +++ b/pc/rtc_stats_collector_unittest.cc @@ -1941,6 +1941,8 @@ TEST_F(RTCStatsCollectorTest, CollectRTCOutboundRTPStreamStats_Video) { video_media_info.senders[0].total_encode_time_ms = 9000; video_media_info.senders[0].total_encoded_bytes_target = 1234; video_media_info.senders[0].total_packet_send_delay_ms = 10000; + video_media_info.senders[0].quality_limitation_reason = + QualityLimitationReason::kBandwidth; video_media_info.senders[0].qp_sum = absl::nullopt; video_media_info.senders[0].content_type = VideoContentType::UNSPECIFIED; @@ -1986,6 +1988,7 @@ TEST_F(RTCStatsCollectorTest, CollectRTCOutboundRTPStreamStats_Video) { expected_video.total_encode_time = 9.0; expected_video.total_encoded_bytes_target = 1234; expected_video.total_packet_send_delay = 10.0; + expected_video.quality_limitation_reason = "bandwidth"; // |expected_video.content_type| should be undefined. // |expected_video.qp_sum| should be undefined. ASSERT_TRUE(report->Get(expected_video.id())); diff --git a/pc/rtc_stats_integrationtest.cc b/pc/rtc_stats_integrationtest.cc index df1d58d77f..36518c9c86 100644 --- a/pc/rtc_stats_integrationtest.cc +++ b/pc/rtc_stats_integrationtest.cc @@ -834,6 +834,7 @@ class RTCStatsReportVerifier { outbound_stream.total_encoded_bytes_target); verifier.TestMemberIsNonNegative( outbound_stream.total_packet_send_delay); + verifier.TestMemberIsDefined(outbound_stream.quality_limitation_reason); // The integration test is not set up to test screen share; don't require // this to be present. verifier.MarkMemberTested(outbound_stream.content_type, true); @@ -844,6 +845,7 @@ class RTCStatsReportVerifier { outbound_stream.total_encoded_bytes_target); // TODO(https://crbug.com/webrtc/10635): Implement for audio as well. verifier.TestMemberIsUndefined(outbound_stream.total_packet_send_delay); + verifier.TestMemberIsUndefined(outbound_stream.quality_limitation_reason); verifier.TestMemberIsUndefined(outbound_stream.content_type); } return verifier.ExpectAllMembersSuccessfullyTested(); diff --git a/stats/rtcstats_objects.cc b/stats/rtcstats_objects.cc index ec2f6e8626..8098707113 100644 --- a/stats/rtcstats_objects.cc +++ b/stats/rtcstats_objects.cc @@ -53,6 +53,12 @@ const char* const RTCNetworkType::kWimax = "wimax"; const char* const RTCNetworkType::kVpn = "vpn"; const char* const RTCNetworkType::kUnknown = "unknown"; +// https://w3c.github.io/webrtc-stats/#dom-rtcqualitylimitationreason +const char* const RTCQualityLimitationReason::kNone = "none"; +const char* const RTCQualityLimitationReason::kCpu = "cpu"; +const char* const RTCQualityLimitationReason::kBandwidth = "bandwidth"; +const char* const RTCQualityLimitationReason::kOther = "other"; + // https://webrtc.org/experiments/rtp-hdrext/video-content-type/ const char* const RTCContentType::kUnspecified = "unspecified"; const char* const RTCContentType::kScreenshare = "screenshare"; @@ -681,6 +687,7 @@ WEBRTC_RTCSTATS_IMPL( &total_encode_time, &total_encoded_bytes_target, &total_packet_send_delay, + &quality_limitation_reason, &content_type) // clang-format on @@ -701,6 +708,7 @@ RTCOutboundRTPStreamStats::RTCOutboundRTPStreamStats(std::string&& id, total_encode_time("totalEncodeTime"), total_encoded_bytes_target("totalEncodedBytesTarget"), total_packet_send_delay("totalPacketSendDelay"), + quality_limitation_reason("qualityLimitationReason"), content_type("contentType") {} RTCOutboundRTPStreamStats::RTCOutboundRTPStreamStats( @@ -716,6 +724,7 @@ RTCOutboundRTPStreamStats::RTCOutboundRTPStreamStats( total_encode_time(other.total_encode_time), total_encoded_bytes_target(other.total_encoded_bytes_target), total_packet_send_delay(other.total_packet_send_delay), + quality_limitation_reason(other.quality_limitation_reason), content_type(other.content_type) {} RTCOutboundRTPStreamStats::~RTCOutboundRTPStreamStats() {} diff --git a/video/BUILD.gn b/video/BUILD.gn index e15b15eeaf..29e1e453ca 100644 --- a/video/BUILD.gn +++ b/video/BUILD.gn @@ -16,6 +16,8 @@ rtc_static_library("video") { "call_stats.h", "encoder_rtcp_feedback.cc", "encoder_rtcp_feedback.h", + "quality_limitation_reason_tracker.cc", + "quality_limitation_reason_tracker.h", "quality_threshold.cc", "quality_threshold.h", "receive_statistics_proxy.cc", @@ -520,6 +522,7 @@ if (rtc_include_tests) { "frame_encode_metadata_writer_unittest.cc", "overuse_frame_detector_unittest.cc", "picture_id_tests.cc", + "quality_limitation_reason_tracker_unittest.cc", "quality_scaling_tests.cc", "quality_threshold_unittest.cc", "receive_statistics_proxy_unittest.cc", diff --git a/video/quality_limitation_reason_tracker.cc b/video/quality_limitation_reason_tracker.cc new file mode 100644 index 0000000000..c2b2cc4043 --- /dev/null +++ b/video/quality_limitation_reason_tracker.cc @@ -0,0 +1,52 @@ +/* + * Copyright 2019 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 "video/quality_limitation_reason_tracker.h" + +#include + +#include "rtc_base/checks.h" + +namespace webrtc { + +QualityLimitationReasonTracker::QualityLimitationReasonTracker(Clock* clock) + : clock_(clock), + current_reason_(QualityLimitationReason::kNone), + current_reason_updated_timestamp_ms_(clock_->TimeInMilliseconds()), + durations_ms_({std::make_pair(QualityLimitationReason::kNone, 0), + std::make_pair(QualityLimitationReason::kCpu, 0), + std::make_pair(QualityLimitationReason::kBandwidth, 0), + std::make_pair(QualityLimitationReason::kOther, 0)}) {} + +QualityLimitationReason QualityLimitationReasonTracker::current_reason() const { + return current_reason_; +} + +void QualityLimitationReasonTracker::SetReason(QualityLimitationReason reason) { + if (reason == current_reason_) + return; + int64_t now_ms = clock_->TimeInMilliseconds(); + durations_ms_[current_reason_] += + now_ms - current_reason_updated_timestamp_ms_; + current_reason_ = reason; + current_reason_updated_timestamp_ms_ = now_ms; +} + +std::map +QualityLimitationReasonTracker::DurationsMs() const { + std::map total_durations_ms = durations_ms_; + auto it = total_durations_ms.find(current_reason_); + RTC_DCHECK(it != total_durations_ms.end()); + it->second += + clock_->TimeInMilliseconds() - current_reason_updated_timestamp_ms_; + return total_durations_ms; +} + +} // namespace webrtc diff --git a/video/quality_limitation_reason_tracker.h b/video/quality_limitation_reason_tracker.h new file mode 100644 index 0000000000..bd0189981e --- /dev/null +++ b/video/quality_limitation_reason_tracker.h @@ -0,0 +1,53 @@ +/* + * Copyright 2019 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 VIDEO_QUALITY_LIMITATION_REASON_TRACKER_H_ +#define VIDEO_QUALITY_LIMITATION_REASON_TRACKER_H_ + +#include + +#include "common_video/include/quality_limitation_reason.h" +#include "system_wrappers/include/clock.h" + +namespace webrtc { + +// A tracker of quality limitation reasons. The quality limitation reason is the +// primary reason for limiting resolution and/or framerate (such as CPU or +// bandwidth limitations). The tracker keeps track of the current reason and the +// duration of time spent in each reason. See qualityLimitationReason[1] and +// qualityLimitationDurations[2] in the webrtc-stats spec. +// [1] +// https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationreason +// [2] +// https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationdurations +class QualityLimitationReasonTracker { + public: + // The caller is responsible for making sure |clock| outlives the tracker. + explicit QualityLimitationReasonTracker(Clock* clock); + + // The current reason defaults to QualityLimitationReason::kNone. + QualityLimitationReason current_reason() const; + void SetReason(QualityLimitationReason reason); + std::map DurationsMs() const; + + private: + Clock* const clock_; + QualityLimitationReason current_reason_; + int64_t current_reason_updated_timestamp_ms_; + // The total amount of time spent in each reason at time + // |current_reason_updated_timestamp_ms_|. To get the total amount duration + // so-far, including the time spent in |current_reason_| elapsed since the + // last time |current_reason_| was updated, see DurationsMs(). + std::map durations_ms_; +}; + +} // namespace webrtc + +#endif // VIDEO_QUALITY_LIMITATION_REASON_TRACKER_H_ diff --git a/video/quality_limitation_reason_tracker_unittest.cc b/video/quality_limitation_reason_tracker_unittest.cc new file mode 100644 index 0000000000..9756b36573 --- /dev/null +++ b/video/quality_limitation_reason_tracker_unittest.cc @@ -0,0 +1,115 @@ +/* + * Copyright 2019 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 "video/quality_limitation_reason_tracker.h" + +#include "common_video/include/quality_limitation_reason.h" +#include "system_wrappers/include/clock.h" +#include "test/gtest.h" + +namespace webrtc { + +class QualityLimitationReasonTrackerTest : public ::testing::Test { + public: + QualityLimitationReasonTrackerTest() + : fake_clock_(1234), tracker_(&fake_clock_) {} + + protected: + SimulatedClock fake_clock_; + QualityLimitationReasonTracker tracker_; +}; + +TEST_F(QualityLimitationReasonTrackerTest, DefaultValues) { + EXPECT_EQ(QualityLimitationReason::kNone, tracker_.current_reason()); + auto durations_ms = tracker_.DurationsMs(); + EXPECT_EQ(4u, durations_ms.size()); + EXPECT_EQ(0, durations_ms.find(QualityLimitationReason::kNone)->second); + EXPECT_EQ(0, durations_ms.find(QualityLimitationReason::kCpu)->second); + EXPECT_EQ(0, durations_ms.find(QualityLimitationReason::kBandwidth)->second); + EXPECT_EQ(0, durations_ms.find(QualityLimitationReason::kOther)->second); +} + +TEST_F(QualityLimitationReasonTrackerTest, NoneDurationIncreasesByDefault) { + int64_t initial_duration_ms = + tracker_.DurationsMs()[QualityLimitationReason::kNone]; + fake_clock_.AdvanceTimeMilliseconds(9999); + EXPECT_EQ(initial_duration_ms + 9999, + tracker_.DurationsMs()[QualityLimitationReason::kNone]); +} + +TEST_F(QualityLimitationReasonTrackerTest, + RememberDurationAfterSwitchingReason) { + tracker_.SetReason(QualityLimitationReason::kCpu); + int64_t initial_duration_ms = + tracker_.DurationsMs()[QualityLimitationReason::kCpu]; + fake_clock_.AdvanceTimeMilliseconds(50); + tracker_.SetReason(QualityLimitationReason::kOther); + fake_clock_.AdvanceTimeMilliseconds(50); + EXPECT_EQ(initial_duration_ms + 50, + tracker_.DurationsMs()[QualityLimitationReason::kCpu]); +} + +class QualityLimitationReasonTrackerTestWithParamReason + : public QualityLimitationReasonTrackerTest, + public ::testing::WithParamInterface { + public: + QualityLimitationReasonTrackerTestWithParamReason() + : reason_(GetParam()), + different_reason_(reason_ != QualityLimitationReason::kCpu + ? QualityLimitationReason::kCpu + : QualityLimitationReason::kOther) {} + + protected: + QualityLimitationReason reason_; + QualityLimitationReason different_reason_; +}; + +TEST_P(QualityLimitationReasonTrackerTestWithParamReason, + DurationIncreasesOverTime) { + int64_t initial_duration_ms = tracker_.DurationsMs()[reason_]; + tracker_.SetReason(reason_); + EXPECT_EQ(initial_duration_ms, tracker_.DurationsMs()[reason_]); + fake_clock_.AdvanceTimeMilliseconds(4321); + EXPECT_EQ(initial_duration_ms + 4321, tracker_.DurationsMs()[reason_]); +} + +TEST_P(QualityLimitationReasonTrackerTestWithParamReason, + SwitchBetweenReasonsBackAndForth) { + int64_t initial_duration_ms = tracker_.DurationsMs()[reason_]; + // Spend 100 ms in |different_reason_|. + tracker_.SetReason(different_reason_); + fake_clock_.AdvanceTimeMilliseconds(100); + EXPECT_EQ(initial_duration_ms, tracker_.DurationsMs()[reason_]); + // Spend 50 ms in |reason_|. + tracker_.SetReason(reason_); + fake_clock_.AdvanceTimeMilliseconds(50); + EXPECT_EQ(initial_duration_ms + 50, tracker_.DurationsMs()[reason_]); + // Spend another 1000 ms in |different_reason_|. + tracker_.SetReason(different_reason_); + fake_clock_.AdvanceTimeMilliseconds(1000); + EXPECT_EQ(initial_duration_ms + 50, tracker_.DurationsMs()[reason_]); + // Spend another 100 ms in |reason_|. + tracker_.SetReason(reason_); + fake_clock_.AdvanceTimeMilliseconds(100); + EXPECT_EQ(initial_duration_ms + 150, tracker_.DurationsMs()[reason_]); + // Change reason one last time without advancing time. + tracker_.SetReason(different_reason_); + EXPECT_EQ(initial_duration_ms + 150, tracker_.DurationsMs()[reason_]); +} + +INSTANTIATE_TEST_SUITE_P( + , + QualityLimitationReasonTrackerTestWithParamReason, + ::testing::Values(QualityLimitationReason::kNone, // "/0" + QualityLimitationReason::kCpu, // "/1" + QualityLimitationReason::kBandwidth, // "/2" + QualityLimitationReason::kOther)); // "/3" + +} // namespace webrtc diff --git a/video/send_statistics_proxy.cc b/video/send_statistics_proxy.cc index 0313316567..cf417f5c3a 100644 --- a/video/send_statistics_proxy.cc +++ b/video/send_statistics_proxy.cc @@ -137,6 +137,7 @@ SendStatisticsProxy::SendStatisticsProxy( encode_time_(kEncodeTimeWeigthFactor), quality_downscales_(-1), cpu_downscales_(-1), + quality_limitation_reason_tracker_(clock_), media_byte_rate_tracker_(kBucketSizeMs, kBucketCount), encoded_frame_rate_tracker_(kBucketSizeMs, kBucketCount), uma_container_( @@ -729,6 +730,8 @@ VideoSendStream::Stats SendStatisticsProxy::GetStats() { : VideoContentType::SCREENSHARE; stats_.encode_frame_rate = round(encoded_frame_rate_tracker_.ComputeRate()); stats_.media_bitrate_bps = media_byte_rate_tracker_.ComputeRate() * 8; + stats_.quality_limitation_durations_ms = + quality_limitation_reason_tracker_.DurationsMs(); return stats_; } @@ -1062,6 +1065,26 @@ void SendStatisticsProxy::OnAdaptationChanged( ++stats_.number_of_quality_adapt_changes; break; } + + bool is_cpu_limited = cpu_counts.num_resolution_reductions > 0 || + cpu_counts.num_framerate_reductions > 0; + bool is_bandwidth_limited = quality_counts.num_resolution_reductions > 0 || + quality_counts.num_framerate_reductions > 0; + if (is_bandwidth_limited) { + // We may be both CPU limited and bandwidth limited at the same time but + // there is no way to express this in standardized stats. Heuristically, + // bandwidth is more likely to be a limiting factor than CPU, and more + // likely to vary over time, so only when we aren't bandwidth limited do we + // want to know about our CPU being the bottleneck. + quality_limitation_reason_tracker_.SetReason( + QualityLimitationReason::kBandwidth); + } else if (is_cpu_limited) { + quality_limitation_reason_tracker_.SetReason(QualityLimitationReason::kCpu); + } else { + quality_limitation_reason_tracker_.SetReason( + QualityLimitationReason::kNone); + } + UpdateAdaptationStats(cpu_counts, quality_counts); } @@ -1075,6 +1098,10 @@ void SendStatisticsProxy::UpdateAdaptationStats( stats_.cpu_limited_framerate = cpu_counts.num_framerate_reductions > 0; stats_.bw_limited_resolution = quality_counts.num_resolution_reductions > 0; stats_.bw_limited_framerate = quality_counts.num_framerate_reductions > 0; + stats_.quality_limitation_reason = + quality_limitation_reason_tracker_.current_reason(); + // |stats_.quality_limitation_durations_ms| depends on the current time + // when it is polled; it is updated in SendStatisticsProxy::GetStats(). } // TODO(asapersson): Include fps changes. diff --git a/video/send_statistics_proxy.h b/video/send_statistics_proxy.h index 51d5b2f381..30e8f8bf94 100644 --- a/video/send_statistics_proxy.h +++ b/video/send_statistics_proxy.h @@ -26,6 +26,7 @@ #include "rtc_base/rate_tracker.h" #include "rtc_base/thread_annotations.h" #include "system_wrappers/include/clock.h" +#include "video/quality_limitation_reason_tracker.h" #include "video/report_block_stats.h" #include "video/stats_counter.h" @@ -244,6 +245,8 @@ class SendStatisticsProxy : public VideoStreamEncoderObserver, rtc::ExpFilter encode_time_ RTC_GUARDED_BY(crit_); int quality_downscales_ RTC_GUARDED_BY(crit_); int cpu_downscales_ RTC_GUARDED_BY(crit_); + QualityLimitationReasonTracker quality_limitation_reason_tracker_ + RTC_GUARDED_BY(crit_); rtc::RateTracker media_byte_rate_tracker_ RTC_GUARDED_BY(crit_); rtc::RateTracker encoded_frame_rate_tracker_ RTC_GUARDED_BY(crit_); diff --git a/video/send_statistics_proxy_unittest.cc b/video/send_statistics_proxy_unittest.cc index 58514e526c..928bc8b701 100644 --- a/video/send_statistics_proxy_unittest.cc +++ b/video/send_statistics_proxy_unittest.cc @@ -1067,6 +1067,145 @@ TEST_F(SendStatisticsProxyTest, AdaptChangesReportedAfterContentSwitch) { "WebRTC.Video.Screenshare.AdaptChangesPerMinute.Quality")); } +TEST_F(SendStatisticsProxyTest, + QualityLimitationReasonIsCpuWhenCpuIsResolutionLimited) { + SendStatisticsProxy::AdaptationSteps cpu_counts; + SendStatisticsProxy::AdaptationSteps quality_counts; + + cpu_counts.num_resolution_reductions = 1; + + statistics_proxy_->OnAdaptationChanged( + VideoStreamEncoderObserver::AdaptationReason::kCpu, cpu_counts, + quality_counts); + + EXPECT_EQ(QualityLimitationReason::kCpu, + statistics_proxy_->GetStats().quality_limitation_reason); +} + +TEST_F(SendStatisticsProxyTest, + QualityLimitationReasonIsCpuWhenCpuIsFramerateLimited) { + SendStatisticsProxy::AdaptationSteps cpu_counts; + SendStatisticsProxy::AdaptationSteps quality_counts; + + cpu_counts.num_framerate_reductions = 1; + + statistics_proxy_->OnAdaptationChanged( + VideoStreamEncoderObserver::AdaptationReason::kCpu, cpu_counts, + quality_counts); + + EXPECT_EQ(QualityLimitationReason::kCpu, + statistics_proxy_->GetStats().quality_limitation_reason); +} + +TEST_F(SendStatisticsProxyTest, + QualityLimitationReasonIsBandwidthWhenQualityIsResolutionLimited) { + SendStatisticsProxy::AdaptationSteps cpu_counts; + SendStatisticsProxy::AdaptationSteps quality_counts; + + quality_counts.num_resolution_reductions = 1; + + statistics_proxy_->OnAdaptationChanged( + VideoStreamEncoderObserver::AdaptationReason::kQuality, cpu_counts, + quality_counts); + + EXPECT_EQ(QualityLimitationReason::kBandwidth, + statistics_proxy_->GetStats().quality_limitation_reason); +} + +TEST_F(SendStatisticsProxyTest, + QualityLimitationReasonIsBandwidthWhenQualityIsFramerateLimited) { + SendStatisticsProxy::AdaptationSteps cpu_counts; + SendStatisticsProxy::AdaptationSteps quality_counts; + + quality_counts.num_framerate_reductions = 1; + + statistics_proxy_->OnAdaptationChanged( + VideoStreamEncoderObserver::AdaptationReason::kQuality, cpu_counts, + quality_counts); + + EXPECT_EQ(QualityLimitationReason::kBandwidth, + statistics_proxy_->GetStats().quality_limitation_reason); +} + +TEST_F(SendStatisticsProxyTest, + QualityLimitationReasonIsBandwidthWhenBothCpuAndQualityIsLimited) { + SendStatisticsProxy::AdaptationSteps cpu_counts; + SendStatisticsProxy::AdaptationSteps quality_counts; + + cpu_counts.num_resolution_reductions = 1; + quality_counts.num_resolution_reductions = 1; + + // Even if the last adaptation reason is kCpu, if the counters indicate being + // both CPU and quality (=bandwidth) limited, kBandwidth takes precedence. + statistics_proxy_->OnAdaptationChanged( + VideoStreamEncoderObserver::AdaptationReason::kCpu, cpu_counts, + quality_counts); + + EXPECT_EQ(QualityLimitationReason::kBandwidth, + statistics_proxy_->GetStats().quality_limitation_reason); +} + +TEST_F(SendStatisticsProxyTest, QualityLimitationReasonIsNoneWhenNotLimited) { + SendStatisticsProxy::AdaptationSteps cpu_counts; + SendStatisticsProxy::AdaptationSteps quality_counts; + + // Observe a limitation due to CPU. This makes sure the test doesn't pass + // due to "none" being the default value. + cpu_counts.num_resolution_reductions = 1; + statistics_proxy_->OnAdaptationChanged( + VideoStreamEncoderObserver::AdaptationReason::kCpu, cpu_counts, + quality_counts); + // Go back to not being limited. + cpu_counts.num_resolution_reductions = 0; + statistics_proxy_->OnAdaptationChanged( + VideoStreamEncoderObserver::AdaptationReason::kNone, cpu_counts, + quality_counts); + + EXPECT_EQ(QualityLimitationReason::kNone, + statistics_proxy_->GetStats().quality_limitation_reason); +} + +TEST_F(SendStatisticsProxyTest, QualityLimitationDurationIncreasesWithTime) { + SendStatisticsProxy::AdaptationSteps cpu_counts; + SendStatisticsProxy::AdaptationSteps quality_counts; + + // Not limited for 3000 ms + fake_clock_.AdvanceTimeMilliseconds(3000); + // CPU limited for 2000 ms + cpu_counts.num_resolution_reductions = 1; + statistics_proxy_->OnAdaptationChanged( + VideoStreamEncoderObserver::AdaptationReason::kCpu, cpu_counts, + quality_counts); + fake_clock_.AdvanceTimeMilliseconds(2000); + // Bandwidth limited for 1000 ms + cpu_counts.num_resolution_reductions = 0; + quality_counts.num_resolution_reductions = 1; + statistics_proxy_->OnAdaptationChanged( + VideoStreamEncoderObserver::AdaptationReason::kQuality, cpu_counts, + quality_counts); + fake_clock_.AdvanceTimeMilliseconds(1000); + // CPU limited for another 2000 ms + cpu_counts.num_resolution_reductions = 1; + quality_counts.num_resolution_reductions = 0; + statistics_proxy_->OnAdaptationChanged( + VideoStreamEncoderObserver::AdaptationReason::kCpu, cpu_counts, + quality_counts); + fake_clock_.AdvanceTimeMilliseconds(2000); + + auto quality_limitation_durations_ms = + statistics_proxy_->GetStats().quality_limitation_durations_ms; + + EXPECT_EQ(3000, + quality_limitation_durations_ms[QualityLimitationReason::kNone]); + EXPECT_EQ(4000, + quality_limitation_durations_ms[QualityLimitationReason::kCpu]); + EXPECT_EQ( + 1000, + quality_limitation_durations_ms[QualityLimitationReason::kBandwidth]); + EXPECT_EQ(0, + quality_limitation_durations_ms[QualityLimitationReason::kOther]); +} + TEST_F(SendStatisticsProxyTest, SwitchContentTypeUpdatesHistograms) { for (int i = 0; i < SendStatisticsProxy::kMinRequiredMetricsSamples; ++i) statistics_proxy_->OnIncomingFrame(kWidth, kHeight);