diff options
author | mlamouri <mlamouri@chromium.org> | 2016-01-20 10:42:27 -0800 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2016-01-20 18:43:32 +0000 |
commit | 44e4ef42ba9104b06ae212042452dfc81d89032b (patch) | |
tree | 1d0e1295796fae997d44544df780e610d0d48e62 /content/browser/media | |
parent | 2f11c3c4f27f58c3737287bf90fd79ddf17a940a (diff) | |
download | chromium_src-44e4ef42ba9104b06ae212042452dfc81d89032b.zip chromium_src-44e4ef42ba9104b06ae212042452dfc81d89032b.tar.gz chromium_src-44e4ef42ba9104b06ae212042452dfc81d89032b.tar.bz2 |
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}
Diffstat (limited to 'content/browser/media')
-rw-r--r-- | content/browser/media/audible_metrics.cc | 79 | ||||
-rw-r--r-- | content/browser/media/audible_metrics.h | 49 | ||||
-rw-r--r-- | content/browser/media/audible_metrics_unittest.cc | 397 | ||||
-rw-r--r-- | content/browser/media/audio_stream_monitor.cc | 16 | ||||
-rw-r--r-- | content/browser/media/audio_stream_monitor.h | 8 | ||||
-rw-r--r-- | content/browser/media/audio_stream_monitor_unittest.cc | 87 | ||||
-rw-r--r-- | content/browser/media/media_web_contents_observer.cc | 27 | ||||
-rw-r--r-- | content/browser/media/media_web_contents_observer.h | 6 |
8 files changed, 643 insertions, 26 deletions
diff --git a/content/browser/media/audible_metrics.cc b/content/browser/media/audible_metrics.cc new file mode 100644 index 0000000..4a74028 --- /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<base::TickClock> 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 0000000..408bc8c --- /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 <set> + +#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<base::TickClock> 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<base::TickClock> clock_; + + std::set<const WebContents*> 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 0000000..1a6216b6 --- /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<WebContents*>(0x00); +static const WebContents* WEB_CONTENTS_1 = reinterpret_cast<WebContents*>(0x01); +static const WebContents* WEB_CONTENTS_2 = reinterpret_cast<WebContents*>(0x10); +static const WebContents* WEB_CONTENTS_3 = reinterpret_cast<WebContents*>(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<base::SimpleTestTickClock>(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<base::HistogramSamples> 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<AudibleMetrics> audible_metrics(new AudibleMetrics()); + } + + { + scoped_ptr<base::HistogramSamples> samples( + GetHistogramSamplesSinceTestStart( + CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM)); + EXPECT_EQ(0, samples->TotalCount()); + } + + { + scoped_ptr<base::HistogramSamples> samples( + GetHistogramSamplesSinceTestStart( + MAX_CONCURRENT_TAB_IN_SESSION_HISTOGRAM)); + EXPECT_EQ(0, samples->TotalCount()); + } + + { + scoped_ptr<base::HistogramSamples> 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<base::HistogramSamples> samples( + GetHistogramSamplesSinceTestStart( + CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(0)); + } + + { + scoped_ptr<base::HistogramSamples> samples( + GetHistogramSamplesSinceTestStart( + MAX_CONCURRENT_TAB_IN_SESSION_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(1)); + } + + { + scoped_ptr<base::HistogramSamples> 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<base::HistogramSamples> samples( + GetHistogramSamplesSinceTestStart( + CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(0)); + } + + { + scoped_ptr<base::HistogramSamples> samples( + GetHistogramSamplesSinceTestStart( + MAX_CONCURRENT_TAB_IN_SESSION_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(1)); + } + + { + scoped_ptr<base::HistogramSamples> 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<base::HistogramSamples> samples( + GetHistogramSamplesSinceTestStart( + CONCURRENT_TAB_WHEN_STARTING_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(0)); + } + + { + scoped_ptr<base::HistogramSamples> samples( + GetHistogramSamplesSinceTestStart( + MAX_CONCURRENT_TAB_IN_SESSION_HISTOGRAM)); + EXPECT_EQ(1, samples->TotalCount()); + EXPECT_EQ(1, samples->GetCount(1)); + } + + { + scoped_ptr<base::HistogramSamples> 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<base::HistogramSamples> 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<base::HistogramSamples> 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<base::HistogramSamples> 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<base::HistogramSamples> 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<base::HistogramSamples> 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<base::HistogramSamples> 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<base::HistogramSamples> 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<base::HistogramSamples> 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<base::HistogramSamples> 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<base::HistogramSamples> 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 2f6a7e4..b19e82ff 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 0eb5147..12f0605 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 d9999bf..9ecee0a 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 0591e25..67ef036 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<AudibleMetrics>::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<WebContentsImpl*>(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 6752256..4429d93 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; |