Bug 1336484 - Don't throttle timeouts in background tabs that are playing audio; r=baku a=gchang
authorEhsan Akhgari <ehsan@mozilla.com>
Wed, 08 Feb 2017 20:03:22 -0500
changeset 378458 c6d0cc154d82c3b3a7af5893e45e6e9313f6e9c1
parent 378457 85ae3b00692108495df1d7c54cc5fd3d70e74137
child 378459 37e8200f6c4b5f27334892d9d5f5ba897385bb7d
push id1419
push userjlund@mozilla.com
push dateMon, 10 Apr 2017 20:44:07 +0000
treeherdermozilla-release@5e6801b73ef6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku, gchang
bugs1336484
milestone53.0a2
Bug 1336484 - Don't throttle timeouts in background tabs that are playing audio; r=baku a=gchang In websites such as Facebook Live, timeout chains are used to drive the playback of a video or something similar in JavaScript. Throttling the minimum timeout values a tab playing a video from such websites in the background could make the timeout based scheduling of video playback to not work correctly, and cause audio buffer under-runs that are audible. In order to address this, other major browsers don't throttle timeouts in tabs that are playing audio. This brings us to parity to other browsers (even though we already do this for websites that use Web Audio since we've had similar bug reports using the Web Audio API.) The current audio agent setup that drives the tab audio notification icons is currently tracking whether a Window is playing audio. We use this setup to decide whether to throttle timeouts when a window goes into background.
dom/audiochannel/AudioChannelService.cpp
dom/audiochannel/AudioChannelService.h
dom/base/TimeoutManager.cpp
dom/base/nsGlobalWindow.cpp
dom/base/nsPIDOMWindow.h
dom/base/test/browser.ini
dom/base/test/browser_timeout_throttling_with_audio_playback.js
dom/base/test/file_audioLoopInIframe.html
dom/base/test/mochitest.ini
dom/base/test/plugin.js
dom/base/test/test_pluginAudioNotification.html
dom/media/webaudio/moz.build
dom/media/webaudio/test/browser.ini
dom/media/webaudio/test/browser_bug1181073.js
--- a/dom/audiochannel/AudioChannelService.cpp
+++ b/dom/audiochannel/AudioChannelService.cpp
@@ -208,16 +208,27 @@ AudioChannelService::GetOrCreate()
     return nullptr;
   }
 
   CreateServiceIfNeeded();
   RefPtr<AudioChannelService> service = gAudioChannelService.get();
   return service.forget();
 }
 
+/* static */ already_AddRefed<AudioChannelService>
+AudioChannelService::Get()
+{
+  if (sXPCOMShuttingDown) {
+    return nullptr;
+  }
+
+  RefPtr<AudioChannelService> service = gAudioChannelService.get();
+  return service.forget();
+}
+
 /* static */ PRLogModuleInfo*
 AudioChannelService::GetAudioChannelLog()
 {
   static PRLogModuleInfo *gAudioChannelLog;
   if (!gAudioChannelLog) {
     gAudioChannelLog = PR_NewLogModule("AudioChannel");
   }
   return gAudioChannelLog;
@@ -908,16 +919,27 @@ AudioChannelService::IsAudioChannelActiv
                                           bool* aActive)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   auto* window = nsPIDOMWindowOuter::From(aWindow)->GetScriptableTop();
   *aActive = IsAudioChannelActive(window, (AudioChannel)aAudioChannel);
   return NS_OK;
 }
+
+bool
+AudioChannelService::IsWindowActive(nsPIDOMWindowOuter* aWindow)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  auto* window = nsPIDOMWindowOuter::From(aWindow)->GetScriptableTop();
+  AudioChannelWindow* winData = GetOrCreateWindowData(window);
+  return !winData->mAudibleAgents.IsEmpty();
+}
+
 void
 AudioChannelService::SetDefaultVolumeControlChannel(int32_t aChannel,
                                                     bool aVisible)
 {
   SetDefaultVolumeControlChannelInternal(aChannel, aVisible,
                                          CONTENT_PROCESS_ID_MAIN);
 }
 
--- a/dom/audiochannel/AudioChannelService.h
+++ b/dom/audiochannel/AudioChannelService.h
@@ -84,21 +84,27 @@ public:
   enum AudibleChangedReasons : uint32_t {
     eVolumeChanged = 0,
     eDataAudibleChanged = 1,
     ePauseStateChanged = 2
   };
 
   /**
    * Returns the AudioChannelServce singleton.
-   * If AudioChannelServce is not exist, create and return new one.
+   * If AudioChannelService doesn't exist, create and return new one.
    * Only to be called from main thread.
    */
   static already_AddRefed<AudioChannelService> GetOrCreate();
 
+  /**
+   * Returns the AudioChannelService singleton if one exists.
+   * If AudioChannelService doesn't exist, returns null.
+   */
+  static already_AddRefed<AudioChannelService> Get();
+
   static bool IsAudioChannelMutedByDefault();
 
   static PRLogModuleInfo* GetAudioChannelLog();
 
   static bool IsEnableAudioCompeting();
 
   /**
    * Any audio channel agent that starts playing should register itself to
@@ -143,16 +149,18 @@ public:
 
   bool GetAudioChannelMuted(nsPIDOMWindowOuter* aWindow, AudioChannel aChannel);
 
   void SetAudioChannelMuted(nsPIDOMWindowOuter* aWindow, AudioChannel aChannel,
                             bool aMuted);
 
   bool IsAudioChannelActive(nsPIDOMWindowOuter* aWindow, AudioChannel aChannel);
 
+  bool IsWindowActive(nsPIDOMWindowOuter* aWindow);
+
   /**
    * Return true if there is a telephony channel active in this process
    * or one of its subprocesses.
    */
   bool TelephonyChannelIsActive();
 
   /**
    * Return true if a normal or content channel is active for the given
--- a/dom/base/TimeoutManager.cpp
+++ b/dom/base/TimeoutManager.cpp
@@ -25,19 +25,20 @@ static int32_t              gRunningTime
 static int32_t gMinTimeoutValue = 0;
 static int32_t gMinBackgroundTimeoutValue = 0;
 static int32_t gMinTrackingTimeoutValue = 0;
 static int32_t gMinTrackingBackgroundTimeoutValue = 0;
 int32_t
 TimeoutManager::DOMMinTimeoutValue(bool aIsTracking) const {
   // First apply any back pressure delay that might be in effect.
   int32_t value = std::max(mBackPressureDelayMS, 0);
-  // Don't use the background timeout value when there are audio contexts
-  // present, so that background audio can keep running smoothly. (bug 1181073)
-  bool isBackground = !mWindow.AsInner()->HasAudioContexts() &&
+  // Don't use the background timeout value when the tab is playing audio.
+  // Until bug 1336484 we only used to do this for pages that use Web Audio.
+  // The original behavior was implemented in bug 11811073.
+  bool isBackground = !mWindow.AsInner()->IsPlayingAudio() &&
     mWindow.IsBackgroundInternal();
   auto minValue = aIsTracking ? (isBackground ? gMinTrackingBackgroundTimeoutValue
                                               : gMinTrackingTimeoutValue)
                               : (isBackground ? gMinBackgroundTimeoutValue
                                               : gMinTimeoutValue);
   return std::max(minValue, value);
 }
 
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -3908,19 +3908,23 @@ nsPIDOMWindowInner::Thaw()
 
 void
 nsPIDOMWindowInner::SyncStateFromParentWindow()
 {
   nsGlobalWindow::Cast(this)->SyncStateFromParentWindow();
 }
 
 bool
-nsPIDOMWindowInner::HasAudioContexts() const
-{
-  return !mAudioContexts.IsEmpty();
+nsPIDOMWindowInner::IsPlayingAudio()
+{
+  RefPtr<AudioChannelService> acs = AudioChannelService::Get();
+  if (!acs) {
+    return false;
+  }
+  return acs->IsWindowActive(GetOuterWindow());
 }
 
 mozilla::dom::TimeoutManager&
 nsPIDOMWindowInner::TimeoutManager()
 {
   return *mTimeoutManager;
 }
 
--- a/dom/base/nsPIDOMWindow.h
+++ b/dom/base/nsPIDOMWindow.h
@@ -873,17 +873,17 @@ public:
   // calls.
   void Freeze();
   void Thaw();
 
   // Apply the parent window's suspend, freeze, and modal state to the current
   // window.
   void SyncStateFromParentWindow();
 
-  bool HasAudioContexts() const;
+  bool IsPlayingAudio();
 
   mozilla::dom::TimeoutManager& TimeoutManager();
 
   bool IsRunningTimeout();
 
 protected:
   void CreatePerformanceObjectIfNeeded();
 };
--- a/dom/base/test/browser.ini
+++ b/dom/base/test/browser.ini
@@ -1,28 +1,35 @@
 [DEFAULT]
 support-files =
+  audio.ogg
   empty.html
+  file_audioLoop.html
+  file_audioLoopInIframe.html
   file_bug1011748_redirect.sjs
   file_bug1011748_OK.sjs
   file_messagemanager_unload.html
+  file_pluginAudio.html
   file_use_counter_outer.html
   file_use_counter_svg_getElementById.svg
   file_use_counter_svg_currentScale.svg
   file_use_counter_svg_fill_pattern_definition.svg
   file_use_counter_svg_fill_pattern.svg
   file_use_counter_svg_fill_pattern_internal.svg
   file_use_counter_svg_fill_pattern_data.svg
+  file_webaudioLoop.html
+  plugin.js
 
 [browser_bug593387.js]
 [browser_bug902350.js]
 tags = mcb
 [browser_bug1011748.js]
 [browser_bug1058164.js]
 [browser_messagemanager_loadprocessscript.js]
 [browser_messagemanager_targetframeloader.js]
 [browser_messagemanager_unload.js]
 [browser_pagehide_on_tab_close.js]
 skip-if = e10s # this tests non-e10s behavior. it's not expected to work in e10s.
 [browser_state_notifications.js]
 skip-if = true # Bug 1271028
 [browser_use_counters.js]
 [browser_bug1307747.js]
+[browser_timeout_throttling_with_audio_playback.js]
new file mode 100644
--- /dev/null
+++ b/dom/base/test/browser_timeout_throttling_with_audio_playback.js
@@ -0,0 +1,61 @@
+const kBaseURI = "http://mochi.test:8888/browser/dom/base/test/empty.html";
+const kPluginJS = "chrome://mochitests/content/browser/dom/base/test/plugin.js";
+var testURLs = [
+  "http://mochi.test:8888/browser/dom/base/test/file_audioLoop.html",
+  "http://mochi.test:8888/browser/dom/base/test/file_audioLoopInIframe.html",
+  "http://mochi.test:8888/browser/dom/base/test/file_pluginAudio.html",
+  "http://mochi.test:8888/browser/dom/base/test/file_webaudioLoop.html",
+];
+
+// We want to ensure that while audio is being played back, a background tab is
+// treated the same as a foreground tab as far as timeout throttling is concerned.
+// So we use a 10ms minimum timeout value for foreground tabs and a 100,000 second
+// minimum timeout value for background tabs.  This means that in case the test
+// fails, it will time out in practice, but just for sanity the test condition
+// ensures that the observed timeout delay falls in this range.
+const kMinTimeoutForeground = 10;
+const kMinTimeoutBackground = 100 * 1000 * 1000;
+
+Services.scriptloader.loadSubScript(kPluginJS, this);
+
+function* runTest(url) {
+  let currentTab = gBrowser.selectedTab;
+  let newTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, kBaseURI);
+  let newBrowser = gBrowser.getBrowserForTab(newTab);
+
+  // Wait for the UI to indicate that audio is being played back.
+  let promise = BrowserTestUtils.waitForAttribute("soundplaying", newTab, "true");
+  newBrowser.loadURI(url);
+  yield promise;
+
+  // Put the tab in the background.
+  yield BrowserTestUtils.switchTab(gBrowser, currentTab);
+
+  let timeout = yield ContentTask.spawn(newBrowser, {}, function() {
+    return new Promise(resolve => {
+      let before = new Date();
+      content.window.setTimeout(function() {
+        let after = new Date();
+        resolve(after - before);
+      }, 0);
+    });
+  });
+  ok(timeout >= kMinTimeoutForeground &&
+     timeout <= kMinTimeoutBackground, `Got the correct timeout (${timeout})`);
+
+  // All done.
+  yield BrowserTestUtils.removeTab(newTab);
+}
+
+add_task(function* setup() {
+  yield SpecialPowers.pushPrefEnv({"set": [
+    ["dom.min_timeout_value", kMinTimeoutForeground],
+    ["dom.min_background_timeout_value", kMinTimeoutBackground],
+  ]});
+});
+
+add_task(function* test() {
+  for (var url of testURLs) {
+    yield runTest(url);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/dom/base/test/file_audioLoopInIframe.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<iframe src="file_audioLoop.html"></iframe>
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -186,16 +186,17 @@ support-files =
   file_youtube_flash_embed.html
   forRemoval.resource
   forRemoval.resource^headers^
   formReset.html
   invalid_accesscontrol.resource
   invalid_accesscontrol.resource^headers^
   mutationobserver_dialog.html
   orientationcommon.js
+  plugin.js
   script-1_bug597345.sjs
   script-2_bug597345.js
   script_bug602838.sjs
   send_gzip_content.sjs
   somedatas.resource
   somedatas.resource^headers^
   variable_style_sheet.sjs
   viewport_helpers.js
new file mode 100644
--- /dev/null
+++ b/dom/base/test/plugin.js
@@ -0,0 +1,32 @@
+// Copied from /dom/plugins/test/mochitest/utils.js
+function getTestPlugin(pluginName) {
+  var ph = SpecialPowers.Cc["@mozilla.org/plugin/host;1"]
+                                 .getService(SpecialPowers.Ci.nsIPluginHost);
+  var tags = ph.getPluginTags();
+  var name = pluginName || "Test Plug-in";
+  for (var tag of tags) {
+    if (tag.name == name) {
+      return tag;
+    }
+  }
+
+  ok(false, "Could not find plugin tag with plugin name '" + name + "'");
+  return null;
+}
+// Copied from /dom/plugins/test/mochitest/utils.js
+function setTestPluginEnabledState(newEnabledState, pluginName) {
+  var oldEnabledState = SpecialPowers.setTestPluginEnabledState(newEnabledState, pluginName);
+  if (!oldEnabledState) {
+    return;
+  }
+  var plugin = getTestPlugin(pluginName);
+  while (plugin.enabledState != newEnabledState) {
+    // Run a nested event loop to wait for the preference change to
+    // propagate to the child. Yuck!
+    SpecialPowers.Services.tm.currentThread.processNextEvent(true);
+  }
+  SimpleTest.registerCleanupFunction(function() {
+    SpecialPowers.setTestPluginEnabledState(oldEnabledState, pluginName);
+  });
+}
+setTestPluginEnabledState(SpecialPowers.Ci.nsIPluginTag.STATE_ENABLED);
--- a/dom/base/test/test_pluginAudioNotification.html
+++ b/dom/base/test/test_pluginAudioNotification.html
@@ -1,55 +1,23 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <title>Test for audio controller in windows</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="plugin.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 <pre id="test">
 </pre>
 <iframe></iframe>
 
 <script type="application/javascript">
 
-// Copied from /dom/plugins/test/mochitest/utils.js
-function getTestPlugin(pluginName) {
-  var ph = SpecialPowers.Cc["@mozilla.org/plugin/host;1"]
-                                 .getService(SpecialPowers.Ci.nsIPluginHost);
-  var tags = ph.getPluginTags();
-  var name = pluginName || "Test Plug-in";
-  for (var tag of tags) {
-    if (tag.name == name) {
-      return tag;
-    }
-  }
-
-  ok(false, "Could not find plugin tag with plugin name '" + name + "'");
-  return null;
-}
-// Copied from /dom/plugins/test/mochitest/utils.js
-function setTestPluginEnabledState(newEnabledState, pluginName) {
-  var oldEnabledState = SpecialPowers.setTestPluginEnabledState(newEnabledState, pluginName);
-  if (!oldEnabledState) {
-    return;
-  }
-  var plugin = getTestPlugin(pluginName);
-  while (plugin.enabledState != newEnabledState) {
-    // Run a nested event loop to wait for the preference change to
-    // propagate to the child. Yuck!
-    SpecialPowers.Services.tm.currentThread.processNextEvent(true);
-  }
-  SimpleTest.registerCleanupFunction(function() {
-    SpecialPowers.setTestPluginEnabledState(oldEnabledState, pluginName);
-  });
-}
-setTestPluginEnabledState(SpecialPowers.Ci.nsIPluginTag.STATE_ENABLED);
-
 SimpleTest.waitForExplicitFinish();
 
 var expectedNotification = null;
 var iframe = null;
 
 var observer = {
   observe: function(subject, topic, data) {
     is(topic, "audio-playback", "audio-playback received");
--- a/dom/media/webaudio/moz.build
+++ b/dom/media/webaudio/moz.build
@@ -11,20 +11,16 @@ DIRS += ['blink']
 
 TEST_DIRS += ['gtest']
 
 MOCHITEST_MANIFESTS += [
     'test/blink/mochitest.ini',
     'test/mochitest.ini',
 ]
 
-BROWSER_CHROME_MANIFESTS += [
-    'test/browser.ini',
-]
-
 TEST_HARNESS_FILES.testing.mochitest.tests.dom.media.webaudio.test.blink += [
     'test/blink/audio-testing.js',
     'test/blink/convolution-testing.js',
     'test/blink/panner-model-testing.js',
 ]
 
 EXPORTS += [
     'AlignedTArray.h',
deleted file mode 100644
--- a/dom/media/webaudio/test/browser.ini
+++ /dev/null
@@ -1,1 +0,0 @@
-[browser_bug1181073.js]
\ No newline at end of file
deleted file mode 100644
--- a/dom/media/webaudio/test/browser_bug1181073.js
+++ /dev/null
@@ -1,40 +0,0 @@
-add_task(function*() {
-  // Make the min_background_timeout_value very high to avoid problems on slow machines
-  yield SpecialPowers.pushPrefEnv({
-    'set': [['dom.min_background_timeout_value', 3000]]
-  });
-
-  // Make a new tab, and put it in the background
-  yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
-    yield BrowserTestUtils.withNewTab("about:blank", function*() {
-      let time = yield ContentTask.spawn(browser, null, function () {
-        return new Promise(resolve => {
-          let start = content.performance.now();
-          let id = content.window.setInterval(function() {
-            let end = content.performance.now();
-            content.window.clearInterval(id);
-            resolve(end - start);
-          }, 0);
-        });
-      });
-
-      ok(time > 2000, "Interval is throttled with no webaudio (" + time + " ms)");
-
-      time = yield ContentTask.spawn(browser, null, function () {
-        return new Promise(resolve => {
-          // Create an audio context, and save it on the window so it doesn't get GCed
-          content.window._audioCtx = new content.window.AudioContext();
-
-          let start = content.performance.now();
-          let id = content.window.setInterval(function() {
-            let end = content.performance.now();
-            content.window.clearInterval(id);
-            resolve(end - start);
-          }, 0);
-        });
-      });
-
-      ok(time < 1000, "Interval is not throttled with an audio context present (" + time + " ms)");
-    });
-  });
-});