Bug 1248507 - p5. DecoderDoctorDiagnostics implementation - r?jya,bz draft
authorGerald Squelart <gsquelart@mozilla.com>
Thu, 14 Apr 2016 20:20:16 +1000
changeset 350859 35ca65e34e01b29e1c804143c273bf33159b5ab5
parent 350858 2aff1967fcf65c559ebca901282ecc205a1a26e6
child 350860 228503bdd9a1a2d339e4af8d653d6bebe88d1b1e
push id15430
push usergsquelart@mozilla.com
push dateThu, 14 Apr 2016 10:20:51 +0000
reviewersjya, bz
bugs1248507
milestone48.0a1
Bug 1248507 - p5. DecoderDoctorDiagnostics implementation - r?jya,bz Minimal implementation of DecoderDoctorDiagnostics. If the Decoder Doctor analysis needs to report something, a notification is logged to the web console, with the media format(s) that cannot be decoded. In this patch, there are only two notification types: "Cannot play" when no decoders are found for any of the requested formats), or "Can play" (if pref "media.decoderdoctor.verbose" is true) when decoders are missing for only some of the requested formats. MozReview-Commit-ID: 4QkiVvcNSU3
dom/media/DecoderDoctorDiagnostics.cpp
dom/media/moz.build
--- a/dom/media/DecoderDoctorDiagnostics.cpp
+++ b/dom/media/DecoderDoctorDiagnostics.cpp
@@ -2,34 +2,461 @@
 /* vim:set ts=2 sw=2 sts=2 et cindent: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "DecoderDoctorDiagnostics.h"
 
 #include "mozilla/Logging.h"
+#include "nsIDocument.h"
+#include "nsIDocumentActivity.h"
+#include "nsITimer.h"
+#include "nsIWeakReference.h"
 
 static mozilla::LazyLogModule sDecoderDoctorLog("DecoderDoctor");
 #define DD_LOG(level, arg, ...) MOZ_LOG(sDecoderDoctorLog, level, (arg, ##__VA_ARGS__))
 #define DD_DEBUG(arg, ...) DD_LOG(mozilla::LogLevel::Debug, arg, ##__VA_ARGS__)
+#define DD_INFO(arg, ...) DD_LOG(mozilla::LogLevel::Info, arg, ##__VA_ARGS__)
 #define DD_WARN(arg, ...) DD_LOG(mozilla::LogLevel::Warning, arg, ##__VA_ARGS__)
 
 namespace mozilla
 {
 
+// Class that collects a sequence of diagnostics from the same document over a
+// small period of time, in order to provide a synthesized analysis.
+// Not thread-safe, incoming calls should come from DecoderDoctorService and
+// own timers.
+// Created and owned by DecoderDoctorService, until it deregisters itself.
+class DecoderDoctorDocumentWatcher : public nsIDocumentActivity
+                                   , public nsITimerCallback
+{
+public:
+  explicit DecoderDoctorDocumentWatcher(nsIDocument* aDocument);
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIDOCUMENTACTIVITY
+  NS_DECL_NSITIMERCALLBACK
+
+  void AddAnalysis(const nsAString& aFormat,
+                   const char* aCallSite,
+                   DecoderDoctorDiagnostics&& aDiagnostics);
+
+  // Get the watched document if still alive, or null.
+  // May StopWatching() if document was just destroyed.
+  nsCOMPtr<nsIDocument> GetDocument();
+
+private:
+  virtual ~DecoderDoctorDocumentWatcher();
+
+  // This will prevent further work from happening, and watcher will deregister
+  // itself from Service and soon die.
+  void StopWatching();
+
+  static const uint32_t sAnalysisPeriod_ms = 1000;
+  void EnsureTimerIsStarted();
+
+  void ReportAnalysis(const char* aReportStringId,
+                      const nsAString& aFormats);
+
+  void SynthesizeAnalysis();
+
+  // Weak pointer to an nsIDocument.
+  // Must be non-null during construction.
+  // Nulled when we want to stop watching, because either:
+  // 0. Trying to get hold of a strong reference failed, indicating that the
+  //    document has already been destroyed.
+  // 1. As nsIDocumentActivity we have received a
+  //    NotifyOwnerDocumentActivityChanged where the inner window was null,
+  //    which indicates that the document has been detached from its window
+  //    and will (probably) get destroyed soon.
+  //    Note that we expect the document to stay alive at least until then.
+  // 2. We have not received new diagnostic information within a short time
+  //    period, so we just stop watching.
+  nsWeakPtr mDocumentWeakPtr;
+  // Original raw pointer, only used for logging. 'void*' to avoid misuse.
+  void* mDocumentOriginalPtr;
+
+  struct Diagnostics
+  {
+    Diagnostics(DecoderDoctorDiagnostics&& aDiagnostics,
+                const nsAString& aFormat,
+                const char* aCallSite)
+      : mDecoderDoctorDiagnostics(Move(aDiagnostics))
+      , mFormat(aFormat)
+      , mCallSite(aCallSite)
+    {}
+    Diagnostics(const Diagnostics&) = delete;
+    Diagnostics(Diagnostics&& aOther)
+      : mDecoderDoctorDiagnostics(Move(aOther.mDecoderDoctorDiagnostics))
+      , mFormat(Move(aOther.mFormat))
+      , mCallSite(Move(aOther.mCallSite))
+    {}
+
+    const DecoderDoctorDiagnostics mDecoderDoctorDiagnostics;
+    const nsString mFormat;
+    const nsCString mCallSite;
+  };
+  typedef nsTArray<Diagnostics> DiagnosticsSequence;
+  DiagnosticsSequence mDiagnosticsSequence;
+
+  nsCOMPtr<nsITimer> mTimer; // Keep timer alive until we run.
+  DiagnosticsSequence::size_type mDiagnosticsHandled = 0;
+};
+
+
+// Class that keeps track of all DecoderDoctorDocumentWatcher's.
+class DecoderDoctorService
+{
+public:
+  static DecoderDoctorService& GetService();
+
+  void AddAnalysis(nsIDocument* aDocument,
+                   const nsAString& aFormat,
+                   const char* aCallSite,
+                   DecoderDoctorDiagnostics&& aDiagnostics);
+
+  void DeregisterDocumentWatcher(nsIDocument* aDocument);
+
+private:
+  // Instance, should only exists when there are active watchers.
+  static DecoderDoctorService* sDecoderDoctorService;
+
+  // Main owning references to watchers.
+  // New watchers are created when a first analysis arrives for a document.
+  // Watchers deregisters themselves later on, which will probably destroy them
+  // when short-lived self-references die too.
+  // If a watcher is found for a given nsIDocument*, it should be checked that
+  // it does internally match the exact same document (not just one that
+  // appears at the same memory location).
+  std::map<nsIDocument*, RefPtr<DecoderDoctorDocumentWatcher>> mWatchers;
+
+  // True when currently in the process of adding an analysis.
+  // This is to prevent killing the service if it kills the last watcher before
+  // adding a new one.
+  bool mAddingAnalysis = false;
+};
+
+
+NS_IMPL_ISUPPORTS(DecoderDoctorDocumentWatcher,
+                  nsIDocumentActivity, nsITimerCallback)
+
+DecoderDoctorDocumentWatcher::DecoderDoctorDocumentWatcher(nsIDocument* aDocument)
+  : mDocumentWeakPtr(do_GetWeakReference(aDocument))
+  , mDocumentOriginalPtr(aDocument)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mDocumentOriginalPtr);
+  MOZ_ASSERT(mDocumentWeakPtr);
+
+  DD_DEBUG("DecoderDoctorDocumentWatcher[%p]::DecoderDoctorDocumentWatcher(doc=%p)",
+           this, aDocument);
+  aDocument->RegisterActivityObserver(
+    NS_ISUPPORTS_CAST(nsIDocumentActivity*, this));
+}
+
+DecoderDoctorDocumentWatcher::~DecoderDoctorDocumentWatcher()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::~DecoderDoctorDocumentWatcher()",
+           this, mDocumentOriginalPtr);
+  // mDocumentWeakPtr should have been reset through StopWatching()!
+  MOZ_ASSERT(!mDocumentWeakPtr);
+}
+
+nsCOMPtr<nsIDocument>
+DecoderDoctorDocumentWatcher::GetDocument()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  nsCOMPtr<nsIDocument> document;
+  if (mDocumentWeakPtr) {
+    document = do_QueryReferent(mDocumentWeakPtr);
+    if (!document) {
+      // Document has been destroyed.
+      StopWatching();
+    }
+  }
+  return document;
+}
+
+void
+DecoderDoctorDocumentWatcher::StopWatching()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  // StopWatching() shouldn't be called twice.
+  MOZ_ASSERT(mDocumentWeakPtr);
+
+  // Keep 'this' alive in this method and the dispatched task.
+  RefPtr<DecoderDoctorDocumentWatcher> self = this;
+
+  // Deregister from the Decoder Doctor service now, so we don't receive more
+  // analysis data. (A new one might get started, that's okay.)
+  DecoderDoctorService::GetService().DeregisterDocumentWatcher(
+    reinterpret_cast<nsIDocument*>(mDocumentOriginalPtr));
+
+  // Keep local weak reference to the document, for the dispatched task.
+  // Forget document outside. This will prevent more work from being started.
+  nsWeakPtr documentWeakPtr;
+  documentWeakPtr.swap(mDocumentWeakPtr);
+
+  // One does not simply removes oneself from watchers, as they sometimes do
+  // not like being disturbed while busy notifying listeners.
+  // Schedule a task to do that when things have quietened.
+  nsCOMPtr<nsIRunnable> task =
+    NS_NewRunnableFunction([self, documentWeakPtr]() {
+      if (self->mTimer) {
+        DD_DEBUG("DecoderDoctorDocumentWatcher[%p]::StopWatching()::task - cancel and forget mTimer[%p]",
+                 self.get(), self->mTimer.get());
+        self->mTimer->Cancel();
+        self->mTimer = nullptr;
+      }
+      nsCOMPtr<nsIDocument> document = do_QueryReferent(documentWeakPtr);
+      if (document) {
+        DD_DEBUG("DecoderDoctorDocumentWatcher[%p]::StopWatching()::task - Unregister from nsIDocument[%p]",
+                 self.get(), document.get());
+        document->UnregisterActivityObserver(
+          NS_ISUPPORTS_CAST(nsIDocumentActivity*, self.get()));
+      }
+    });
+  NS_DispatchToMainThread(task.forget(), NS_DISPATCH_NORMAL);
+}
+
+void
+DecoderDoctorDocumentWatcher::EnsureTimerIsStarted()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (!mTimer) {
+    mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+    if (NS_WARN_IF(!mTimer)) {
+      return;
+    }
+    if (NS_WARN_IF(NS_FAILED(
+          mTimer->InitWithCallback(
+            this, sAnalysisPeriod_ms, nsITimer::TYPE_ONE_SHOT)))) {
+      mTimer = nullptr;
+    }
+  }
+}
+
+void
+DecoderDoctorDocumentWatcher::ReportAnalysis(const char* aReportStringId,
+                                             const nsAString& aFormats)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  nsCOMPtr<nsIDocument> document = GetDocument();
+  if (!document) {
+    return;
+  }
+
+  const char16_t* params[] = { aFormats.Data() };
+  DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::ReportAnalysis() ReportToConsole - aMsg='%s' params[0]='%s'",
+           this, mDocumentOriginalPtr, aReportStringId,
+           NS_ConvertUTF16toUTF8(params[0]).get());
+  nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+                                  NS_LITERAL_CSTRING("Media"),
+                                  document,
+                                  nsContentUtils::eDOM_PROPERTIES,
+                                  aReportStringId,
+                                  params,
+                                  ArrayLength(params));
+}
+
+void
+DecoderDoctorDocumentWatcher::SynthesizeAnalysis()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  bool canPlay = false;
+  nsAutoString formats;
+  for (auto& diag : mDiagnosticsSequence) {
+    if (diag.mDecoderDoctorDiagnostics.CanPlay()) {
+      canPlay = true;
+    } else {
+      if (!formats.IsEmpty()) {
+        formats += NS_LITERAL_STRING(", ");
+      }
+      formats += diag.mFormat;
+    }
+  }
+  if (!canPlay) {
+    DD_WARN("DecoderDoctorDocumentWatcher[%p, doc=%p]::Notify() - Cannot play media, formats: %s",
+            this, mDocumentOriginalPtr, NS_ConvertUTF16toUTF8(formats).get());
+    ReportAnalysis("MediaCannotPlayNoDecoders", formats);
+  } else if (!formats.IsEmpty()) {
+    DD_INFO("DecoderDoctorDocumentWatcher[%p, doc=%p]::Notify() - Can play media, but no decoders for some requested formats: %s",
+            this, mDocumentOriginalPtr, NS_ConvertUTF16toUTF8(formats).get());
+    if (Preferences::GetBool("media.decoderdoctor.verbose", false)) {
+      ReportAnalysis("MediaNoDecoders", formats);
+    }
+  } else {
+    DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::Notify() - Can play media, decoders available for all requested formats",
+             this, mDocumentOriginalPtr);
+  }
+}
+
+void
+DecoderDoctorDocumentWatcher::AddAnalysis(const nsAString& aFormat,
+                                          const char* aCallSite,
+                                          DecoderDoctorDiagnostics&& aDiagnostics)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  nsCOMPtr<nsIDocument> document = GetDocument();
+  if (!document) {
+    return;
+  }
+
+  DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::AddAnalysis(format='%s', call site '%s', can play=%d)",
+           this, mDocumentOriginalPtr, NS_ConvertUTF16toUTF8(aFormat).get(),
+           aCallSite, aDiagnostics.CanPlay());
+  mDiagnosticsSequence.AppendElement(
+    Diagnostics(Move(aDiagnostics), aFormat, aCallSite));
+  EnsureTimerIsStarted();
+}
+
+void
+DecoderDoctorDocumentWatcher::NotifyOwnerDocumentActivityChanged()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  nsCOMPtr<nsIDocument> document = GetDocument();
+  if (!document) {
+    return;
+  }
+
+  if (!document->GetInnerWindow()) {
+    DD_DEBUG("DecoderDoctorDocumentWatcher[%p,doc=%p]::NotifyOwnerDocumentActivityChanged() - innerwindow=0 -> Stop watching",
+             this, mDocumentOriginalPtr);
+    // No more inner window -> Assume document is about to die, stop watching.
+    StopWatching();
+  }
+}
+
+NS_IMETHODIMP
+DecoderDoctorDocumentWatcher::Notify(nsITimer* timer)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(timer == mTimer);
+
+  // Forget timer. (Assuming timer keeps itself and us alive during this call.)
+  mTimer = nullptr;
+
+  nsCOMPtr<nsIDocument> document = GetDocument();
+  if (!document) {
+    return NS_OK;
+  }
+
+  if (mDiagnosticsSequence.Length() > mDiagnosticsHandled) {
+    // We have new diagnostic data.
+    mDiagnosticsHandled = mDiagnosticsSequence.Length();
+
+    SynthesizeAnalysis();
+
+    // Restart timer, to redo analysis or stop watching this document,
+    // depending on whether anything new happens.
+    EnsureTimerIsStarted();
+  } else {
+    DD_DEBUG("DecoderDoctorDocumentWatcher[%p, doc=%p]::Notify() - No new diagnostics to analyze -> Stop watching",
+             this, mDocumentOriginalPtr);
+    // Stop watching this document, we don't expect more diagnostics for now.
+    // If more diagnostics come in, we'll treat them as another burst, separately.
+    StopWatching();
+  }
+
+  return NS_OK;
+}
+
+
+//static
+DecoderDoctorService* DecoderDoctorService::sDecoderDoctorService = nullptr;
+
+//static
+DecoderDoctorService&
+DecoderDoctorService::GetService()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (!sDecoderDoctorService) {
+    sDecoderDoctorService = new DecoderDoctorService();
+  }
+  return *sDecoderDoctorService;
+}
+
+void
+DecoderDoctorService::AddAnalysis(nsIDocument* aDocument,
+                                  const nsAString& aFormat,
+                                  const char* aCallSite,
+                                  DecoderDoctorDiagnostics&& aDiagnostics)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aDocument);
+  if (!aDocument->GetInnerWindow()) {
+    // No (more) inner window -> Don't watch. We shouldn't be watching anyway.
+    MOZ_ASSERT(!mWatchers[aDocument]);
+    return;
+  }
+
+  mAddingAnalysis = true;
+
+  // Re-use existing watcher, or create a null-ref for it in the map.
+  RefPtr<DecoderDoctorDocumentWatcher> watcher = mWatchers[aDocument];
+
+  if (watcher) {
+    // There is a watcher associated with this nsIDocument*, but does it really
+    // point to the exact same document?
+    // (e.g. it could happen that a previous document existed at the same
+    // address but has been destroyed since, so we need to see if this
+    // watcher's weak-pointed-at document is still there.)
+    nsCOMPtr<nsIDocument> document = watcher->GetDocument();
+    if (document) {
+      // Watcher still pointing at a real document, keep it.
+      MOZ_ASSERT(document == aDocument);
+    } else {
+      // Watcher to a non-existent document, it must have deregistered itself.
+      MOZ_ASSERT(!mWatchers[aDocument]);
+      // Drop it.
+      watcher = nullptr;
+    }
+  }
+
+  if (!watcher) {
+    watcher = new DecoderDoctorDocumentWatcher(aDocument);
+    mWatchers[aDocument] = watcher;
+  }
+
+  watcher->AddAnalysis(aFormat, aCallSite, Move(aDiagnostics));
+
+  mAddingAnalysis = false;
+}
+
+void
+DecoderDoctorService::DeregisterDocumentWatcher(nsIDocument* aDocument)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  mWatchers.erase(aDocument);
+  if (mWatchers.empty() && !mAddingAnalysis) {
+    // No more watchers (and we're not trying to add a new one) -> Kill service.
+    delete sDecoderDoctorService;
+    sDecoderDoctorService = nullptr;
+  }
+}
+
+
 void
 DecoderDoctorDiagnostics::StoreDiagnostics(nsIDocument* aDocument,
                                            const nsAString& aFormat,
                                            const char* aCallSite)
 {
+  MOZ_ASSERT(NS_IsMainThread());
   if (NS_WARN_IF(!aDocument)) {
     DD_WARN("DecoderDoctorDiagnostics[%p]::StoreDiagnostics(nsIDocument* aDocument=nullptr, format='%s', call site '%s')",
             this, NS_ConvertUTF16toUTF8(aFormat).get(), aCallSite);
     return;
   }
 
-  // TODO: Actually analyze data.
-  DD_DEBUG("DecoderDoctorDiagnostics[%p]::StoreDiagnostics(nsIDocument* aDocument=%p, format='%s', call site '%s')",
-           this, aDocument, NS_ConvertUTF16toUTF8(aFormat).get(), aCallSite);
+  // StoreDiagnostics should only be called once after all data is available,
+  // so it is safe to Move() from this object.
+  DecoderDoctorService::GetService().AddAnalysis(aDocument, aFormat, aCallSite, Move(*this));
 }
 
 } // namespace mozilla
--- a/dom/media/moz.build
+++ b/dom/media/moz.build
@@ -92,16 +92,17 @@ EXPORTS += [
     'AudioMixer.h',
     'AudioPacketizer.h',
     'AudioSampleFormat.h',
     'AudioSegment.h',
     'AudioStream.h',
     'Benchmark.h',
     'BufferMediaResource.h',
     'CubebUtils.h',
+    'DecoderDoctorDiagnostics.h',
     'DecoderTraits.h',
     'DOMMediaStream.h',
     'EncodedBufferCache.h',
     'FileBlockCache.h',
     'FlushableTaskQueue.h',
     'FrameStatistics.h',
     'Intervals.h',
     'Latency.h',
@@ -199,16 +200,17 @@ UNIFIED_SOURCES += [
     'AudioSegment.cpp',
     'AudioStream.cpp',
     'AudioStreamTrack.cpp',
     'AudioTrack.cpp',
     'AudioTrackList.cpp',
     'Benchmark.cpp',
     'CanvasCaptureMediaStream.cpp',
     'CubebUtils.cpp',
+    'DecoderDoctorDiagnostics.cpp',
     'DOMMediaStream.cpp',
     'EncodedBufferCache.cpp',
     'FileBlockCache.cpp',
     'FlushableTaskQueue.cpp',
     'GetUserMediaRequest.cpp',
     'GraphDriver.cpp',
     'Latency.cpp',
     'MediaCache.cpp',