Bug 1512277 - Add Telemetry to know AudioContext blocking status if we enable blocking autoplay for web audio. r=karlt,Dexter
authoralwu <alwu@mozilla.com>
Tue, 08 Jan 2019 21:52:31 +0000
changeset 452962 54b628e16c5c87cecadbc246838e72fb1c9b4d54
parent 452961 e64d57b541e483c6464435158bf5ff6fdc35ff8e
child 452963 9635e730e0be047e046656c6fa98142547e9e664
push id35336
push userrmaries@mozilla.com
push dateWed, 09 Jan 2019 03:47:21 +0000
treeherdermozilla-central@a36388b88fcc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskarlt, Dexter
bugs1512277
milestone66.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1512277 - Add Telemetry to know AudioContext blocking status if we enable blocking autoplay for web audio. r=karlt,Dexter Differential Revision: https://phabricator.services.mozilla.com/D14118
dom/media/AutoplayPolicy.cpp
dom/media/AutoplayPolicy.h
dom/media/webaudio/AudioContext.cpp
dom/media/webaudio/AudioContext.h
toolkit/components/telemetry/Histograms.json
--- a/dom/media/AutoplayPolicy.cpp
+++ b/dom/media/AutoplayPolicy.cpp
@@ -147,21 +147,39 @@ static bool IsMediaElementAllowedToPlay(
       isAllowedMuted) {
     AUTOPLAY_LOG("Allow media %p without audio track to autoplay", &aElement);
     return true;
   }
 
   return false;
 }
 
+static bool IsAudioContextAllowedToPlay(const AudioContext& aContext) {
+  // Offline context won't directly output sound to audio devices.
+  return aContext.IsOffline() ||
+         IsWindowAllowedToPlay(aContext.GetParentObject());
+}
+
+static bool IsEnableBlockingWebAudioByUserGesturePolicy() {
+  return DefaultAutoplayBehaviour() != nsIAutoplay::ALLOWED &&
+         Preferences::GetBool("media.autoplay.block-webaudio", false) &&
+         Preferences::GetBool("media.autoplay.enabled.user-gestures-needed",
+                              false);
+}
+
 /* static */ bool AutoplayPolicy::WouldBeAllowedToPlayIfAutoplayDisabled(
     const HTMLMediaElement& aElement) {
   return IsMediaElementAllowedToPlay(aElement);
 }
 
+/* static */ bool AutoplayPolicy::WouldBeAllowedToPlayIfAutoplayDisabled(
+    const AudioContext& aContext) {
+  return IsAudioContextAllowedToPlay(aContext);
+}
+
 /* static */ bool AutoplayPolicy::IsAllowedToPlay(
     const HTMLMediaElement& aElement) {
   const uint32_t autoplayDefault = DefaultAutoplayBehaviour();
   // TODO : this old way would be removed when user-gestures-needed becomes
   // as a default option to block autoplay.
   if (!Preferences::GetBool("media.autoplay.enabled.user-gestures-needed",
                             false)) {
     // If element is blessed, it would always be allowed to play().
@@ -179,35 +197,17 @@ static bool IsMediaElementAllowedToPlay(
   AUTOPLAY_LOG("IsAllowedToPlay, mediaElement=%p, isAllowToPlay=%s", &aElement,
                result ? "allowed" : "blocked");
 
   return result;
 }
 
 /* static */ bool AutoplayPolicy::IsAllowedToPlay(
     const AudioContext& aContext) {
-  if (!Preferences::GetBool("media.autoplay.block-webaudio", false)) {
-    return true;
-  }
-
-  if (DefaultAutoplayBehaviour() == nsIAutoplay::ALLOWED) {
+  if (!IsEnableBlockingWebAudioByUserGesturePolicy()) {
     return true;
   }
 
-  if (!Preferences::GetBool("media.autoplay.enabled.user-gestures-needed",
-                            false)) {
-    return true;
-  }
-
-  // Offline context won't directly output sound to audio devices.
-  if (aContext.IsOffline()) {
-    return true;
-  }
-
-  if (IsWindowAllowedToPlay(aContext.GetParentObject())) {
-    return true;
-  }
-
-  return false;
+  return IsAudioContextAllowedToPlay(aContext);
 }
 
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/media/AutoplayPolicy.h
+++ b/dom/media/AutoplayPolicy.h
@@ -38,14 +38,22 @@ class AutoplayPolicy {
 
   // Returns true if a given media element would be allowed to play
   // if block autoplay was enabled. If this returns false, it means we would
   // either block or ask for permission.
   // Note: this is for telemetry purposes, and doesn't check the prefs
   // which enable/disable block autoplay. Do not use for blocking logic!
   static bool WouldBeAllowedToPlayIfAutoplayDisabled(
       const HTMLMediaElement& aElement);
+
+  // Returns true if a given AudioContext would be allowed to play
+  // if block autoplay was enabled. If this returns false, it means we would
+  // either block or ask for permission.
+  // Note: this is for telemetry purposes, and doesn't check the prefs
+  // which enable/disable block autoplay. Do not use for blocking logic!
+  static bool WouldBeAllowedToPlayIfAutoplayDisabled(
+      const AudioContext& aContext);
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif
--- a/dom/media/webaudio/AudioContext.cpp
+++ b/dom/media/webaudio/AudioContext.cpp
@@ -147,17 +147,20 @@ AudioContext::AudioContext(nsPIDOMWindow
       mAudioContextState(AudioContextState::Suspended),
       mNumberOfChannels(aNumberOfChannels),
       mIsOffline(aIsOffline),
       mIsStarted(!aIsOffline),
       mIsShutDown(false),
       mCloseCalled(false),
       mSuspendCalled(false),
       mIsDisconnecting(false),
-      mWasAllowedToStart(true) {
+      mWasAllowedToStart(true),
+      mWasEverAllowedToStart(false),
+      mWasEverBlockedToStart(false),
+      mWouldBeAllowedToStart(true) {
   bool mute = aWindow->AddAudioContext(this);
 
   // Note: AudioDestinationNode needs an AudioContext that must already be
   // bound to the window.
   const bool allowedToStart = AutoplayPolicy::IsAllowedToPlay(*this);
   mDestination = new AudioDestinationNode(this, aIsOffline, allowedToStart,
                                           aNumberOfChannels, aLength);
 
@@ -170,21 +173,24 @@ AudioContext::AudioContext(nsPIDOMWindow
   // transition from `suspended` to `running` until sites explicitly call
   // AudioContext.resume() or AudioScheduledSourceNode.start().
   if (!allowedToStart) {
     AUTOPLAY_LOG("AudioContext %p is not allowed to start", this);
     SuspendInternal(nullptr);
     ReportBlocked();
   }
 
+  UpdateAutoplayAssumptionStatus();
+
   FFTBlock::MainThreadInit();
 }
 
 void AudioContext::NotifyScheduledSourceNodeStarted() {
   MOZ_ASSERT(NS_IsMainThread());
+  MaybeUpdateAutoplayTelemetry();
   // Only try to start AudioContext when AudioContext was not allowed to start.
   if (mWasAllowedToStart) {
     return;
   }
 
   const bool isAllowedToPlay = AutoplayPolicy::IsAllowedToPlay(*this);
   AUTOPLAY_LOG("Trying to start AudioContext %p, IsAllowedToPlay=%d", this,
                isAllowedToPlay);
@@ -668,16 +674,20 @@ void AudioContext::BindToOwner(nsIGlobal
 
   nsCOMPtr<nsPIDOMWindowInner> newWindow = do_QueryInterface(aNew);
   if (newWindow) {
     newWindow->AddAudioContext(this);
   }
 }
 
 void AudioContext::Shutdown() {
+  // Avoid resend the Telemetry data.
+  if (!mIsShutDown) {
+    MaybeUpdateAutoplayTelemetryWhenShutdown();
+  }
   mIsShutDown = true;
 
   // We don't want to touch promises if the global is going away soon.
   if (!mIsDisconnecting) {
     if (!mIsOffline) {
       RefPtr<Promise> ignored = Close(IgnoreErrors());
     }
 
@@ -939,16 +949,18 @@ already_AddRefed<Promise> AudioContext::
   AUTOPLAY_LOG("Trying to resume AudioContext %p, IsAllowedToPlay=%d", this,
                isAllowedToPlay);
   if (isAllowedToPlay) {
     ResumeInternal();
   } else {
     ReportBlocked();
   }
 
+  MaybeUpdateAutoplayTelemetry();
+
   return promise.forget();
 }
 
 void AudioContext::ResumeInternal() {
   AUTOPLAY_LOG("Allow to resume AudioContext %p", this);
   mWasAllowedToStart = true;
 
   Destination()->Resume();
@@ -961,16 +973,55 @@ void AudioContext::ResumeInternal() {
   if (mSuspendCalled) {
     streams = GetAllStreams();
   }
   Graph()->ApplyAudioContextOperation(DestinationStream(), streams,
                                       AudioContextOperation::Resume, nullptr);
   mSuspendCalled = false;
 }
 
+void AudioContext::UpdateAutoplayAssumptionStatus() {
+  if (AutoplayPolicy::WouldBeAllowedToPlayIfAutoplayDisabled(*this)) {
+    mWasEverAllowedToStart |= true;
+    mWouldBeAllowedToStart = true;
+  } else {
+    mWasEverBlockedToStart |= true;
+    mWouldBeAllowedToStart = false;
+  }
+}
+
+void AudioContext::MaybeUpdateAutoplayTelemetry() {
+  // Exclude offline AudioContext because it's always allowed to start.
+  if (mIsOffline) {
+    return;
+  }
+
+  if (AutoplayPolicy::WouldBeAllowedToPlayIfAutoplayDisabled(*this) &&
+      !mWouldBeAllowedToStart) {
+    AccumulateCategorical(
+        mozilla::Telemetry::LABELS_WEB_AUDIO_AUTOPLAY::AllowedAfterBlocked);
+  }
+  UpdateAutoplayAssumptionStatus();
+}
+
+void AudioContext::MaybeUpdateAutoplayTelemetryWhenShutdown() {
+  // Exclude offline AudioContext because it's always allowed to start.
+  if (mIsOffline) {
+    return;
+  }
+
+  if (mWasEverAllowedToStart && !mWasEverBlockedToStart) {
+    AccumulateCategorical(
+        mozilla::Telemetry::LABELS_WEB_AUDIO_AUTOPLAY::NeverBlocked);
+  } else if (!mWasEverAllowedToStart && mWasEverBlockedToStart) {
+    AccumulateCategorical(
+        mozilla::Telemetry::LABELS_WEB_AUDIO_AUTOPLAY::NeverAllowed);
+  }
+}
+
 void AudioContext::ReportBlocked() {
   ReportToConsole(nsIScriptError::warningFlag, "BlockAutoplayWebAudioError");
   mWasAllowedToStart = false;
 
   if (!StaticPrefs::MediaBlockEventEnabled()) {
     return;
   }
 
--- a/dom/media/webaudio/AudioContext.h
+++ b/dom/media/webaudio/AudioContext.h
@@ -323,16 +323,27 @@ class AudioContext final : public DOMEve
   void SuspendInternal(void* aPromise);
 
   // Will report error message to console and dispatch testing event if needed
   // when AudioContext is blocked by autoplay policy.
   void ReportBlocked();
 
   void ReportToConsole(uint32_t aErrorFlags, const char* aMsg) const;
 
+  // This function should be called everytime we decide whether allow to start
+  // audio context, it's used to update Telemetry related variables.
+  void UpdateAutoplayAssumptionStatus();
+
+  // These functions are used for updating Telemetry.
+  // - MaybeUpdateAutoplayTelemetry: update category 'AllowedAfterBlocked'
+  // - MaybeUpdateAutoplayTelemetryWhenShutdown: update category 'NeverBlocked'
+  //   and 'NeverAllowed', so we need to call it when shutdown AudioContext
+  void MaybeUpdateAutoplayTelemetry();
+  void MaybeUpdateAutoplayTelemetryWhenShutdown();
+
  private:
   // Each AudioContext has an id, that is passed down the MediaStreams that
   // back the AudioNodes, so we can easily compute the set of all the
   // MediaStreams for a given context, on the MediasStreamGraph side.
   const AudioContextId mId;
   // Note that it's important for mSampleRate to be initialized before
   // mDestination, as mDestination's constructor needs to access it!
   const float mSampleRate;
@@ -363,16 +374,30 @@ class AudioContext final : public DOMEve
   bool mIsShutDown;
   // Close has been called, reject suspend and resume call.
   bool mCloseCalled;
   // Suspend has been called with no following resume.
   bool mSuspendCalled;
   bool mIsDisconnecting;
   // This flag stores the value of previous status of `allowed-to-start`.
   bool mWasAllowedToStart;
+
+  // These variables are used for telemetry, they're not reflect the actual
+  // status of AudioContext, they are based on the "assumption" of enabling
+  // blocking web audio. Because we want to record Telemetry no matter user
+  // enable blocking autoplay or not.
+  // - 'mWasEverAllowedToStart' would be true when AudioContext had ever been
+  //   allowed to start if we enable blocking web audio.
+  // - 'mWasEverBlockedToStart' would be true when AudioContext had ever been
+  //   blocked to start if we enable blocking web audio.
+  // - 'mWouldBeAllowedToStart' stores the value of previous status of
+  //   `allowed-to-start` if we enable blocking web audio.
+  bool mWasEverAllowedToStart;
+  bool mWasEverBlockedToStart;
+  bool mWouldBeAllowedToStart;
 };
 
 static const dom::AudioContext::AudioContextId NO_AUDIO_CONTEXT = 0;
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -14457,16 +14457,31 @@
     "expires_in_version": "69",
     "kind": "linear",
     "high": 60,
     "n_buckets": 10,
     "bug_numbers": [1490074],
     "description": "How long the AudioContext would become audible since it was created, time unit is seconds.",
     "releaseChannelCollection": "opt-out"
   },
+  "WEB_AUDIO_AUTOPLAY": {
+    "record_in_processes": ["main", "content"],
+    "alert_emails": [
+      "alwu@mozilla.com",
+      "cpearce@mozilla.com",
+      "padenot@mozilla.com",
+      "ktomlinson@mozilla.com"
+    ],
+    "expires_in_version": "70",
+    "kind": "categorical",
+    "labels": ["NeverBlocked", "AllowedAfterBlocked", "NeverAllowed"],
+    "bug_numbers": [1512277],
+    "description": "The number of times an AudioContext is allowed to start after being blocked, or the number of AudioContexts attempting to start which were never blocked or never allowed.",
+    "releaseChannelCollection": "opt-out"
+  },
   "QM_REPOSITORIES_INITIALIZATION_TIME": {
     "record_in_processes": ["main"],
     "expires_in_version": "68",
     "bug_numbers": [1481716],
     "kind": "exponential",
     "high": 30000,
     "n_buckets": 30,
     "releaseChannelCollection": "opt-out",