From 1f83af1978b886e6dbe728bd793074c23293d55d Mon Sep 17 00:00:00 2001 From: Mounir Lamouri Date: Thu, 21 Jan 2016 18:12:31 +0000 Subject: [PATCH] Merge of "Add metrics regarding concurrent audible tabs in Chromium." These metrics are keeping track of: - whether there is another audible tab when a tab becomes audible; - the maximum number of concurrent audible tab in a session; - how long there are 2 or more audible tabs at the same time. It is also recording when a tab gain or loses audible status. BUG=578049 Review URL: https://codereview.chromium.org/1591453005 Cr-Commit-Position: refs/heads/master@{#370435} (cherry picked from commit 44e4ef42ba9104b06ae212042452dfc81d89032b) Review URL: https://codereview.chromium.org/1618523003 . Cr-Commit-Position: refs/branch-heads/2623@{#44} Cr-Branched-From: 92d77538a86529ca35f9220bd3cd512cbea1f086-refs/heads/master@{#369907} --- content/browser/media/audible_metrics.cc | 79 ++++ content/browser/media/audible_metrics.h | 49 +++ .../browser/media/audible_metrics_unittest.cc | 397 ++++++++++++++++++ content/browser/media/audio_stream_monitor.cc | 16 +- content/browser/media/audio_stream_monitor.h | 8 + .../media/audio_stream_monitor_unittest.cc | 87 +++- .../media/media_web_contents_observer.cc | 27 +- .../media/media_web_contents_observer.h | 6 +- .../browser/web_contents/web_contents_impl.cc | 3 +- content/content_browser.gypi | 2 + content/content_tests.gypi | 1 + tools/metrics/actions/actions.xml | 10 + tools/metrics/histograms/histograms.xml | 27 ++ 13 files changed, 684 insertions(+), 28 deletions(-) create mode 100644 content/browser/media/audible_metrics.cc create mode 100644 content/browser/media/audible_metrics.h create mode 100644 content/browser/media/audible_metrics_unittest.cc diff --git a/content/browser/media/audible_metrics.cc b/content/browser/media/audible_metrics.cc new file mode 100644 index 0000000000000..4a740288e795b --- /dev/null +++ b/content/browser/media/audible_metrics.cc @@ -0,0 +1,79 @@ +// Copyright 2016 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 "content/browser/media/audible_metrics.h" + +#include "base/metrics/histogram_macros.h" +#include "base/metrics/user_metrics.h" +#include "base/time/default_tick_clock.h" + +namespace content { + +AudibleMetrics::AudibleMetrics() + : max_concurrent_audible_web_contents_in_session_(0), + clock_(new base::DefaultTickClock()) { +} + +AudibleMetrics::~AudibleMetrics() { +} + +void AudibleMetrics::UpdateAudibleWebContentsState( + const WebContents* web_contents, bool audible) { + bool found = + audible_web_contents_.find(web_contents) != audible_web_contents_.end(); + if (found == audible) + return; + + if (audible) + AddAudibleWebContents(web_contents); + else + RemoveAudibleWebContents(web_contents); +} + +void AudibleMetrics::SetClockForTest(scoped_ptr test_clock) { + clock_ = std::move(test_clock); +} + +void AudibleMetrics::AddAudibleWebContents(const WebContents* web_contents) { + base::RecordAction(base::UserMetricsAction("Media.Audible.AddTab")); + + UMA_HISTOGRAM_CUSTOM_COUNTS( + "Media.Audible.ConcurrentTabsWhenStarting", audible_web_contents_.size(), + 1, 10, 11); + + audible_web_contents_.insert(web_contents); + if (audible_web_contents_.size() > 1 && + concurrent_web_contents_start_time_.is_null()) { + concurrent_web_contents_start_time_ = clock_->NowTicks(); + } + + if (audible_web_contents_.size() > + max_concurrent_audible_web_contents_in_session_) { + max_concurrent_audible_web_contents_in_session_ = + audible_web_contents_.size(); + + UMA_HISTOGRAM_CUSTOM_COUNTS( + "Media.Audible.MaxConcurrentTabsInSession", + max_concurrent_audible_web_contents_in_session_, + 1, 10, 11); + } +} + +void AudibleMetrics::RemoveAudibleWebContents(const WebContents* web_contents) { + base::RecordAction(base::UserMetricsAction("Media.Audible.RemoveTab")); + + audible_web_contents_.erase(web_contents); + + if (audible_web_contents_.size() <= 1 && + !concurrent_web_contents_start_time_.is_null()) { + base::TimeDelta concurrent_total_time = + clock_->NowTicks() - concurrent_web_contents_start_time_; + concurrent_web_contents_start_time_ = base::TimeTicks(); + + UMA_HISTOGRAM_LONG_TIMES("Media.Audible.ConcurrentTabsTime", + concurrent_total_time); + } +} + +} // namespace content diff --git a/content/browser/media/audible_metrics.h b/content/browser/media/audible_metrics.h new file mode 100644 index 0000000000000..408bc8cba2744 --- /dev/null +++ b/content/browser/media/audible_metrics.h @@ -0,0 +1,49 @@ +// Copyright 2016 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. + +#ifndef CONTENT_BROWSER_MEDIA_AUDIBLE_METRICS_H_ +#define CONTENT_BROWSER_MEDIA_AUDIBLE_METRICS_H_ + +#include + +#include "base/memory/scoped_ptr.h" +#include "base/time/tick_clock.h" +#include "content/common/content_export.h" + +namespace content { + +class WebContents; + +// This class handles metrics regarding audible WebContents. +// It does register three different information: +// - how many WebContents are audible when a WebContents become audible. +// - how long multiple WebContents are audible at the same time. +// - for a browsing session, how often and how many WebContents get audible at +// the same time. +class CONTENT_EXPORT AudibleMetrics { + public: + AudibleMetrics(); + ~AudibleMetrics(); + + void UpdateAudibleWebContentsState(const WebContents* web_contents, + bool audible); + + void SetClockForTest(scoped_ptr test_clock); + + private: + void AddAudibleWebContents(const WebContents* web_contents); + void RemoveAudibleWebContents(const WebContents* web_contents); + + base::TimeTicks concurrent_web_contents_start_time_; + size_t max_concurrent_audible_web_contents_in_session_; + scoped_ptr clock_; + + std::set audible_web_contents_; + + DISALLOW_COPY_AND_ASSIGN(AudibleMetrics); +}; + +} // namespace content + +#endif // CONTENT_BROWSER_MEDIA_AUDIBLE_METRICS_H_ diff --git a/content/browser/media/audible_metrics_unittest.cc b/content/browser/media/audible_metrics_unittest.cc new file mode 100644 index 0000000000000..1a6216b64599b --- /dev/null +++ b/content/browser/media/audible_metrics_unittest.cc @@ -0,0 +1,397 @@ +// Copyright 2016 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 "content/browser/media/audible_metrics.h" + +#include "base/metrics/histogram_samples.h" +#include "base/test/histogram_tester.h" +#include "base/test/simple_test_tick_clock.h" +#include "base/test/user_action_tester.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +namespace { + +static const WebContents* WEB_CONTENTS_0 = reinterpret_cast(0x00); +static const WebContents* WEB_CONTENTS_1 = reinterpret_cast(0x01); +static const WebContents* WEB_CONTENTS_2 = reinterpret_cast(0x10); +static const WebContents* WEB_CONTENTS_3 = reinterpret_cast(0x11); + +static const char* CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM = + "Media.Audible.ConcurrentTabsWhenStarting"; +static const char* MAX_CONCURRENT_TAB_IN_SESSION_HISTOGRAM = + "Media.Audible.MaxConcurrentTabsInSession"; +static const char* CONCURRENT_TABS_TIME_HISTOGRAM = + "Media.Audible.ConcurrentTabsTime"; + +static const char* ADD_TAB_USER_ACTION = "Media.Audible.AddTab"; +static const char* REMOVE_TAB_USER_ACTION = "Media.Audible.RemoveTab"; + +class AudibleMetricsTest : public testing::Test { + public: + AudibleMetricsTest() = default; + + void SetUp() override { + clock_ = new base::SimpleTestTickClock(); + // Set the clock to a value different than 0 so the time it gives is + // recognized as initialized. + clock_->Advance(base::TimeDelta::FromMilliseconds(1)); + audible_metrics_.SetClockForTest( + scoped_ptr(clock_)); + } + + void TearDown() override { + clock_ = nullptr; + } + + base::SimpleTestTickClock* clock() { return clock_; } + + AudibleMetrics* audible_metrics() { + return &audible_metrics_; + }; + + const base::UserActionTester& user_action_tester() const { + return user_action_tester_; + } + + scoped_ptr GetHistogramSamplesSinceTestStart( + const std::string& name) { + return histogram_tester_.GetHistogramSamplesSinceCreation(name); + } + + private: + base::SimpleTestTickClock* clock_ = nullptr; + AudibleMetrics audible_metrics_; + base::HistogramTester histogram_tester_; + base::UserActionTester user_action_tester_; + + DISALLOW_COPY_AND_ASSIGN(AudibleMetricsTest); +}; + +} // anonymous namespace + +TEST_F(AudibleMetricsTest, CreateAndKillDoesNothing) { + { + scoped_ptr audible_metrics(new AudibleMetrics()); + } + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart( + CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM)); + EXPECT_EQ(0, samples->TotalCount()); + } + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart( + MAX_CONCURRENT_TAB_IN_SESSION_HISTOGRAM)); + EXPECT_EQ(0, samples->TotalCount()); + } + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart(CONCURRENT_TABS_TIME_HISTOGRAM)); + EXPECT_EQ(0, samples->TotalCount()); + } + + EXPECT_EQ(0, user_action_tester().GetActionCount(ADD_TAB_USER_ACTION)); + EXPECT_EQ(0, user_action_tester().GetActionCount(REMOVE_TAB_USER_ACTION)); +} + +TEST_F(AudibleMetricsTest, AudibleStart) { + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart( + CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(0)); + } + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart( + MAX_CONCURRENT_TAB_IN_SESSION_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(1)); + } + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart(CONCURRENT_TABS_TIME_HISTOGRAM)); + EXPECT_EQ(0, samples->TotalCount()); + } + + EXPECT_EQ(1, user_action_tester().GetActionCount(ADD_TAB_USER_ACTION)); + EXPECT_EQ(0, user_action_tester().GetActionCount(REMOVE_TAB_USER_ACTION)); +} + +TEST_F(AudibleMetricsTest, AudibleStartAndStop) { + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, false); + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart( + CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(0)); + } + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart( + MAX_CONCURRENT_TAB_IN_SESSION_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(1)); + } + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart(CONCURRENT_TABS_TIME_HISTOGRAM)); + EXPECT_EQ(0, samples->TotalCount()); + } + + EXPECT_EQ(1, user_action_tester().GetActionCount(ADD_TAB_USER_ACTION)); + EXPECT_EQ(1, user_action_tester().GetActionCount(REMOVE_TAB_USER_ACTION)); +} + +TEST_F(AudibleMetricsTest, AddSameTabIsNoOp) { + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart( + CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(0)); + } + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart( + MAX_CONCURRENT_TAB_IN_SESSION_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(1)); + } + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart(CONCURRENT_TABS_TIME_HISTOGRAM)); + EXPECT_EQ(0, samples->TotalCount()); + } + + EXPECT_EQ(1, user_action_tester().GetActionCount(ADD_TAB_USER_ACTION)); + EXPECT_EQ(0, user_action_tester().GetActionCount(REMOVE_TAB_USER_ACTION)); +} + +TEST_F(AudibleMetricsTest, RemoveUnknownTabIsNoOp) { + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, false); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, false); + + EXPECT_EQ(0, GetHistogramSamplesSinceTestStart( + CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM)->TotalCount()); + EXPECT_EQ(0, GetHistogramSamplesSinceTestStart( + MAX_CONCURRENT_TAB_IN_SESSION_HISTOGRAM)->TotalCount()); + EXPECT_EQ(0, GetHistogramSamplesSinceTestStart( + CONCURRENT_TABS_TIME_HISTOGRAM)->TotalCount()); + + EXPECT_EQ(0, user_action_tester().GetActionCount(ADD_TAB_USER_ACTION)); + EXPECT_EQ(0, user_action_tester().GetActionCount(REMOVE_TAB_USER_ACTION)); +} + +TEST_F(AudibleMetricsTest, ConcurrentTabsInSessionIsIncremental) { + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_2, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_3, true); + + scoped_ptr samples(GetHistogramSamplesSinceTestStart( + MAX_CONCURRENT_TAB_IN_SESSION_HISTOGRAM)); + EXPECT_EQ(4, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(1)); + EXPECT_EQ(1, samples->GetCount(2)); + EXPECT_EQ(1, samples->GetCount(3)); + EXPECT_EQ(1, samples->GetCount(4)); + + EXPECT_EQ(4, user_action_tester().GetActionCount(ADD_TAB_USER_ACTION)); + EXPECT_EQ(0, user_action_tester().GetActionCount(REMOVE_TAB_USER_ACTION)); +} + +TEST_F(AudibleMetricsTest, ConcurrentTabsInSessionKeepTrackOfRemovedTabs) { + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, false); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_2, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, false); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_2, false); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_3, true); + + scoped_ptr samples(GetHistogramSamplesSinceTestStart( + MAX_CONCURRENT_TAB_IN_SESSION_HISTOGRAM)); + EXPECT_EQ(2, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(1)); + EXPECT_EQ(1, samples->GetCount(2)); + + EXPECT_EQ(4, user_action_tester().GetActionCount(ADD_TAB_USER_ACTION)); + EXPECT_EQ(3, user_action_tester().GetActionCount(REMOVE_TAB_USER_ACTION)); +} + +TEST_F(AudibleMetricsTest, ConcurrentTabsInSessionIsNotCountedTwice) { + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_2, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_3, true); + + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, false); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, false); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_2, false); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_3, false); + + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_2, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_3, true); + + scoped_ptr samples(GetHistogramSamplesSinceTestStart( + MAX_CONCURRENT_TAB_IN_SESSION_HISTOGRAM)); + EXPECT_EQ(4, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(1)); + EXPECT_EQ(1, samples->GetCount(2)); + EXPECT_EQ(1, samples->GetCount(3)); + EXPECT_EQ(1, samples->GetCount(4)); + + EXPECT_EQ(8, user_action_tester().GetActionCount(ADD_TAB_USER_ACTION)); + EXPECT_EQ(4, user_action_tester().GetActionCount(REMOVE_TAB_USER_ACTION)); +} + +TEST_F(AudibleMetricsTest, ConcurrentTabsWhenStartingAddedPerTab) { + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, true); + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart( + CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM)); + EXPECT_EQ(2, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(0)); + EXPECT_EQ(1, samples->GetCount(1)); + } + + EXPECT_EQ(2, user_action_tester().GetActionCount(ADD_TAB_USER_ACTION)); + EXPECT_EQ(0, user_action_tester().GetActionCount(REMOVE_TAB_USER_ACTION)); + + // Added again: ignored. + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, true); + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart( + CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM)); + EXPECT_EQ(2, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(0)); + EXPECT_EQ(1, samples->GetCount(1)); + } + + EXPECT_EQ(2, user_action_tester().GetActionCount(ADD_TAB_USER_ACTION)); + EXPECT_EQ(0, user_action_tester().GetActionCount(REMOVE_TAB_USER_ACTION)); + + // Removing both. + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, false); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, false); + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart( + CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM)); + EXPECT_EQ(2, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(0)); + EXPECT_EQ(1, samples->GetCount(1)); + } + + EXPECT_EQ(2, user_action_tester().GetActionCount(ADD_TAB_USER_ACTION)); + EXPECT_EQ(2, user_action_tester().GetActionCount(REMOVE_TAB_USER_ACTION)); + + // Adding them after removed, it is counted. + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, true); + + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart( + CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM)); + EXPECT_EQ(4, samples->TotalCount()); + EXPECT_EQ(2, samples->GetCount(0)); + EXPECT_EQ(2, samples->GetCount(1)); + } + + EXPECT_EQ(4, user_action_tester().GetActionCount(ADD_TAB_USER_ACTION)); + EXPECT_EQ(2, user_action_tester().GetActionCount(REMOVE_TAB_USER_ACTION)); +} + +TEST_F(AudibleMetricsTest, ConcurrentTabsTimeRequiresTwoAudibleTabs) { + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, true); + + clock()->Advance(base::TimeDelta::FromMilliseconds(1000)); + + // No record because concurrent audible tabs still running. + EXPECT_EQ(0, GetHistogramSamplesSinceTestStart( + CONCURRENT_TABS_TIME_HISTOGRAM)->TotalCount()); + + // No longer concurrent. + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, false); + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart(CONCURRENT_TABS_TIME_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(1000)); + } + + // Stopping the second tab is a no-op. + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, false); + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart(CONCURRENT_TABS_TIME_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(1000)); + } +} + +TEST_F(AudibleMetricsTest, ConcurrentTabsTimeRunsAsLongAsTwoAudibleTabs) { + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, true); + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, true); + + clock()->Advance(base::TimeDelta::FromMilliseconds(1000)); + + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_2, true); + + clock()->Advance(base::TimeDelta::FromMilliseconds(500)); + + // Mutes one of the three audible tabs. + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_1, false); + + // No record because concurrent audible tabs still running. + EXPECT_EQ(0, GetHistogramSamplesSinceTestStart( + CONCURRENT_TABS_TIME_HISTOGRAM)->TotalCount()); + + // Mutes the first audible tab. + audible_metrics()->UpdateAudibleWebContentsState(WEB_CONTENTS_0, false); + { + scoped_ptr samples( + GetHistogramSamplesSinceTestStart(CONCURRENT_TABS_TIME_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(1500)); + } +} + +} // namespace content diff --git a/content/browser/media/audio_stream_monitor.cc b/content/browser/media/audio_stream_monitor.cc index 2f6a7e45ce0eb..b19e82ff46272 100644 --- a/content/browser/media/audio_stream_monitor.cc +++ b/content/browser/media/audio_stream_monitor.cc @@ -30,7 +30,8 @@ AudioStreamMonitor* AudioStreamMonitorFromRenderFrame(int render_process_id, AudioStreamMonitor::AudioStreamMonitor(WebContents* contents) : web_contents_(contents), clock_(&default_tick_clock_), - was_recently_audible_(false) + was_recently_audible_(false), + is_audible_(false) { DCHECK(web_contents_); } @@ -42,6 +43,11 @@ bool AudioStreamMonitor::WasRecentlyAudible() const { return was_recently_audible_; } +bool AudioStreamMonitor::IsCurrentlyAudible() const { + DCHECK(thread_checker_.CalledOnValidThread()); + return is_audible_; +} + // static void AudioStreamMonitor::StartMonitoringStream( int render_process_id, @@ -124,6 +130,9 @@ void AudioStreamMonitor::StopMonitoringStreamOnUIThread(int render_process_id, } void AudioStreamMonitor::Poll() { + bool was_audible = is_audible_; + is_audible_ = false; + for (StreamPollCallbackMap::const_iterator it = poll_callbacks_.begin(); it != poll_callbacks_.end(); ++it) { @@ -132,12 +141,17 @@ void AudioStreamMonitor::Poll() { // information except for "is it audible?" const float power_dbfs = it->second.Run().first; const float kSilenceThresholdDBFS = -72.24719896f; + if (power_dbfs >= kSilenceThresholdDBFS) { last_blurt_time_ = clock_->NowTicks(); + is_audible_ = true; MaybeToggle(); break; // No need to poll remaining streams. } } + + if (is_audible_ != was_audible) + web_contents_->NotifyNavigationStateChanged(INVALIDATE_TYPE_TAB); } void AudioStreamMonitor::MaybeToggle() { diff --git a/content/browser/media/audio_stream_monitor.h b/content/browser/media/audio_stream_monitor.h index 0eb51475fbff5..12f06055c5497 100644 --- a/content/browser/media/audio_stream_monitor.h +++ b/content/browser/media/audio_stream_monitor.h @@ -52,6 +52,11 @@ class CONTENT_EXPORT AudioStreamMonitor { // the killing of tabs making sounds). bool WasRecentlyAudible() const; + // Returns true if the audio is currently audible from the given WebContents. + // The difference from WasRecentlyAudible() is that this method will return + // false as soon as the WebContents stop producing sound. + bool IsCurrentlyAudible() const; + // Starts or stops audio level monitoring respectively for the stream owned by // the specified renderer. Safe to call from any thread. // @@ -139,6 +144,9 @@ class CONTENT_EXPORT AudioStreamMonitor { // should be turned on. bool was_recently_audible_; + // Whether the WebContents is currently audible. + bool is_audible_; + // Calls Poll() at regular intervals while |poll_callbacks_| is non-empty. base::RepeatingTimer poll_timer_; diff --git a/content/browser/media/audio_stream_monitor_unittest.cc b/content/browser/media/audio_stream_monitor_unittest.cc index d9999bf9db394..9ecee0a5aecd9 100644 --- a/content/browser/media/audio_stream_monitor_unittest.cc +++ b/content/browser/media/audio_stream_monitor_unittest.cc @@ -93,16 +93,37 @@ class AudioStreamMonitorTest : public RenderViewHostTestHarness { monitor_->off_timer_.IsRunning()); } - void ExpectWebContentsWillBeNotifiedOnce(bool should_be_audible) { + void ExpectIsCurrentlyAudible() const { + EXPECT_TRUE(monitor_->IsCurrentlyAudible()); + } + + void ExpectNotCurrentlyAudible() const { + EXPECT_FALSE(monitor_->IsCurrentlyAudible()); + } + + void ExpectRecentlyAudibleChangeNotification(bool new_recently_audible) { + EXPECT_CALL( + mock_web_contents_delegate_, + NavigationStateChanged(RenderViewHostTestHarness::web_contents(), + INVALIDATE_TYPE_TAB)) + .WillOnce(InvokeWithoutArgs( + this, + new_recently_audible + ? &AudioStreamMonitorTest::ExpectWasRecentlyAudible + : &AudioStreamMonitorTest::ExpectNotRecentlyAudible)) + .RetiresOnSaturation(); + } + + void ExpectCurrentlyAudibleChangeNotification(bool new_audible) { EXPECT_CALL( mock_web_contents_delegate_, NavigationStateChanged(RenderViewHostTestHarness::web_contents(), INVALIDATE_TYPE_TAB)) .WillOnce(InvokeWithoutArgs( this, - should_be_audible - ? &AudioStreamMonitorTest::ExpectIsNotifyingForToggleOn - : &AudioStreamMonitorTest::ExpectIsNotifyingForToggleOff)) + new_audible + ? &AudioStreamMonitorTest::ExpectIsCurrentlyAudible + : &AudioStreamMonitorTest::ExpectNotCurrentlyAudible)) .RetiresOnSaturation(); } @@ -136,11 +157,11 @@ class AudioStreamMonitorTest : public RenderViewHostTestHarness { return std::make_pair(current_power_[stream_id], false); } - void ExpectIsNotifyingForToggleOn() { + void ExpectWasRecentlyAudible() const { EXPECT_TRUE(monitor_->WasRecentlyAudible()); } - void ExpectIsNotifyingForToggleOff() { + void ExpectNotRecentlyAudible() const { EXPECT_FALSE(monitor_->WasRecentlyAudible()); } @@ -155,14 +176,17 @@ class AudioStreamMonitorTest : public RenderViewHostTestHarness { // ReadPowerAndClipCallback, and is not polling at other times. TEST_F(AudioStreamMonitorTest, PollsWhenProvidedACallback) { EXPECT_FALSE(monitor_->WasRecentlyAudible()); + ExpectNotCurrentlyAudible(); ExpectIsPolling(kRenderProcessId, kStreamId, false); StartMonitoring(kRenderProcessId, kStreamId, CreatePollCallback(kStreamId)); EXPECT_FALSE(monitor_->WasRecentlyAudible()); + ExpectNotCurrentlyAudible(); ExpectIsPolling(kRenderProcessId, kStreamId, true); StopMonitoring(kRenderProcessId, kStreamId); EXPECT_FALSE(monitor_->WasRecentlyAudible()); + ExpectNotCurrentlyAudible(); ExpectIsPolling(kRenderProcessId, kStreamId, false); } @@ -175,13 +199,16 @@ TEST_F(AudioStreamMonitorTest, // Expect WebContents will get one call form AudioStreamMonitor to toggle the // indicator on upon the very first poll. - ExpectWebContentsWillBeNotifiedOnce(true); + ExpectRecentlyAudibleChangeNotification(true); // Loop, each time testing a slightly longer period of polled silence. The - // indicator should remain on throughout. - int num_silence_polls = 0; + // recently audible state should not change while the currently audible one + // should. + int num_silence_polls = 1; base::TimeTicks last_blurt_time; do { + ExpectCurrentlyAudibleChangeNotification(true); + // Poll an audible signal, and expect tab indicator state is on. SetStreamPower(kStreamId, media::AudioPowerMonitor::max_power()); last_blurt_time = GetTestClockTime(); @@ -189,6 +216,8 @@ TEST_F(AudioStreamMonitorTest, ExpectTabWasRecentlyAudible(true, last_blurt_time); AdvanceClock(one_polling_interval()); + ExpectCurrentlyAudibleChangeNotification(false); + // Poll a silent signal repeatedly, ensuring that the indicator is being // held on during the holding period. SetStreamPower(kStreamId, media::AudioPowerMonitor::zero_power()); @@ -207,7 +236,7 @@ TEST_F(AudioStreamMonitorTest, // At this point, the clock has just advanced to beyond the holding period, so // the next firing of the off timer should turn off the tab indicator. Also, // make sure it stays off for several cycles thereafter. - ExpectWebContentsWillBeNotifiedOnce(false); + ExpectRecentlyAudibleChangeNotification(false); for (int i = 0; i < 10; ++i) { SimulateOffTimerFired(); ExpectTabWasRecentlyAudible(false, last_blurt_time); @@ -224,15 +253,19 @@ TEST_F(AudioStreamMonitorTest, HandlesMultipleStreamsBlurting) { base::TimeTicks last_blurt_time; ExpectTabWasRecentlyAudible(false, last_blurt_time); + ExpectNotCurrentlyAudible(); // Measure audible sound from the first stream and silence from the second. - // The indicator turns on (i.e., tab was recently audible). - ExpectWebContentsWillBeNotifiedOnce(true); + // The tab becomes audible. + ExpectRecentlyAudibleChangeNotification(true); + ExpectCurrentlyAudibleChangeNotification(true); + SetStreamPower(kStreamId, media::AudioPowerMonitor::max_power()); SetStreamPower(kAnotherStreamId, media::AudioPowerMonitor::zero_power()); last_blurt_time = GetTestClockTime(); SimulatePollTimerFired(); ExpectTabWasRecentlyAudible(true, last_blurt_time); + ExpectIsCurrentlyAudible(); // Halfway through the holding period, the second stream joins in. The // indicator stays on. @@ -242,39 +275,52 @@ TEST_F(AudioStreamMonitorTest, HandlesMultipleStreamsBlurting) { last_blurt_time = GetTestClockTime(); SimulatePollTimerFired(); // Restarts holding period. ExpectTabWasRecentlyAudible(true, last_blurt_time); + ExpectIsCurrentlyAudible(); // Now, measure silence from both streams. After an entire holding period - // has passed (since the second stream joined in), the indicator should turn - // off. - ExpectWebContentsWillBeNotifiedOnce(false); + // has passed (since the second stream joined in), the tab will no longer + // become audible nor recently audible. + ExpectCurrentlyAudibleChangeNotification(false); + ExpectRecentlyAudibleChangeNotification(false); + AdvanceClock(holding_period()); SimulateOffTimerFired(); SetStreamPower(kStreamId, media::AudioPowerMonitor::zero_power()); SetStreamPower(kAnotherStreamId, media::AudioPowerMonitor::zero_power()); SimulatePollTimerFired(); ExpectTabWasRecentlyAudible(false, last_blurt_time); + ExpectNotCurrentlyAudible(); // Now, measure silence from the first stream and audible sound from the - // second. The indicator turns back on. - ExpectWebContentsWillBeNotifiedOnce(true); + // second. The tab becomes audible again. + ExpectRecentlyAudibleChangeNotification(true); + ExpectCurrentlyAudibleChangeNotification(true); + SetStreamPower(kAnotherStreamId, media::AudioPowerMonitor::max_power()); last_blurt_time = GetTestClockTime(); SimulatePollTimerFired(); ExpectTabWasRecentlyAudible(true, last_blurt_time); + ExpectIsCurrentlyAudible(); // From here onwards, both streams are silent. Halfway through the holding - // period, the indicator should not have changed. + // period, the tab is no longer audible but stays as recently audible. + ExpectCurrentlyAudibleChangeNotification(false); + SetStreamPower(kAnotherStreamId, media::AudioPowerMonitor::zero_power()); AdvanceClock(holding_period() / 2); SimulatePollTimerFired(); SimulateOffTimerFired(); ExpectTabWasRecentlyAudible(true, last_blurt_time); + ExpectNotCurrentlyAudible(); + + // Just past the holding period, the tab is no longer marked as recently + // audible. + ExpectRecentlyAudibleChangeNotification(false); - // Just past the holding period, the indicator should be turned off. - ExpectWebContentsWillBeNotifiedOnce(false); AdvanceClock(holding_period() - (GetTestClockTime() - last_blurt_time)); SimulateOffTimerFired(); ExpectTabWasRecentlyAudible(false, last_blurt_time); + ExpectNotCurrentlyAudible(); // Polling should not turn the indicator back while both streams are remaining // silent. @@ -282,6 +328,7 @@ TEST_F(AudioStreamMonitorTest, HandlesMultipleStreamsBlurting) { AdvanceClock(one_polling_interval()); SimulatePollTimerFired(); ExpectTabWasRecentlyAudible(false, last_blurt_time); + ExpectNotCurrentlyAudible(); } } diff --git a/content/browser/media/media_web_contents_observer.cc b/content/browser/media/media_web_contents_observer.cc index 0591e25b73eaf..67ef0368680e8 100644 --- a/content/browser/media/media_web_contents_observer.cc +++ b/content/browser/media/media_web_contents_observer.cc @@ -4,8 +4,11 @@ #include "content/browser/media/media_web_contents_observer.h" +#include "base/lazy_instance.h" #include "base/memory/scoped_ptr.h" #include "build/build_config.h" +#include "content/browser/media/audible_metrics.h" +#include "content/browser/media/audio_stream_monitor.h" #include "content/browser/power_save_blocker_impl.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/common/frame_messages.h" @@ -15,23 +18,43 @@ namespace content { +namespace { + +static base::LazyInstance::Leaky g_audible_metrics = + LAZY_INSTANCE_INITIALIZER; + +} // anonymous namespace + MediaWebContentsObserver::MediaWebContentsObserver(WebContents* web_contents) : WebContentsObserver(web_contents) {} MediaWebContentsObserver::~MediaWebContentsObserver() {} +void MediaWebContentsObserver::WebContentsDestroyed() { + g_audible_metrics.Get().UpdateAudibleWebContentsState(web_contents(), false); +} + void MediaWebContentsObserver::RenderFrameDeleted( RenderFrameHost* render_frame_host) { ClearPowerSaveBlockers(render_frame_host); } -void MediaWebContentsObserver::MaybeUpdateAudibleState(bool recently_audible) { - if (recently_audible) { +void MediaWebContentsObserver::MaybeUpdateAudibleState() { + if (!AudioStreamMonitor::monitoring_available()) + return; + + AudioStreamMonitor* audio_stream_monitor = + static_cast(web_contents())->audio_stream_monitor(); + + if (audio_stream_monitor->WasRecentlyAudible()) { if (!audio_power_save_blocker_) CreateAudioPowerSaveBlocker(); } else { audio_power_save_blocker_.reset(); } + + g_audible_metrics.Get().UpdateAudibleWebContentsState( + web_contents(), audio_stream_monitor->IsCurrentlyAudible()); } bool MediaWebContentsObserver::OnMessageReceived( diff --git a/content/browser/media/media_web_contents_observer.h b/content/browser/media/media_web_contents_observer.h index 452349f4287f3..1f76029cf4e5d 100644 --- a/content/browser/media/media_web_contents_observer.h +++ b/content/browser/media/media_web_contents_observer.h @@ -28,11 +28,11 @@ class CONTENT_EXPORT MediaWebContentsObserver : public WebContentsObserver { explicit MediaWebContentsObserver(WebContents* web_contents); ~MediaWebContentsObserver() override; - // Called when the audible state has changed. If inaudible any audio power - // save blockers are released. - void MaybeUpdateAudibleState(bool recently_audible); + // Called by WebContentsImpl when the audible state may have changed. + void MaybeUpdateAudibleState(); // WebContentsObserver implementation. + void WebContentsDestroyed() override; void RenderFrameDeleted(RenderFrameHost* render_frame_host) override; bool OnMessageReceived(const IPC::Message& message, RenderFrameHost* render_frame_host) override; diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc index ccadfb2dd064e..233c1e09382b4 100644 --- a/content/browser/web_contents/web_contents_impl.cc +++ b/content/browser/web_contents/web_contents_impl.cc @@ -1185,8 +1185,7 @@ void WebContentsImpl::NotifyNavigationStateChanged( "466285 WebContentsImpl::NotifyNavigationStateChanged")); // Notify the media observer of potential audibility changes. if (changed_flags & INVALIDATE_TYPE_TAB) { - media_web_contents_observer_->MaybeUpdateAudibleState( - AudioStreamMonitor::monitoring_available() && WasRecentlyAudible()); + media_web_contents_observer_->MaybeUpdateAudibleState(); } if (delegate_) diff --git a/content/content_browser.gypi b/content/content_browser.gypi index bc4ed2117b644..4369cda28c220 100644 --- a/content/content_browser.gypi +++ b/content/content_browser.gypi @@ -1002,6 +1002,8 @@ 'browser/media/android/provision_fetcher_impl.h', 'browser/media/android/url_provision_fetcher.cc', 'browser/media/android/url_provision_fetcher.h', + 'browser/media/audible_metrics.cc', + 'browser/media/audible_metrics.h', 'browser/media/audio_stream_monitor.cc', 'browser/media/audio_stream_monitor.h', 'browser/media/capture/audio_mirroring_manager.cc', diff --git a/content/content_tests.gypi b/content/content_tests.gypi index a3e511c65bade..2792d423b8ef9 100644 --- a/content/content_tests.gypi +++ b/content/content_tests.gypi @@ -504,6 +504,7 @@ 'browser/loader/upload_data_stream_builder_unittest.cc', 'browser/mach_broker_mac_unittest.cc', 'browser/media/android/media_session_uma_helper_unittest.cc', + 'browser/media/audible_metrics_unittest.cc', 'browser/media/audio_stream_monitor_unittest.cc', 'browser/media/capture/audio_mirroring_manager_unittest.cc', 'browser/media/capture/cursor_renderer_aura_unittest.cc', diff --git a/tools/metrics/actions/actions.xml b/tools/metrics/actions/actions.xml index 0606a32f5958d..47e6bbaae4879 100644 --- a/tools/metrics/actions/actions.xml +++ b/tools/metrics/actions/actions.xml @@ -7718,6 +7718,16 @@ should be able to be added at any place in this file. Please enter the description of this user action. + + mlamouri@chromium.org + A tab became audible. + + + + mlamouri@chromium.org + A tab is no longer audible. + + Please list the metric's owners. Add more owner tags as needed. Please enter the description of this user action. diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml index 3fcab8d951ae1..5b35fa3467455 100644 --- a/tools/metrics/histograms/histograms.xml +++ b/tools/metrics/histograms/histograms.xml @@ -18245,6 +18245,33 @@ http://cs/file:chrome/histograms.xml - but prefer this file for new entries. + + mlamouri@chromium.org + + Records how long more than one tab is audible at the same time. The + concurrent tabs might change during this period. The time recorded starts + when the browser goes from one to two audbile tabs and stops when it is back + below two. + + + + + mlamouri@chromium.org + + Records how many tabs were audible when a new tab started to be audible. + + + + + mlamouri@chromium.org + + Records how many tabs are audible at the same time during the session. It is + recording the maximum audible tab count everytime it increases. In other + words, a session with N concurrent audible tabs will record entries from 1 + through N exactly once. + + + henrika@chromium.org