| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <algorithm> |
| |
| #include "base/test/trace_event_analyzer.h" |
| #include "chrome/browser/media/webrtc/webrtc_browsertest_base.h" |
| #include "chrome/browser/media/webrtc/webrtc_browsertest_common.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_tabstrip.h" |
| #include "chrome/test/base/tracing.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/feature_h264_with_openh264_ffmpeg.h" |
| #include "media/base/media_switches.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "testing/perf/perf_test.h" |
| #include "ui/gl/gl_switches.h" |
| |
| using trace_analyzer::TraceEvent; |
| using trace_analyzer::TraceEventVector; |
| using trace_analyzer::Query; |
| |
| namespace { |
| |
| // Trace events |
| static const char kStartRenderEventName[] = |
| "RemoteVideoSourceDelegate::RenderFrame"; |
| static const char kEnqueueFrameEventName[] = |
| "WebMediaPlayerMSCompositor::EnqueueFrame"; |
| static const char kSetFrameEventName[] = |
| "WebMediaPlayerMSCompositor::SetCurrentFrame"; |
| static const char kGetFrameEventName[] = |
| "WebMediaPlayerMSCompositor::GetCurrentFrame"; |
| static const char kVideoResourceEventName[] = |
| "VideoResourceUpdater::ObtainFrameResources"; |
| static const char kVsyncEventName[] = "Display::DrawAndSwap"; |
| |
| static const char kEventMatchKey[] = "Timestamp"; |
| static const char kTestResultString[] = "TestVideoDisplayPerf"; |
| static const char kMainWebrtcTestHtmlPage[] = |
| "/webrtc/webrtc_video_display_perf_test.html"; |
| |
| struct VideoDisplayPerfTestConfig { |
| int width; |
| int height; |
| int fps; |
| bool disable_render_smoothness_algorithm; |
| }; |
| |
| void CalculateMeanAndMax(const std::vector<double>& inputs, |
| double* mean, |
| double* std_dev, |
| double* max) { |
| double sum = 0.0; |
| double sqr_sum = 0.0; |
| double max_so_far = 0.0; |
| size_t count = inputs.size(); |
| for (const auto& input : inputs) { |
| sum += input; |
| sqr_sum += input * input; |
| max_so_far = std::max(input, max_so_far); |
| } |
| *max = max_so_far; |
| *mean = sum / count; |
| *std_dev = sqrt(std::max(0.0, count * sqr_sum - sum * sum)) / count; |
| } |
| |
| void PrintMeanAndMax(const std::string& var_name, |
| const std::string& name_modifier, |
| const std::vector<double>& vars) { |
| double mean = 0.0; |
| double std_dev = 0.0; |
| double max = 0.0; |
| CalculateMeanAndMax(vars, &mean, &std_dev, &max); |
| perf_test::PrintResultMeanAndError( |
| kTestResultString, name_modifier, var_name + " Mean", |
| base::StringPrintf("%.0lf,%.0lf", mean, std_dev), "μs", true); |
| perf_test::PrintResult(kTestResultString, name_modifier, var_name + " Max", |
| base::StringPrintf("%.0lf", max), "μs", true); |
| } |
| |
| void FindEvents(trace_analyzer::TraceAnalyzer* analyzer, |
| const std::string& event_name, |
| const Query& base_query, |
| TraceEventVector* events) { |
| Query query = Query::EventNameIs(event_name) && base_query; |
| analyzer->FindEvents(query, events); |
| } |
| |
| void AssociateEvents(trace_analyzer::TraceAnalyzer* analyzer, |
| const std::vector<std::string>& event_names, |
| const std::string& match_string, |
| const Query& base_query) { |
| for (size_t i = 0; i < event_names.size() - 1; ++i) { |
| Query begin = Query::EventNameIs(event_names[i]); |
| Query end = Query::EventNameIs(event_names[i + 1]); |
| Query match(Query::EventArg(match_string) == Query::OtherArg(match_string)); |
| analyzer->AssociateEvents(begin, end, base_query && match); |
| } |
| } |
| |
| } // anonymous namespace |
| |
| // Tests the performance of Chrome displaying remote video. |
| // |
| // This test creates a WebRTC peer connection between two tabs and measures the |
| // trace events listed in the beginning of this file on the tab receiving |
| // remote video. In order to cut down from the encode cost, the tab receiving |
| // remote video does not send any video to its peer. |
| // |
| // This test traces certain categories for a period of time. It follows the |
| // lifetime of a single video frame by synchronizing on the timestamps values |
| // attached to trace events. Then, it calculates the duration and related stats. |
| class WebRtcVideoDisplayPerfBrowserTest |
| : public WebRtcTestBase, |
| public testing::WithParamInterface< |
| std::tuple<gfx::Size /* resolution */, |
| int /* fps */, |
| bool /* disable_render_smoothness_algorithm */>> { |
| public: |
| WebRtcVideoDisplayPerfBrowserTest() { |
| const auto& params = GetParam(); |
| const gfx::Size& resolution = std::get<0>(params); |
| test_config_ = {resolution.width(), resolution.height(), |
| std::get<1>(params), std::get<2>(params)}; |
| } |
| |
| void SetUpInProcessBrowserTestFixture() override { |
| DetectErrorsInJavaScript(); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| command_line->AppendSwitch(switches::kUseFakeUIForMediaStream); |
| command_line->AppendSwitchASCII( |
| switches::kUseFakeDeviceForMediaStream, |
| base::StringPrintf("fps=%d", test_config_.fps)); |
| if (test_config_.disable_render_smoothness_algorithm) |
| command_line->AppendSwitch(switches::kDisableRTCSmoothnessAlgorithm); |
| command_line->AppendSwitch(switches::kUseGpuInTests); |
| } |
| |
| void TestVideoDisplayPerf(const std::string& video_codec) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| |
| content::WebContents* left_tab = |
| OpenPageAndGetUserMediaInNewTabWithConstraints( |
| embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage), |
| base::StringPrintf( |
| "{audio: true, video: {mandatory: {minWidth: %d, maxWidth: %d, " |
| "minHeight: %d, maxHeight: %d}}}", |
| test_config_.width, test_config_.width, test_config_.height, |
| test_config_.height)); |
| content::WebContents* right_tab = |
| OpenPageAndGetUserMediaInNewTabWithConstraints( |
| embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage), |
| "{audio: true, video: false}"); |
| const int process_id = |
| right_tab->GetRenderViewHost()->GetProcess()->GetProcess().Pid(); |
| |
| const std::string disable_cpu_adaptation_constraint( |
| "{'optional': [{'googCpuOveruseDetection': false}]}"); |
| SetupPeerconnectionWithConstraintsAndLocalStream( |
| left_tab, disable_cpu_adaptation_constraint); |
| SetupPeerconnectionWithConstraintsAndLocalStream( |
| right_tab, disable_cpu_adaptation_constraint); |
| |
| if (!video_codec.empty()) { |
| SetDefaultVideoCodec(left_tab, video_codec, |
| true /*prefer_hw_video_codec*/); |
| SetDefaultVideoCodec(right_tab, video_codec, |
| true /*prefer_hw_video_codec*/); |
| } |
| NegotiateCall(left_tab, right_tab); |
| |
| StartDetectingVideo(right_tab, "remote-view"); |
| WaitForVideoToPlay(right_tab); |
| // Run the connection a bit to ramp up. |
| test::SleepInJavascript(left_tab, 10000); |
| |
| ASSERT_TRUE(tracing::BeginTracing("media,viz,webrtc")); |
| // Run the connection for 5 seconds to collect metrics. |
| test::SleepInJavascript(left_tab, 5000); |
| |
| std::string json_events; |
| ASSERT_TRUE(tracing::EndTracing(&json_events)); |
| std::unique_ptr<trace_analyzer::TraceAnalyzer> analyzer( |
| trace_analyzer::TraceAnalyzer::Create(json_events)); |
| |
| HangUp(left_tab); |
| HangUp(right_tab); |
| chrome::CloseWebContents(browser(), left_tab, false); |
| chrome::CloseWebContents(browser(), right_tab, false); |
| |
| ASSERT_TRUE(CalculatePerfResults(analyzer.get(), process_id)); |
| PrintResults(video_codec); |
| } |
| |
| private: |
| bool CalculatePerfResults(trace_analyzer::TraceAnalyzer* analyzer, |
| int render_process_id) { |
| Query match_process_id = Query::EventPidIs(render_process_id); |
| const std::vector<std::string> chain_of_events = { |
| kStartRenderEventName, kEnqueueFrameEventName, kSetFrameEventName, |
| kGetFrameEventName, kVideoResourceEventName}; |
| AssociateEvents(analyzer, chain_of_events, |
| kEventMatchKey, match_process_id); |
| |
| TraceEventVector start_render_events; |
| FindEvents(analyzer, kStartRenderEventName, match_process_id, |
| &start_render_events); |
| if (start_render_events.empty()) |
| return false; |
| |
| // We are only interested in vsync events coming after the first render |
| // event. Earlier ones are already missed. |
| Query after_first_render_event = |
| Query::EventTime() > |
| Query::Double(start_render_events.front()->timestamp); |
| TraceEventVector vsync_events; |
| FindEvents(analyzer, kVsyncEventName, after_first_render_event, |
| &vsync_events); |
| if (vsync_events.empty()) |
| return false; |
| |
| size_t found_vsync_index = 0; |
| size_t skipped_frame_count = 0; |
| for (const auto* event : start_render_events) { |
| const double start = event->timestamp; |
| |
| const TraceEvent* enqueue_frame_event = event->other_event; |
| if (!enqueue_frame_event) { |
| skipped_frame_count++; |
| continue; |
| } |
| const double enqueue_frame_duration = |
| enqueue_frame_event->timestamp - start; |
| |
| const TraceEvent* set_frame_event = enqueue_frame_event->other_event; |
| if (!set_frame_event) { |
| skipped_frame_count++; |
| continue; |
| } |
| const double set_frame_duration = |
| set_frame_event->timestamp - enqueue_frame_event->timestamp; |
| |
| const TraceEvent* get_frame_event = set_frame_event->other_event; |
| if (!get_frame_event) { |
| skipped_frame_count++; |
| continue; |
| } |
| const double get_frame_duration = |
| get_frame_event->timestamp - set_frame_event->timestamp; |
| |
| const TraceEvent* video_resource_event = get_frame_event->other_event; |
| if (!video_resource_event) { |
| skipped_frame_count++; |
| continue; |
| } |
| const double resource_ready_duration = |
| video_resource_event->timestamp - get_frame_event->timestamp; |
| |
| // We try to find the closest vsync event after video resource is ready. |
| const bool found_vsync = FindFirstOf( |
| vsync_events, |
| Query::EventTime() > Query::Double(video_resource_event->timestamp + |
| video_resource_event->duration), |
| found_vsync_index, &found_vsync_index); |
| if (!found_vsync) { |
| skipped_frame_count++; |
| continue; |
| } |
| const double vsync_duration = vsync_events[found_vsync_index]->timestamp - |
| video_resource_event->timestamp; |
| const double total_duration = |
| vsync_events[found_vsync_index]->timestamp - start; |
| |
| enqueue_frame_durations_.push_back(enqueue_frame_duration); |
| set_frame_durations_.push_back(set_frame_duration); |
| get_frame_durations_.push_back(get_frame_duration); |
| resource_ready_durations_.push_back(resource_ready_duration); |
| vsync_durations_.push_back(vsync_duration); |
| total_controlled_durations_.push_back(total_duration - |
| set_frame_duration); |
| total_durations_.push_back(total_duration); |
| } |
| |
| if (start_render_events.size() == skipped_frame_count) |
| return false; |
| |
| // Calculate the percentage by dividing by the number of frames received. |
| skipped_frame_percentage_ = |
| 100.0 * skipped_frame_count / start_render_events.size(); |
| return true; |
| } |
| |
| void PrintResults(const std::string& video_codec) { |
| std::string smoothness_indicator = |
| test_config_.disable_render_smoothness_algorithm ? "_DisableSmoothness" |
| : ""; |
| std::string name_modifier = base::StringPrintf( |
| "%s_%dp%df%s", video_codec.c_str(), test_config_.height, |
| test_config_.fps, smoothness_indicator.c_str()); |
| perf_test::PrintResult( |
| kTestResultString, name_modifier, "Skipped frames", |
| base::StringPrintf("%.2lf", skipped_frame_percentage_), "percent", |
| true); |
| // We identify intervals in a way that can help us easily bisect the source |
| // of added latency in case of a regression. From these intervals, "Render |
| // Algorithm" can take random amount of times based on the vsync cycle it is |
| // closest to. Therefore, "Total Controlled Latency" refers to the total |
| // times without that section for semi-consistent results. |
| PrintMeanAndMax("Passing to Render Algorithm Latency", name_modifier, |
| enqueue_frame_durations_); |
| PrintMeanAndMax("Render Algorithm Latency", name_modifier, |
| set_frame_durations_); |
| PrintMeanAndMax("Compositor Picking Frame Latency", name_modifier, |
| get_frame_durations_); |
| PrintMeanAndMax("Compositor Resource Preparation Latency", name_modifier, |
| resource_ready_durations_); |
| PrintMeanAndMax("Vsync Latency", name_modifier, vsync_durations_); |
| PrintMeanAndMax("Total Controlled Latency", name_modifier, |
| total_controlled_durations_); |
| PrintMeanAndMax("Total Latency", name_modifier, total_durations_); |
| } |
| |
| VideoDisplayPerfTestConfig test_config_; |
| // Containers for test results. |
| double skipped_frame_percentage_ = 0; |
| std::vector<double> enqueue_frame_durations_; |
| std::vector<double> set_frame_durations_; |
| std::vector<double> get_frame_durations_; |
| std::vector<double> resource_ready_durations_; |
| std::vector<double> vsync_durations_; |
| std::vector<double> total_controlled_durations_; |
| std::vector<double> total_durations_; |
| }; |
| |
| INSTANTIATE_TEST_CASE_P(WebRtcVideoDisplayPerfBrowserTests, |
| WebRtcVideoDisplayPerfBrowserTest, |
| testing::Combine(testing::Values(gfx::Size(1280, 720), |
| gfx::Size(1920, 1080)), |
| testing::Values(30, 60), |
| testing::Bool())); |
| |
| IN_PROC_BROWSER_TEST_P(WebRtcVideoDisplayPerfBrowserTest, |
| MANUAL_TestVideoDisplayPerfVP9) { |
| TestVideoDisplayPerf("VP9"); |
| } |
| |
| #if BUILDFLAG(RTC_USE_H264) |
| IN_PROC_BROWSER_TEST_P(WebRtcVideoDisplayPerfBrowserTest, |
| MANUAL_TestVideoDisplayPerfH264) { |
| if (!base::FeatureList::IsEnabled(content::kWebRtcH264WithOpenH264FFmpeg)) { |
| LOG(WARNING) << "Run-time feature WebRTC-H264WithOpenH264FFmpeg disabled. " |
| "Skipping WebRtcVideoDisplayPerfBrowserTest.MANUAL_" |
| "TestVideoDisplayPerfH264 " |
| "(test \"OK\")"; |
| return; |
| } |
| TestVideoDisplayPerf("H264"); |
| } |
| #endif // BUILDFLAG(RTC_USE_H264) |