From fcf4e2cd674d5a01bf8d50e44ac20546a1ab7b59 Mon Sep 17 00:00:00 2001 From: Artem Titov Date: Tue, 25 Feb 2020 10:56:26 +0100 Subject: [PATCH] Fix potential memory exhaustion in DefaultVideoQualityAnalyzer DefaultVideoQualityAnalyzer accumulates in flight frames in internal queue to perform psnr/ssim computation. This queue can grow a lot if test experience high frame loss. As a result of this, the analyzer can use quite a lot of memory and cause OOM crashes. This CL limits the size of the queue based on the assumption that after a certain point a frame can be considered lost and so it is impossible to calculate PSNR/SSIM. Bug: webrtc:11373 Change-Id: Iaabcc8d1c3c9142dc58ea5f2f30f599864b088e8 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/168951 Reviewed-by: Mirko Bonadei Commit-Queue: Artem Titov Cr-Commit-Position: refs/heads/master@{#30602} --- test/pc/e2e/BUILD.gn | 17 ++ .../video/default_video_quality_analyzer.cc | 110 ++++++--- .../video/default_video_quality_analyzer.h | 79 +++++-- .../default_video_quality_analyzer_test.cc | 209 ++++++++++++++++++ 4 files changed, 365 insertions(+), 50 deletions(-) create mode 100644 test/pc/e2e/analyzer/video/default_video_quality_analyzer_test.cc diff --git a/test/pc/e2e/BUILD.gn b/test/pc/e2e/BUILD.gn index 9a235271d7..1e61ee0827 100644 --- a/test/pc/e2e/BUILD.gn +++ b/test/pc/e2e/BUILD.gn @@ -35,6 +35,7 @@ if (rtc_include_tests) { deps = [ ":default_encoded_image_data_injector_unittest", + ":default_video_quality_analyzer_test", ":peer_connection_e2e_smoke_test", ":single_process_encoded_image_data_injector_unittest", ] @@ -394,6 +395,22 @@ if (rtc_include_tests) { "../../../rtc_base:logging", ] } + + rtc_library("default_video_quality_analyzer_test") { + testonly = true + sources = [ "analyzer/video/default_video_quality_analyzer_test.cc" ] + deps = [ + ":default_video_quality_analyzer", + "../..:test_support", + "../../../api:create_frame_generator", + "../../../api:rtp_packet_info", + "../../../api/video:encoded_image", + "../../../api/video:video_frame", + "../../../api/video:video_frame_i420", + "../../../modules/rtp_rtcp:rtp_rtcp_format", + "../../../system_wrappers", + ] + } } rtc_library("analyzer_helper") { diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer.cc b/test/pc/e2e/analyzer/video/default_video_quality_analyzer.cc index c20650c366..0b0c3b1add 100644 --- a/test/pc/e2e/analyzer/video/default_video_quality_analyzer.cc +++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer.cc @@ -64,8 +64,11 @@ double RateCounter::GetEventsPerSecond() const { } DefaultVideoQualityAnalyzer::DefaultVideoQualityAnalyzer( - bool heavy_metrics_computation_enabled) + bool heavy_metrics_computation_enabled, + int max_frames_in_flight_per_stream_count) : heavy_metrics_computation_enabled_(heavy_metrics_computation_enabled), + max_frames_in_flight_per_stream_count_( + max_frames_in_flight_per_stream_count), clock_(Clock::GetRealTimeClock()) {} DefaultVideoQualityAnalyzer::~DefaultVideoQualityAnalyzer() { Stop(); @@ -120,7 +123,7 @@ uint16_t DefaultVideoQualityAnalyzer::OnFrameCaptured( stream_frame_counters_[stream_label].captured++; StreamState* state = &stream_states_[stream_label]; - state->frame_ids.push_back(frame_id); + state->PushBack(frame_id); // Update frames in flight info. auto it = captured_frames_in_flight_.find(frame_id); if (it != captured_frames_in_flight_.end()) { @@ -130,8 +133,8 @@ uint16_t DefaultVideoQualityAnalyzer::OnFrameCaptured( auto stats_it = frame_stats_.find(frame_id); RTC_DCHECK(stats_it != frame_stats_.end()); - RTC_DCHECK(frame_id == state->frame_ids.front()); - state->frame_ids.pop_front(); + uint16_t oldest_frame_id = state->PopFront(); + RTC_DCHECK_EQ(frame_id, oldest_frame_id); frame_counters_.dropped++; stream_frame_counters_[stream_label].dropped++; AddComparison(it->second, absl::nullopt, true, stats_it->second); @@ -152,6 +155,15 @@ uint16_t DefaultVideoQualityAnalyzer::OnFrameCaptured( it->second.erase(frame_id); } stream_to_frame_id_history_[stream_label].insert(frame_id); + + // If state has too many frames that are in flight => remove the oldest + // queued frame in order to avoid to use too much memory. + if (state->GetAliveFramesCount() > max_frames_in_flight_per_stream_count_) { + uint16_t frame_id_to_remove = state->MarkNextAliveFrameAsDead(); + auto removed_count = captured_frames_in_flight_.erase(frame_id_to_remove); + RTC_DCHECK_EQ(removed_count, 1) + << "Invalid stream state: alive frame is removed already"; + } } return frame_id; } @@ -247,8 +259,10 @@ void DefaultVideoQualityAnalyzer::OnFrameRendered( // Find corresponding captured frame. auto frame_it = captured_frames_in_flight_.find(frame.id()); - RTC_DCHECK(frame_it != captured_frames_in_flight_.end()); - const VideoFrame& captured_frame = frame_it->second; + absl::optional captured_frame = + frame_it != captured_frames_in_flight_.end() + ? absl::optional(frame_it->second) + : absl::nullopt; // After we received frame here we need to check if there are any dropped // frames between this one and last one, that was rendered for this video @@ -257,10 +271,9 @@ void DefaultVideoQualityAnalyzer::OnFrameRendered( const std::string& stream_label = frame_stats->stream_label; StreamState* state = &stream_states_[stream_label]; int dropped_count = 0; - while (!state->frame_ids.empty() && state->frame_ids.front() != frame.id()) { + while (!state->Empty() && state->Front() != frame.id()) { dropped_count++; - uint16_t dropped_frame_id = state->frame_ids.front(); - state->frame_ids.pop_front(); + uint16_t dropped_frame_id = state->PopFront(); // Frame with id |dropped_frame_id| was dropped. We need: // 1. Update global and stream frame counters // 2. Extract corresponding frame from |captured_frames_in_flight_| @@ -273,22 +286,27 @@ void DefaultVideoQualityAnalyzer::OnFrameRendered( auto dropped_frame_stats_it = frame_stats_.find(dropped_frame_id); RTC_DCHECK(dropped_frame_stats_it != frame_stats_.end()); auto dropped_frame_it = captured_frames_in_flight_.find(dropped_frame_id); - RTC_CHECK(dropped_frame_it != captured_frames_in_flight_.end()); + absl::optional dropped_frame = + dropped_frame_it != captured_frames_in_flight_.end() + ? absl::optional(dropped_frame_it->second) + : absl::nullopt; - AddComparison(dropped_frame_it->second, absl::nullopt, true, + AddComparison(dropped_frame, absl::nullopt, true, dropped_frame_stats_it->second); frame_stats_.erase(dropped_frame_stats_it); - captured_frames_in_flight_.erase(dropped_frame_it); + if (dropped_frame_it != captured_frames_in_flight_.end()) { + captured_frames_in_flight_.erase(dropped_frame_it); + } } - RTC_DCHECK(!state->frame_ids.empty()); - state->frame_ids.pop_front(); + RTC_DCHECK(!state->Empty()); + state->PopFront(); - if (state->last_rendered_frame_time) { + if (state->last_rendered_frame_time()) { frame_stats->prev_frame_rendered_time = - state->last_rendered_frame_time.value(); + state->last_rendered_frame_time().value(); } - state->last_rendered_frame_time = frame_stats->rendered_time; + state->set_last_rendered_frame_time(frame_stats->rendered_time); { rtc::CritScope cr(&comparison_lock_); stream_stats_[stream_label].skipped_between_rendered.AddSample( @@ -296,7 +314,9 @@ void DefaultVideoQualityAnalyzer::OnFrameRendered( } AddComparison(captured_frame, frame, false, *frame_stats); - captured_frames_in_flight_.erase(frame_it); + if (frame_it != captured_frames_in_flight_.end()) { + captured_frames_in_flight_.erase(frame_it); + } frame_stats_.erase(stats_it); } @@ -343,9 +363,9 @@ void DefaultVideoQualityAnalyzer::Stop() { // |stream_last_freeze_end_time_| for this stream will be |start_time_|. // If there is freeze, then we need add time from last rendered frame // to last freeze end as time between freezes. - if (state.last_rendered_frame_time) { + if (state.last_rendered_frame_time()) { item.second.time_between_freezes_ms.AddSample( - (state.last_rendered_frame_time.value() - + (state.last_rendered_frame_time().value() - stream_last_freeze_end_time_.at(item.first)) .ms()); } @@ -380,7 +400,7 @@ std::set DefaultVideoQualityAnalyzer::GetKnownVideoStreams() return out; } -const FrameCounters& DefaultVideoQualityAnalyzer::GetGlobalCounters() { +const FrameCounters& DefaultVideoQualityAnalyzer::GetGlobalCounters() const { rtc::CritScope crit(&lock_); return frame_counters_; } @@ -465,10 +485,15 @@ void DefaultVideoQualityAnalyzer::AddComparison( // If there too many computations waiting in the queue, we won't provide // frames itself to make future computations lighter. if (comparisons_.size() >= kMaxActiveComparisons) { - comparisons_.emplace_back(dropped, frame_stats); + comparisons_.emplace_back(absl::nullopt, absl::nullopt, dropped, + frame_stats, OverloadReason::kCpu); } else { + OverloadReason overload_reason = OverloadReason::kNone; + if (!captured && !dropped) { + overload_reason = OverloadReason::kMemory; + } comparisons_.emplace_back(std::move(captured), std::move(rendered), dropped, - frame_stats); + frame_stats, overload_reason); } comparison_available_event_.Set(); } @@ -529,8 +554,10 @@ void DefaultVideoQualityAnalyzer::ProcessComparison( RTC_CHECK(stats_it != stream_stats_.end()); StreamStats* stats = &stats_it->second; analyzer_stats_.comparisons_done++; - if (!comparison.captured) { - analyzer_stats_.overloaded_comparisons_done++; + if (comparison.overload_reason == OverloadReason::kCpu) { + analyzer_stats_.cpu_overloaded_comparisons_done++; + } else if (comparison.overload_reason == OverloadReason::kMemory) { + analyzer_stats_.memory_overloaded_comparisons_done++; } if (psnr > 0) { stats->psnr.AddSample(psnr); @@ -612,8 +639,10 @@ void DefaultVideoQualityAnalyzer::ReportResults() { << analyzer_stats_.comparisons_queue_size.GetPercentile(0.99); } RTC_LOG(INFO) << "comparisons_done=" << analyzer_stats_.comparisons_done; - RTC_LOG(INFO) << "overloaded_comparisons_done=" - << analyzer_stats_.overloaded_comparisons_done; + RTC_LOG(INFO) << "cpu_overloaded_comparisons_done=" + << analyzer_stats_.cpu_overloaded_comparisons_done; + RTC_LOG(INFO) << "memory_overloaded_comparisons_done=" + << analyzer_stats_.memory_overloaded_comparisons_done; } void DefaultVideoQualityAnalyzer::ReportVideoBweResults( @@ -737,19 +766,28 @@ DefaultVideoQualityAnalyzer::FrameComparison::FrameComparison( absl::optional captured, absl::optional rendered, bool dropped, - FrameStats frame_stats) + FrameStats frame_stats, + OverloadReason overload_reason) : captured(std::move(captured)), rendered(std::move(rendered)), dropped(dropped), - frame_stats(std::move(frame_stats)) {} + frame_stats(std::move(frame_stats)), + overload_reason(overload_reason) {} -DefaultVideoQualityAnalyzer::FrameComparison::FrameComparison( - bool dropped, - FrameStats frame_stats) - : captured(absl::nullopt), - rendered(absl::nullopt), - dropped(dropped), - frame_stats(std::move(frame_stats)) {} +uint16_t DefaultVideoQualityAnalyzer::StreamState::PopFront() { + uint16_t frame_id = frame_ids_.front(); + frame_ids_.pop_front(); + if (dead_frames_count_ > 0) { + dead_frames_count_--; + } + return frame_id; +} + +uint16_t DefaultVideoQualityAnalyzer::StreamState::MarkNextAliveFrameAsDead() { + uint16_t frame_id = frame_ids_[dead_frames_count_]; + dead_frames_count_++; + return frame_id; +} } // namespace webrtc_pc_e2e } // namespace webrtc diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer.h b/test/pc/e2e/analyzer/video/default_video_quality_analyzer.h index 736cd9da01..3ed7a65475 100644 --- a/test/pc/e2e/analyzer/video/default_video_quality_analyzer.h +++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer.h @@ -33,6 +33,11 @@ namespace webrtc { namespace webrtc_pc_e2e { +// WebRTC will request a key frame after 3 seconds if no frames were received. +// We assume max frame rate ~60 fps, so 270 frames will cover max freeze without +// key frame request. +constexpr int kDefaultMaxFramesInFlightPerStream = 270; + class RateCounter { public: void AddEvent(Timestamp event_time); @@ -105,14 +110,18 @@ struct AnalyzerStats { // Size of analyzer internal comparisons queue, measured when new element // id added to the queue. SamplesStatsCounter comparisons_queue_size; - // Amount of performed comparisons of 2 video frames from captured and + // Number of performed comparisons of 2 video frames from captured and // rendered streams. int64_t comparisons_done = 0; - // Amount of overloaded comparisons. Comparison is overloaded if it is queued - // when there are too many not processed comparisons in the queue. Overloaded - // comparison doesn't include metrics, that require heavy computations like - // SSIM and PSNR. - int64_t overloaded_comparisons_done = 0; + // Number of cpu overloaded comparisons. Comparison is cpu overloaded if it is + // queued when there are too many not processed comparisons in the queue. + // Overloaded comparison doesn't include metrics like SSIM and PSNR that + // require heavy computations. + int64_t cpu_overloaded_comparisons_done = 0; + // Number of memory overloaded comparisons. Comparison is memory overloaded if + // it is queued when its captured frame was already removed due to high memory + // usage for that video stream. + int64_t memory_overloaded_comparisons_done = 0; }; struct VideoBweStats { @@ -126,7 +135,9 @@ struct VideoBweStats { class DefaultVideoQualityAnalyzer : public VideoQualityAnalyzerInterface { public: explicit DefaultVideoQualityAnalyzer( - bool heavy_metrics_computation_enabled = true); + bool heavy_metrics_computation_enabled = true, + int max_frames_in_flight_per_stream_count = + kDefaultMaxFramesInFlightPerStream); ~DefaultVideoQualityAnalyzer() override; void Start(std::string test_case_name, int max_threads_count) override; @@ -149,7 +160,7 @@ class DefaultVideoQualityAnalyzer : public VideoQualityAnalyzerInterface { // Returns set of stream labels, that were met during test call. std::set GetKnownVideoStreams() const; - const FrameCounters& GetGlobalCounters(); + const FrameCounters& GetGlobalCounters() const; // Returns frame counter per stream label. Valid stream labels can be obtained // by calling GetKnownVideoStreams() const std::map& GetPerStreamCounters() const; @@ -186,6 +197,16 @@ class DefaultVideoQualityAnalyzer : public VideoQualityAnalyzerInterface { absl::optional rendered_frame_height = absl::nullopt; }; + // Describes why comparison was done in overloaded mode (without calculating + // PSNR and SSIM). + enum class OverloadReason { + kNone, + // Not enough CPU to process all incoming comparisons. + kCpu, + // Not enough memory to store captured frames for all comparisons. + kMemory + }; + // Represents comparison between two VideoFrames. Contains video frames itself // and stats. Can be one of two types: // 1. Normal - in this case |captured| is presented and either |rendered| is @@ -198,8 +219,8 @@ class DefaultVideoQualityAnalyzer : public VideoQualityAnalyzerInterface { FrameComparison(absl::optional captured, absl::optional rendered, bool dropped, - FrameStats frame_stats); - FrameComparison(bool dropped, FrameStats frameStats); + FrameStats frame_stats, + OverloadReason overload_reason); // Frames can be omitted if there too many computations waiting in the // queue. @@ -210,10 +231,32 @@ class DefaultVideoQualityAnalyzer : public VideoQualityAnalyzerInterface { // will be |absl::nullopt|. bool dropped; FrameStats frame_stats; + OverloadReason overload_reason; }; // Represents a current state of video stream. - struct StreamState { + class StreamState { + public: + void PushBack(uint16_t frame_id) { frame_ids_.emplace_back(frame_id); } + + uint16_t PopFront(); + + bool Empty() { return frame_ids_.empty(); } + + uint16_t Front() { return frame_ids_.front(); } + + int GetAliveFramesCount() { return frame_ids_.size() - dead_frames_count_; } + + uint16_t MarkNextAliveFrameAsDead(); + + void set_last_rendered_frame_time(Timestamp time) { + last_rendered_frame_time_ = time; + } + absl::optional last_rendered_frame_time() const { + return last_rendered_frame_time_; + } + + private: // To correctly determine dropped frames we have to know sequence of frames // in each stream so we will keep a list of frame ids inside the stream. // When the frame is rendered, we will pop ids from the list for until id @@ -225,8 +268,10 @@ class DefaultVideoQualityAnalyzer : public VideoQualityAnalyzerInterface { // If we received frame with id frame_id3, then we will pop frame_id1 and // frame_id2 and consider that frames as dropped and then compare received // frame with the one from |captured_frames_in_flight_| with id frame_id3. - std::deque frame_ids; - absl::optional last_rendered_frame_time = absl::nullopt; + std::deque frame_ids_; + // Count of dead frames in the beginning of the deque. + int dead_frames_count_; + absl::optional last_rendered_frame_time_ = absl::nullopt; }; enum State { kNew, kActive, kStopped }; @@ -258,6 +303,7 @@ class DefaultVideoQualityAnalyzer : public VideoQualityAnalyzerInterface { Timestamp Now(); const bool heavy_metrics_computation_enabled_; + const int max_frames_in_flight_per_stream_count_; webrtc::Clock* const clock_; std::atomic next_frame_id_{0}; @@ -267,7 +313,12 @@ class DefaultVideoQualityAnalyzer : public VideoQualityAnalyzerInterface { State state_ RTC_GUARDED_BY(lock_) = State::kNew; Timestamp start_time_ RTC_GUARDED_BY(lock_) = Timestamp::MinusInfinity(); // Frames that were captured by all streams and still aren't rendered by any - // stream or deemed dropped. + // stream or deemed dropped. Frame with id X can be removed from this map if: + // 1. The frame with id X was received in OnFrameRendered + // 2. The frame with id Y > X was received in OnFrameRendered + // 3. Next available frame id for newly captured frame is X + // 4. There too many frames in flight for current video stream and X is the + // oldest frame id in this stream. std::map captured_frames_in_flight_ RTC_GUARDED_BY(lock_); // Global frames count for all video streams. diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_test.cc b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_test.cc new file mode 100644 index 0000000000..1a59015e10 --- /dev/null +++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_test.cc @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2020 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 +#include +#include + +#include "api/rtp_packet_info.h" +#include "api/rtp_packet_infos.h" +#include "api/test/create_frame_generator.h" +#include "api/video/encoded_image.h" +#include "api/video/i420_buffer.h" +#include "api/video/video_frame.h" +#include "system_wrappers/include/sleep.h" +#include "test/gtest.h" +#include "test/pc/e2e/analyzer/video/default_video_quality_analyzer.h" + +namespace webrtc { +namespace webrtc_pc_e2e { +namespace { + +constexpr int kAnalyzerMaxThreadsCount = 1; +constexpr int kMaxFramesInFlightPerStream = 10; +constexpr int kFrameWidth = 320; +constexpr int kFrameHeight = 240; +constexpr char kStreamLabel[] = "video-stream"; + +VideoFrame NextFrame(test::FrameGeneratorInterface* frame_generator, + int64_t timestamp_us) { + test::FrameGeneratorInterface::VideoFrameData frame_data = + frame_generator->NextFrame(); + return VideoFrame::Builder() + .set_video_frame_buffer(frame_data.buffer) + .set_update_rect(frame_data.update_rect) + .set_timestamp_us(timestamp_us) + .build(); +} + +EncodedImage FakeEncode(const VideoFrame& frame) { + EncodedImage image; + std::vector packet_infos; + packet_infos.push_back( + RtpPacketInfo(/*ssrc=*/1, + /*csrcs=*/{}, + /*rtp_timestamp=*/frame.timestamp(), + /*audio_level=*/absl::nullopt, + /*absolute_capture_time=*/absl::nullopt, + /*receive_time_ms=*/frame.timestamp_us() + 10)); + image.SetPacketInfos(RtpPacketInfos(packet_infos)); + return image; +} + +VideoFrame DeepCopy(const VideoFrame& frame) { + VideoFrame copy = frame; + copy.set_video_frame_buffer( + I420Buffer::Copy(*frame.video_frame_buffer()->ToI420())); + return copy; +} + +TEST(DefaultVideoQualityAnalyzerTest, + MemoryOverloadedAndThenAllFramesReceived) { + std::unique_ptr frame_generator = + test::CreateSquareFrameGenerator(kFrameWidth, kFrameHeight, + /*type=*/absl::nullopt, + /*num_squares=*/absl::nullopt); + + DefaultVideoQualityAnalyzer analyzer( + /*heavy_metrics_computation_enabled=*/false, kMaxFramesInFlightPerStream); + analyzer.Start("test_case", kAnalyzerMaxThreadsCount); + + std::map captured_frames; + std::vector frames_order; + for (int i = 0; i < kMaxFramesInFlightPerStream * 2; ++i) { + VideoFrame frame = NextFrame(frame_generator.get(), i); + frame.set_id(analyzer.OnFrameCaptured(kStreamLabel, frame)); + frames_order.push_back(frame.id()); + captured_frames.insert({frame.id(), frame}); + analyzer.OnFramePreEncode(frame); + analyzer.OnFrameEncoded(frame.id(), FakeEncode(frame)); + } + + for (const uint16_t& frame_id : frames_order) { + VideoFrame received_frame = DeepCopy(captured_frames.at(frame_id)); + analyzer.OnFramePreDecode(received_frame.id(), FakeEncode(received_frame)); + analyzer.OnFrameDecoded(received_frame, /*decode_time_ms=*/absl::nullopt, + /*qp=*/absl::nullopt); + analyzer.OnFrameRendered(received_frame); + } + + // Give analyzer some time to process frames on async thread. The computations + // have to be fast (heavy metrics are disabled!), so if doesn't fit 100ms it + // means we have an issue! + SleepMs(100); + analyzer.Stop(); + + AnalyzerStats stats = analyzer.GetAnalyzerStats(); + EXPECT_EQ(stats.memory_overloaded_comparisons_done, + kMaxFramesInFlightPerStream); + EXPECT_EQ(stats.comparisons_done, kMaxFramesInFlightPerStream * 2); + FrameCounters frame_counters = analyzer.GetGlobalCounters(); + EXPECT_EQ(frame_counters.captured, kMaxFramesInFlightPerStream * 2); + EXPECT_EQ(frame_counters.rendered, kMaxFramesInFlightPerStream * 2); + EXPECT_EQ(frame_counters.dropped, 0); +} + +TEST(DefaultVideoQualityAnalyzerTest, + MemoryOverloadedHalfDroppedAndThenHalfFramesReceived) { + std::unique_ptr frame_generator = + test::CreateSquareFrameGenerator(kFrameWidth, kFrameHeight, + /*type=*/absl::nullopt, + /*num_squares=*/absl::nullopt); + + DefaultVideoQualityAnalyzer analyzer( + /*heavy_metrics_computation_enabled=*/false, kMaxFramesInFlightPerStream); + analyzer.Start("test_case", kAnalyzerMaxThreadsCount); + + std::map captured_frames; + std::vector frames_order; + for (int i = 0; i < kMaxFramesInFlightPerStream * 2; ++i) { + VideoFrame frame = NextFrame(frame_generator.get(), i); + frame.set_id(analyzer.OnFrameCaptured(kStreamLabel, frame)); + frames_order.push_back(frame.id()); + captured_frames.insert({frame.id(), frame}); + analyzer.OnFramePreEncode(frame); + analyzer.OnFrameEncoded(frame.id(), FakeEncode(frame)); + } + + for (size_t i = kMaxFramesInFlightPerStream; i < frames_order.size(); ++i) { + uint16_t frame_id = frames_order.at(i); + VideoFrame received_frame = DeepCopy(captured_frames.at(frame_id)); + analyzer.OnFramePreDecode(received_frame.id(), FakeEncode(received_frame)); + analyzer.OnFrameDecoded(received_frame, /*decode_time_ms=*/absl::nullopt, + /*qp=*/absl::nullopt); + analyzer.OnFrameRendered(received_frame); + } + + // Give analyzer some time to process frames on async thread. The computations + // have to be fast (heavy metrics are disabled!), so if doesn't fit 100ms it + // means we have an issue! + SleepMs(100); + analyzer.Stop(); + + AnalyzerStats stats = analyzer.GetAnalyzerStats(); + EXPECT_EQ(stats.memory_overloaded_comparisons_done, 0); + EXPECT_EQ(stats.comparisons_done, kMaxFramesInFlightPerStream * 2); + FrameCounters frame_counters = analyzer.GetGlobalCounters(); + EXPECT_EQ(frame_counters.captured, kMaxFramesInFlightPerStream * 2); + EXPECT_EQ(frame_counters.rendered, kMaxFramesInFlightPerStream); + EXPECT_EQ(frame_counters.dropped, kMaxFramesInFlightPerStream); +} + +TEST(DefaultVideoQualityAnalyzerTest, NormalScenario) { + std::unique_ptr frame_generator = + test::CreateSquareFrameGenerator(kFrameWidth, kFrameHeight, + /*type=*/absl::nullopt, + /*num_squares=*/absl::nullopt); + + DefaultVideoQualityAnalyzer analyzer( + /*heavy_metrics_computation_enabled=*/false, kMaxFramesInFlightPerStream); + analyzer.Start("test_case", kAnalyzerMaxThreadsCount); + + std::map captured_frames; + std::vector frames_order; + for (int i = 0; i < kMaxFramesInFlightPerStream; ++i) { + VideoFrame frame = NextFrame(frame_generator.get(), i); + frame.set_id(analyzer.OnFrameCaptured(kStreamLabel, frame)); + frames_order.push_back(frame.id()); + captured_frames.insert({frame.id(), frame}); + analyzer.OnFramePreEncode(frame); + analyzer.OnFrameEncoded(frame.id(), FakeEncode(frame)); + } + + for (size_t i = 1; i < frames_order.size(); i += 2) { + uint16_t frame_id = frames_order.at(i); + VideoFrame received_frame = DeepCopy(captured_frames.at(frame_id)); + analyzer.OnFramePreDecode(received_frame.id(), FakeEncode(received_frame)); + analyzer.OnFrameDecoded(received_frame, /*decode_time_ms=*/absl::nullopt, + /*qp=*/absl::nullopt); + analyzer.OnFrameRendered(received_frame); + } + + // Give analyzer some time to process frames on async thread. The computations + // have to be fast (heavy metrics are disabled!), so if doesn't fit 100ms it + // means we have an issue! + SleepMs(100); + analyzer.Stop(); + + AnalyzerStats stats = analyzer.GetAnalyzerStats(); + EXPECT_EQ(stats.memory_overloaded_comparisons_done, 0); + EXPECT_EQ(stats.comparisons_done, kMaxFramesInFlightPerStream); + + FrameCounters frame_counters = analyzer.GetGlobalCounters(); + EXPECT_EQ(frame_counters.captured, kMaxFramesInFlightPerStream); + EXPECT_EQ(frame_counters.received, kMaxFramesInFlightPerStream / 2); + EXPECT_EQ(frame_counters.decoded, kMaxFramesInFlightPerStream / 2); + EXPECT_EQ(frame_counters.rendered, kMaxFramesInFlightPerStream / 2); + EXPECT_EQ(frame_counters.dropped, kMaxFramesInFlightPerStream / 2); +} + +} // namespace +} // namespace webrtc_pc_e2e +} // namespace webrtc