summaryrefslogtreecommitdiffstats
path: root/dom/media/doctor
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/media/doctor
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media/doctor')
-rw-r--r--dom/media/doctor/DDLifetime.cpp33
-rw-r--r--dom/media/doctor/DDLifetime.h72
-rw-r--r--dom/media/doctor/DDLifetimes.cpp84
-rw-r--r--dom/media/doctor/DDLifetimes.h130
-rw-r--r--dom/media/doctor/DDLogCategory.cpp30
-rw-r--r--dom/media/doctor/DDLogCategory.h41
-rw-r--r--dom/media/doctor/DDLogMessage.cpp42
-rw-r--r--dom/media/doctor/DDLogMessage.h48
-rw-r--r--dom/media/doctor/DDLogObject.cpp22
-rw-r--r--dom/media/doctor/DDLogObject.h62
-rw-r--r--dom/media/doctor/DDLogUtils.cpp11
-rw-r--r--dom/media/doctor/DDLogUtils.h33
-rw-r--r--dom/media/doctor/DDLogValue.cpp120
-rw-r--r--dom/media/doctor/DDLogValue.h43
-rw-r--r--dom/media/doctor/DDLoggedTypeTraits.h104
-rw-r--r--dom/media/doctor/DDMediaLog.cpp27
-rw-r--r--dom/media/doctor/DDMediaLog.h42
-rw-r--r--dom/media/doctor/DDMediaLogs.cpp667
-rw-r--r--dom/media/doctor/DDMediaLogs.h193
-rw-r--r--dom/media/doctor/DDMessageIndex.h26
-rw-r--r--dom/media/doctor/DDTimeStamp.cpp20
-rw-r--r--dom/media/doctor/DDTimeStamp.h24
-rw-r--r--dom/media/doctor/DecoderDoctorDiagnostics.cpp1319
-rw-r--r--dom/media/doctor/DecoderDoctorDiagnostics.h167
-rw-r--r--dom/media/doctor/DecoderDoctorLogger.cpp176
-rw-r--r--dom/media/doctor/DecoderDoctorLogger.h472
-rw-r--r--dom/media/doctor/MultiWriterQueue.h523
-rw-r--r--dom/media/doctor/RollingNumber.h163
-rw-r--r--dom/media/doctor/moz.build40
-rw-r--r--dom/media/doctor/test/browser/browser.ini7
-rw-r--r--dom/media/doctor/test/browser/browser_decoderDoctor.js356
-rw-r--r--dom/media/doctor/test/browser/browser_doctor_notification.js265
-rw-r--r--dom/media/doctor/test/gtest/TestMultiWriterQueue.cpp382
-rw-r--r--dom/media/doctor/test/gtest/TestRollingNumber.cpp146
-rw-r--r--dom/media/doctor/test/gtest/moz.build19
35 files changed, 5909 insertions, 0 deletions
diff --git a/dom/media/doctor/DDLifetime.cpp b/dom/media/doctor/DDLifetime.cpp
new file mode 100644
index 0000000000..2d4c6cb966
--- /dev/null
+++ b/dom/media/doctor/DDLifetime.cpp
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "DDLifetime.h"
+
+namespace mozilla {
+
+void DDLifetime::AppendPrintf(nsCString& aString) const {
+ if (!mDerivedObject.Pointer()) {
+ mObject.AppendPrintf(aString);
+ aString.AppendPrintf("#%" PRIi32, mTag);
+ } else {
+ mDerivedObject.AppendPrintf(aString);
+ aString.AppendPrintf("#%" PRIi32 " (as ", mTag);
+ if (mObject.Pointer() == mDerivedObject.Pointer()) {
+ aString.Append(mObject.TypeName());
+ } else {
+ mObject.AppendPrintf(aString);
+ }
+ aString.Append(")");
+ }
+}
+
+nsCString DDLifetime::Printf() const {
+ nsCString s;
+ AppendPrintf(s);
+ return s;
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDLifetime.h b/dom/media/doctor/DDLifetime.h
new file mode 100644
index 0000000000..3c97c064d8
--- /dev/null
+++ b/dom/media/doctor/DDLifetime.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DDLifetime_h_
+#define DDLifetime_h_
+
+#include "DDLogObject.h"
+#include "DDMessageIndex.h"
+#include "DDTimeStamp.h"
+
+namespace mozilla {
+
+namespace dom {
+class HTMLMediaElement;
+} // namespace dom
+
+// This struct records the lifetime of one C++ object.
+// Note that multiple objects may have the same address and type (at different
+// times), so the recorded construction/destruction times should be used to
+// distinguish them.
+struct DDLifetime {
+ const DDLogObject mObject;
+ const DDMessageIndex mConstructionIndex;
+ const DDTimeStamp mConstructionTimeStamp;
+ // Only valid when mDestructionTimeStamp is not null.
+ DDMessageIndex mDestructionIndex;
+ DDTimeStamp mDestructionTimeStamp;
+ // Associated HTMLMediaElement, initially nullptr until this object can be
+ // linked to its HTMLMediaElement.
+ const dom::HTMLMediaElement* mMediaElement;
+ // If not null, derived object for which this DDLifetime is a base class.
+ // This is used to link messages from the same object, even when they
+ // originate from a method on a base class.
+ // Note: We assume a single-inheritance hierarchy.
+ DDLogObject mDerivedObject;
+ DDMessageIndex mDerivedObjectLinkingIndex;
+ // Unique tag used to identify objects in a log, easier to read than object
+ // pointers.
+ // Negative and unique for unassociated objects.
+ // Positive for associated objects, and unique for that HTMLMediaElement
+ // group.
+ int32_t mTag;
+
+ DDLifetime(DDLogObject aObject, DDMessageIndex aConstructionIndex,
+ DDTimeStamp aConstructionTimeStamp, int32_t aTag)
+ : mObject(aObject),
+ mConstructionIndex(aConstructionIndex),
+ mConstructionTimeStamp(aConstructionTimeStamp),
+ mDestructionIndex(0),
+ mMediaElement(nullptr),
+ mDerivedObjectLinkingIndex(0),
+ mTag(aTag) {}
+
+ // Is this lifetime alive at the given index?
+ // I.e.: Constructed before, and destroyed later or not yet.
+ bool IsAliveAt(DDMessageIndex aIndex) const {
+ return aIndex >= mConstructionIndex &&
+ (!mDestructionTimeStamp || aIndex <= mDestructionIndex);
+ }
+
+ // Print the object's pointer, tag and class name (and derived class). E.g.:
+ // "dom::HTMLVideoElement[134073800]#1 (as dom::HTMLMediaElement)"
+ void AppendPrintf(nsCString& aString) const;
+ nsCString Printf() const;
+};
+
+} // namespace mozilla
+
+#endif // DDLifetime_h_
diff --git a/dom/media/doctor/DDLifetimes.cpp b/dom/media/doctor/DDLifetimes.cpp
new file mode 100644
index 0000000000..ba14e33eac
--- /dev/null
+++ b/dom/media/doctor/DDLifetimes.cpp
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "DDLifetimes.h"
+
+#include "DDLogUtils.h"
+
+namespace mozilla {
+
+DDLifetime* DDLifetimes::FindLifetime(const DDLogObject& aObject,
+ const DDMessageIndex& aIndex) {
+ LifetimesForObject* lifetimes = mLifetimes.Get(aObject);
+ if (lifetimes) {
+ for (DDLifetime& lifetime : *lifetimes) {
+ if (lifetime.mObject == aObject && lifetime.IsAliveAt(aIndex)) {
+ return &lifetime;
+ }
+ }
+ }
+ return nullptr;
+}
+
+const DDLifetime* DDLifetimes::FindLifetime(
+ const DDLogObject& aObject, const DDMessageIndex& aIndex) const {
+ const LifetimesForObject* lifetimes = mLifetimes.Get(aObject);
+ if (lifetimes) {
+ for (const DDLifetime& lifetime : *lifetimes) {
+ if (lifetime.mObject == aObject && lifetime.IsAliveAt(aIndex)) {
+ return &lifetime;
+ }
+ }
+ }
+ return nullptr;
+}
+
+DDLifetime& DDLifetimes::CreateLifetime(
+ const DDLogObject& aObject, DDMessageIndex aIndex,
+ const DDTimeStamp& aConstructionTimeStamp) {
+ // Use negative tags for yet-unclassified messages.
+ static int32_t sTag = 0;
+ if (--sTag > 0) {
+ sTag = -1;
+ }
+ LifetimesForObject* lifetimes = mLifetimes.GetOrInsertNew(aObject, 1);
+ DDLifetime& lifetime = *lifetimes->AppendElement(
+ DDLifetime(aObject, aIndex, aConstructionTimeStamp, sTag));
+ return lifetime;
+}
+
+void DDLifetimes::RemoveLifetime(const DDLifetime* aLifetime) {
+ LifetimesForObject* lifetimes = mLifetimes.Get(aLifetime->mObject);
+ MOZ_ASSERT(lifetimes);
+ DDL_LOG(aLifetime->mMediaElement ? mozilla::LogLevel::Debug
+ : mozilla::LogLevel::Warning,
+ "Remove lifetime %s", aLifetime->Printf().get());
+ // We should have been given a pointer inside this lifetimes array.
+ auto arrayIndex = aLifetime - lifetimes->Elements();
+ MOZ_ASSERT(static_cast<size_t>(arrayIndex) < lifetimes->Length());
+ lifetimes->RemoveElementAt(arrayIndex);
+}
+
+void DDLifetimes::RemoveLifetimesFor(
+ const dom::HTMLMediaElement* aMediaElement) {
+ for (const auto& lifetimes : mLifetimes.Values()) {
+ lifetimes->RemoveElementsBy([aMediaElement](const DDLifetime& lifetime) {
+ return lifetime.mMediaElement == aMediaElement;
+ });
+ }
+}
+
+void DDLifetimes::Clear() { mLifetimes.Clear(); }
+
+size_t DDLifetimes::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t size = mLifetimes.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const auto& lifetimes : mLifetimes.Values()) {
+ size += lifetimes->ShallowSizeOfExcludingThis(aMallocSizeOf);
+ }
+ return size;
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDLifetimes.h b/dom/media/doctor/DDLifetimes.h
new file mode 100644
index 0000000000..3cfcafc995
--- /dev/null
+++ b/dom/media/doctor/DDLifetimes.h
@@ -0,0 +1,130 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DDLifetimes_h_
+#define DDLifetimes_h_
+
+#include <type_traits>
+
+#include "DDLifetime.h"
+#include "DDLoggedTypeTraits.h"
+#include "nsClassHashtable.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+// Managed list of lifetimes.
+class DDLifetimes {
+ public:
+ // DDLifetime for a given aObject, that exists at the aIndex time;
+ // otherwise nullptr.
+ DDLifetime* FindLifetime(const DDLogObject& aObject,
+ const DDMessageIndex& aIndex);
+
+ // DDLifetime for a given aObject, that exists at the aIndex time;
+ // otherwise nullptr.
+ const DDLifetime* FindLifetime(const DDLogObject& aObject,
+ const DDMessageIndex& aIndex) const;
+
+ // Create a lifetime with the given object and construction index&time.
+ DDLifetime& CreateLifetime(const DDLogObject& aObject, DDMessageIndex aIndex,
+ const DDTimeStamp& aConstructionTimeStamp);
+
+ // Remove an existing lifetime (assumed to just have been found by
+ // FindLifetime()).
+ void RemoveLifetime(const DDLifetime* aLifetime);
+
+ // Remove all lifetimes associated with the given HTMLMediaElement.
+ void RemoveLifetimesFor(const dom::HTMLMediaElement* aMediaElement);
+
+ // Remove all lifetimes.
+ void Clear();
+
+ // Visit all lifetimes associated with an HTMLMediaElement and run
+ // `aF(const DDLifetime&)` on each one.
+ // If aOnlyHTMLMediaElement is true, only run aF once of that element.
+ template <typename F>
+ void Visit(const dom::HTMLMediaElement* aMediaElement, F&& aF,
+ bool aOnlyHTMLMediaElement = false) const {
+ for (const auto& lifetimes : mLifetimes.Values()) {
+ for (const DDLifetime& lifetime : *lifetimes) {
+ if (lifetime.mMediaElement == aMediaElement) {
+ if (aOnlyHTMLMediaElement) {
+ if (lifetime.mObject.Pointer() == aMediaElement &&
+ lifetime.mObject.TypeName() ==
+ DDLoggedTypeTraits<dom::HTMLMediaElement>::Name()) {
+ aF(lifetime);
+ break;
+ }
+ continue;
+ }
+ static_assert(std::is_same_v<decltype(aF(lifetime)), void>, "");
+ aF(lifetime);
+ }
+ }
+ }
+ }
+
+ // Visit all lifetimes associated with an HTMLMediaElement and run
+ // `aF(const DDLifetime&)` on each one.
+ // If aF() returns false, the loop continues.
+ // If aF() returns true, the loop stops, and true is returned immediately.
+ // If all aF() calls have returned false, false is returned at the end.
+ template <typename F>
+ bool VisitBreakable(const dom::HTMLMediaElement* aMediaElement,
+ F&& aF) const {
+ for (const auto& lifetimes : mLifetimes.Values()) {
+ for (const DDLifetime& lifetime : *lifetimes) {
+ if (lifetime.mMediaElement == aMediaElement) {
+ static_assert(std::is_same_v<decltype(aF(lifetime)), bool>, "");
+ if (aF(lifetime)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ // Hashtable key to use for each DDLogObject.
+ class DDLogObjectHashKey : public PLDHashEntryHdr {
+ public:
+ typedef const DDLogObject& KeyType;
+ typedef const DDLogObject* KeyTypePointer;
+
+ explicit DDLogObjectHashKey(KeyTypePointer aKey) : mValue(*aKey) {}
+ DDLogObjectHashKey(const DDLogObjectHashKey& aToCopy)
+ : mValue(aToCopy.mValue) {}
+ ~DDLogObjectHashKey() = default;
+
+ KeyType GetKey() const { return mValue; }
+ bool KeyEquals(KeyTypePointer aKey) const { return *aKey == mValue; }
+
+ static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
+ static PLDHashNumber HashKey(KeyTypePointer aKey) {
+ return HashBytes(aKey, sizeof(DDLogObject));
+ }
+ enum { ALLOW_MEMMOVE = true };
+
+ private:
+ const DDLogObject mValue;
+ };
+
+ // Array of all DDLifetimes for a given DDLogObject; they should be
+ // distinguished by their construction&destruction times.
+ using LifetimesForObject = nsTArray<DDLifetime>;
+
+ // For each DDLogObject, we store an array of all objects that have used this
+ // pointer and type.
+ nsClassHashtable<DDLogObjectHashKey, LifetimesForObject> mLifetimes;
+};
+
+} // namespace mozilla
+
+#endif // DDLifetimes_h_
diff --git a/dom/media/doctor/DDLogCategory.cpp b/dom/media/doctor/DDLogCategory.cpp
new file mode 100644
index 0000000000..76fce1f4ae
--- /dev/null
+++ b/dom/media/doctor/DDLogCategory.cpp
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "DDLogCategory.h"
+
+namespace mozilla {
+
+const char* const kDDLogCategoryShortStrings[kDDLogCategoryCount] = {
+ "con", "dcn", "des", "lnk", "ulk", "prp", "evt",
+ "api", "log", "mze", "mzw", "mzi", "mzd", "mzv"};
+const char* const kDDLogCategoryLongStrings[kDDLogCategoryCount] = {
+ "Construction",
+ "Derived Construction",
+ "Destruction",
+ "Link",
+ "Unlink",
+ "Property",
+ "Event",
+ "API",
+ "Log",
+ "MozLog-Error",
+ "MozLog-Warning",
+ "MozLog-Info",
+ "MozLog-Debug",
+ "MozLog-Verbose"};
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDLogCategory.h b/dom/media/doctor/DDLogCategory.h
new file mode 100644
index 0000000000..2c8494b690
--- /dev/null
+++ b/dom/media/doctor/DDLogCategory.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DDLogCategory_h_
+#define DDLogCategory_h_
+
+#include "mozilla/Assertions.h"
+#include "mozilla/DefineEnum.h"
+
+namespace mozilla {
+
+// Enum used to categorize log messages.
+// Those starting with '_' are for internal use only.
+MOZ_DEFINE_ENUM_CLASS(DDLogCategory,
+ (_Construction, _DerivedConstruction, _Destruction, _Link,
+ _Unlink, Property, Event, API, Log, MozLogError,
+ MozLogWarning, MozLogInfo, MozLogDebug, MozLogVerbose));
+
+// Corresponding short strings, used as JSON property names when logs are
+// retrieved.
+extern const char* const kDDLogCategoryShortStrings[kDDLogCategoryCount];
+
+inline const char* ToShortString(DDLogCategory aCategory) {
+ MOZ_ASSERT(static_cast<size_t>(aCategory) < kDDLogCategoryCount);
+ return kDDLogCategoryShortStrings[static_cast<size_t>(aCategory)];
+}
+
+// Corresponding long strings, for use in descriptive UI.
+extern const char* const kDDLogCategoryLongStrings[kDDLogCategoryCount];
+
+inline const char* ToLongString(DDLogCategory aCategory) {
+ MOZ_ASSERT(static_cast<size_t>(aCategory) < kDDLogCategoryCount);
+ return kDDLogCategoryLongStrings[static_cast<size_t>(aCategory)];
+}
+
+} // namespace mozilla
+
+#endif // DDLogCategory_h_
diff --git a/dom/media/doctor/DDLogMessage.cpp b/dom/media/doctor/DDLogMessage.cpp
new file mode 100644
index 0000000000..488c39216a
--- /dev/null
+++ b/dom/media/doctor/DDLogMessage.cpp
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "DDLogMessage.h"
+
+#include "DDLifetimes.h"
+
+namespace mozilla {
+
+nsCString DDLogMessage::Print() const {
+ nsCString str;
+ str.AppendPrintf("%" PRImi " | %f | %s[%p] | %s | %s | ", mIndex.Value(),
+ ToSeconds(mTimeStamp), mObject.TypeName(), mObject.Pointer(),
+ ToShortString(mCategory), mLabel);
+ AppendToString(mValue, str);
+ return str;
+}
+
+nsCString DDLogMessage::Print(const DDLifetimes& aLifetimes) const {
+ nsCString str;
+ const DDLifetime* lifetime = aLifetimes.FindLifetime(mObject, mIndex);
+ str.AppendPrintf("%" PRImi " | %f | ", mIndex.Value(), ToSeconds(mTimeStamp));
+ lifetime->AppendPrintf(str);
+ str.AppendPrintf(" | %s | %s | ", ToShortString(mCategory), mLabel);
+ if (!mValue.is<DDLogObject>()) {
+ AppendToString(mValue, str);
+ } else {
+ const DDLifetime* lifetime2 =
+ aLifetimes.FindLifetime(mValue.as<DDLogObject>(), mIndex);
+ if (lifetime2) {
+ lifetime2->AppendPrintf(str);
+ } else {
+ AppendToString(mValue, str);
+ }
+ }
+ return str;
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDLogMessage.h b/dom/media/doctor/DDLogMessage.h
new file mode 100644
index 0000000000..5470e01da5
--- /dev/null
+++ b/dom/media/doctor/DDLogMessage.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DDLogMessage_h_
+#define DDLogMessage_h_
+
+#include "DDLogCategory.h"
+#include "DDLogObject.h"
+#include "DDLogValue.h"
+#include "DDMessageIndex.h"
+#include "DDTimeStamp.h"
+#include "mozilla/Atomics.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+class DDLifetimes;
+
+// Structure containing all the information needed in each log message
+// (before and after processing).
+struct DDLogMessage {
+ DDMessageIndex mIndex;
+ DDTimeStamp mTimeStamp;
+ DDLogObject mObject;
+ DDLogCategory mCategory;
+ const char* mLabel;
+ DDLogValue mValue = DDLogValue{DDNoValue{}};
+
+ // Print the message. Format:
+ // "index | timestamp | object | category | label | value". E.g.:
+ // "29 | 5.047547 | dom::HTMLMediaElement[134073800] | lnk | decoder |
+ // MediaDecoder[136078200]"
+ nsCString Print() const;
+
+ // Print the message, using object information from aLifetimes. Format:
+ // "index | timestamp | object | category | label | value". E.g.:
+ // "29 | 5.047547 | dom::HTMLVideoElement[134073800]#1 (as
+ // dom::HTMLMediaElement) | lnk | decoder | MediaSourceDecoder[136078200]#5
+ // (as MediaDecoder)"
+ nsCString Print(const DDLifetimes& aLifetimes) const;
+};
+
+} // namespace mozilla
+
+#endif // DDLogMessage_h_
diff --git a/dom/media/doctor/DDLogObject.cpp b/dom/media/doctor/DDLogObject.cpp
new file mode 100644
index 0000000000..7a8b3341de
--- /dev/null
+++ b/dom/media/doctor/DDLogObject.cpp
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "DDLogObject.h"
+
+namespace mozilla {
+
+void DDLogObject::AppendPrintf(nsCString& mString) const {
+ MOZ_ASSERT(mTypeName);
+ mString.AppendPrintf("%s[%p]", mTypeName, mPointer);
+}
+
+nsCString DDLogObject::Printf() const {
+ nsCString s;
+ AppendPrintf(s);
+ return s;
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDLogObject.h b/dom/media/doctor/DDLogObject.h
new file mode 100644
index 0000000000..d16d601eb2
--- /dev/null
+++ b/dom/media/doctor/DDLogObject.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DDLogObject_h_
+#define DDLogObject_h_
+
+#include "nsString.h"
+
+namespace mozilla {
+
+// DDLogObject identifies a C++ object by its pointer and its class name (as
+// provided in a DDLoggedTypeTrait.)
+// Note that a DDLogObject could have the exact same pointer&type as a previous
+// one, so extra information is needed to distinguish them, see DDLifetime.
+class DDLogObject {
+ public:
+ // Default-initialization with null pointer.
+ DDLogObject() : mTypeName("<unset>"), mPointer(nullptr) {}
+
+ // Construction with given non-null type name and pointer.
+ DDLogObject(const char* aTypeName, const void* aPointer)
+ : mTypeName(aTypeName), mPointer(aPointer) {
+ MOZ_ASSERT(aTypeName);
+ MOZ_ASSERT(aPointer);
+ }
+
+ // Sets this DDLogObject to an actual object.
+ void Set(const char* aTypeName, const void* aPointer) {
+ MOZ_ASSERT(aTypeName);
+ MOZ_ASSERT(aPointer);
+ mTypeName = aTypeName;
+ mPointer = aPointer;
+ }
+
+ // Object pointer, used for identification purposes only.
+ const void* Pointer() const { return mPointer; }
+
+ // Type name. Should only be accessed after non-null pointer initialization.
+ const char* TypeName() const {
+ MOZ_ASSERT(mPointer);
+ return mTypeName;
+ }
+
+ bool operator==(const DDLogObject& a) const {
+ return mPointer == a.mPointer && (!mPointer || mTypeName == a.mTypeName);
+ }
+
+ // Print the type name and pointer, e.g.: "MediaDecoder[136078200]".
+ void AppendPrintf(nsCString& mString) const;
+ nsCString Printf() const;
+
+ private:
+ const char* mTypeName;
+ const void* mPointer;
+};
+
+} // namespace mozilla
+
+#endif // DDLogObject_h_
diff --git a/dom/media/doctor/DDLogUtils.cpp b/dom/media/doctor/DDLogUtils.cpp
new file mode 100644
index 0000000000..50f0b676d0
--- /dev/null
+++ b/dom/media/doctor/DDLogUtils.cpp
@@ -0,0 +1,11 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "DDLogUtils.h"
+
+mozilla::LazyLogModule sDecoderDoctorLoggerLog("DDLogger");
+
+mozilla::LazyLogModule sDecoderDoctorLoggerEndLog("DDLoggerEnd");
diff --git a/dom/media/doctor/DDLogUtils.h b/dom/media/doctor/DDLogUtils.h
new file mode 100644
index 0000000000..256aed1e3a
--- /dev/null
+++ b/dom/media/doctor/DDLogUtils.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DDLogUtils_h_
+#define DDLogUtils_h_
+
+#include "mozilla/Logging.h"
+
+// Logging for the DecoderDoctorLoggger code.
+extern mozilla::LazyLogModule sDecoderDoctorLoggerLog;
+#define DDL_LOG(level, arg, ...) \
+ MOZ_LOG(sDecoderDoctorLoggerLog, level, (arg, ##__VA_ARGS__))
+#define DDL_DEBUG(arg, ...) \
+ DDL_LOG(mozilla::LogLevel::Debug, arg, ##__VA_ARGS__)
+#define DDL_INFO(arg, ...) DDL_LOG(mozilla::LogLevel::Info, arg, ##__VA_ARGS__)
+#define DDL_WARN(arg, ...) \
+ DDL_LOG(mozilla::LogLevel::Warning, arg, ##__VA_ARGS__)
+
+// Output at shutdown the log given to DecoderDoctorLogger.
+extern mozilla::LazyLogModule sDecoderDoctorLoggerEndLog;
+#define DDLE_LOG(level, arg, ...) \
+ MOZ_LOG(sDecoderDoctorLoggerEndLog, level, (arg, ##__VA_ARGS__))
+#define DDLE_DEBUG(arg, ...) \
+ DDLE_LOG(mozilla::LogLevel::Debug, arg, ##__VA_ARGS__)
+#define DDLE_INFO(arg, ...) \
+ DDLE_LOG(mozilla::LogLevel::Info, arg, ##__VA_ARGS__)
+#define DDLE_WARN(arg, ...) \
+ DDLE_LOG(mozilla::LogLevel::Warning, arg, ##__VA_ARGS__)
+
+#endif // DDLogUtils_h_
diff --git a/dom/media/doctor/DDLogValue.cpp b/dom/media/doctor/DDLogValue.cpp
new file mode 100644
index 0000000000..74044ca05a
--- /dev/null
+++ b/dom/media/doctor/DDLogValue.cpp
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "DDLogValue.h"
+
+#include "mozilla/JSONWriter.h"
+
+namespace mozilla {
+
+struct LogValueMatcher {
+ nsCString& mString;
+
+ void operator()(const DDNoValue&) const {}
+ void operator()(const DDLogObject& a) const { a.AppendPrintf(mString); }
+ void operator()(const char* a) const { mString.AppendPrintf(R"("%s")", a); }
+ void operator()(const nsCString& a) const {
+ mString.AppendPrintf(R"(nsCString("%s"))", a.Data());
+ }
+ void operator()(bool a) const { mString.AppendPrintf(a ? "true" : "false"); }
+ void operator()(int8_t a) const {
+ mString.AppendPrintf("int8_t(%" PRIi8 ")", a);
+ }
+ void operator()(uint8_t a) const {
+ mString.AppendPrintf("uint8_t(%" PRIu8 ")", a);
+ }
+ void operator()(int16_t a) const {
+ mString.AppendPrintf("int16_t(%" PRIi16 ")", a);
+ }
+ void operator()(uint16_t a) const {
+ mString.AppendPrintf("uint16_t(%" PRIu16 ")", a);
+ }
+ void operator()(int32_t a) const {
+ mString.AppendPrintf("int32_t(%" PRIi32 ")", a);
+ }
+ void operator()(uint32_t a) const {
+ mString.AppendPrintf("uint32_t(%" PRIu32 ")", a);
+ }
+ void operator()(int64_t a) const {
+ mString.AppendPrintf("int64_t(%" PRIi64 ")", a);
+ }
+ void operator()(uint64_t a) const {
+ mString.AppendPrintf("uint64_t(%" PRIu64 ")", a);
+ }
+ void operator()(double a) const { mString.AppendPrintf("double(%f)", a); }
+ void operator()(const DDRange& a) const {
+ mString.AppendPrintf("%" PRIi64 "<=(%" PRIi64 "B)<%" PRIi64 "", a.mOffset,
+ a.mBytes, a.mOffset + a.mBytes);
+ }
+ void operator()(const nsresult& a) const {
+ nsCString name;
+ GetErrorName(a, name);
+ mString.AppendPrintf("nsresult(%s =0x%08" PRIx32 ")", name.get(),
+ static_cast<uint32_t>(a));
+ }
+ void operator()(const MediaResult& a) const {
+ nsCString name;
+ GetErrorName(a.Code(), name);
+ mString.AppendPrintf("MediaResult(%s =0x%08" PRIx32 ", \"%s\")", name.get(),
+ static_cast<uint32_t>(a.Code()), a.Message().get());
+ }
+};
+
+void AppendToString(const DDLogValue& aValue, nsCString& aString) {
+ aValue.match(LogValueMatcher{aString});
+}
+
+struct LogValueMatcherJson {
+ JSONWriter& mJW;
+ const Span<const char> mPropertyName;
+
+ void operator()(const DDNoValue&) const { mJW.NullProperty(mPropertyName); }
+ void operator()(const DDLogObject& a) const {
+ nsPrintfCString s(R"("%s[%p]")", a.TypeName(), a.Pointer());
+ mJW.StringProperty(mPropertyName, s);
+ }
+ void operator()(const char* a) const {
+ mJW.StringProperty(mPropertyName, MakeStringSpan(a));
+ }
+ void operator()(const nsCString& a) const {
+ mJW.StringProperty(mPropertyName, a);
+ }
+ void operator()(bool a) const { mJW.BoolProperty(mPropertyName, a); }
+ void operator()(int8_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(uint8_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(int16_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(uint16_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(int32_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(uint32_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(int64_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(uint64_t a) const { mJW.DoubleProperty(mPropertyName, a); }
+ void operator()(double a) const { mJW.DoubleProperty(mPropertyName, a); }
+ void operator()(const DDRange& a) const {
+ mJW.StartArrayProperty(mPropertyName);
+ mJW.IntElement(a.mOffset);
+ mJW.IntElement(a.mOffset + a.mBytes);
+ mJW.EndArray();
+ }
+ void operator()(const nsresult& a) const {
+ nsCString name;
+ GetErrorName(a, name);
+ mJW.StringProperty(mPropertyName, name);
+ }
+ void operator()(const MediaResult& a) const {
+ nsCString name;
+ GetErrorName(a.Code(), name);
+ mJW.StringProperty(mPropertyName,
+ nsPrintfCString(R"lit("MediaResult(%s, %s)")lit",
+ name.get(), a.Message().get()));
+ }
+};
+
+void ToJSON(const DDLogValue& aValue, JSONWriter& aJSONWriter,
+ const char* aPropertyName) {
+ aValue.match(LogValueMatcherJson{aJSONWriter, MakeStringSpan(aPropertyName)});
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDLogValue.h b/dom/media/doctor/DDLogValue.h
new file mode 100644
index 0000000000..9a8253916f
--- /dev/null
+++ b/dom/media/doctor/DDLogValue.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DDLogValue_h_
+#define DDLogValue_h_
+
+#include "DDLogObject.h"
+#include "MediaResult.h"
+#include "mozilla/Variant.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+// Placeholder when no value is needed.
+struct DDNoValue {};
+
+// Value capturing a range (typically an offset and a number of bytes in a
+// resource).
+struct DDRange {
+ const int64_t mOffset;
+ const int64_t mBytes;
+ DDRange(int64_t aOffset, int64_t aBytes) : mOffset(aOffset), mBytes(aBytes) {}
+};
+
+// Value associated with a log message.
+using DDLogValue = Variant<DDNoValue, DDLogObject,
+ const char*, // Assumed to be a literal string.
+ const nsCString, bool, int8_t, uint8_t, int16_t,
+ uint16_t, int32_t, uint32_t, int64_t, uint64_t,
+ double, DDRange, nsresult, MediaResult>;
+
+void AppendToString(const DDLogValue& aValue, nsCString& aString);
+
+class JSONWriter;
+void ToJSON(const DDLogValue& aValue, JSONWriter& aJSONWriter,
+ const char* aPropertyName);
+
+} // namespace mozilla
+
+#endif // DDLogValue_h_
diff --git a/dom/media/doctor/DDLoggedTypeTraits.h b/dom/media/doctor/DDLoggedTypeTraits.h
new file mode 100644
index 0000000000..2c9de4dbc5
--- /dev/null
+++ b/dom/media/doctor/DDLoggedTypeTraits.h
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DDLoggedTypeTraits_h_
+#define DDLoggedTypeTraits_h_
+
+#include <type_traits>
+
+namespace mozilla {
+
+// Templated struct carrying compile-time information about C++ types that may
+// log messages (including about their lifetime and links to other objects.)
+// Classes should declare a specialization by using one of the macros below.
+template <typename T>
+struct DDLoggedTypeTraits;
+
+#define DDLoggedTypeName(TYPE) \
+ template <> \
+ struct DDLoggedTypeTraits<TYPE> { \
+ using Type = TYPE; \
+ static constexpr const char* Name() { return #TYPE; } \
+ using HasBase = std::false_type; \
+ using BaseType = TYPE; \
+ static constexpr const char* BaseTypeName() { return ""; } \
+ }
+
+#define DDLoggedTypeNameAndBase(TYPE, BASE) \
+ template <> \
+ struct DDLoggedTypeTraits<TYPE> { \
+ using Type = TYPE; \
+ static constexpr const char* Name() { return #TYPE; } \
+ using HasBase = std::true_type; \
+ using BaseType = BASE; \
+ static constexpr const char* BaseTypeName() { \
+ return DDLoggedTypeTraits<BASE>::Name(); \
+ } \
+ }
+
+#define DDLoggedTypeCustomName(TYPE, NAME) \
+ template <> \
+ struct DDLoggedTypeTraits<TYPE> { \
+ using Type = TYPE; \
+ static constexpr const char* Name() { return #NAME; } \
+ using HasBase = std::false_type; \
+ using BaseType = TYPE; \
+ static constexpr const char* BaseTypeName() { return ""; } \
+ }
+
+#define DDLoggedTypeCustomNameAndBase(TYPE, NAME, BASE) \
+ template <> \
+ struct DDLoggedTypeTraits<TYPE> { \
+ using Type = TYPE; \
+ static constexpr const char* Name() { return #NAME; } \
+ using HasBase = std::true_type; \
+ using BaseType = BASE; \
+ static constexpr const char* BaseTypeName() { \
+ return DDLoggedTypeTraits<BASE>::Name(); \
+ } \
+ }
+
+// Variants with built-in forward-declaration, will only work
+// - in mozilla namespace,
+// - with unqualified mozilla-namespace type names.
+#define DDLoggedTypeDeclName(TYPE) \
+ class TYPE; \
+ DDLoggedTypeName(TYPE);
+#define DDLoggedTypeDeclNameAndBase(TYPE, BASE) \
+ class TYPE; \
+ DDLoggedTypeNameAndBase(TYPE, BASE);
+#define DDLoggedTypeDeclCustomName(TYPE, NAME) \
+ class TYPE; \
+ DDLoggedTypeCustomName(TYPE, NAME);
+#define DDLoggedTypeDeclCustomNameAndBase(TYPE, NAME, BASE) \
+ class TYPE; \
+ DDLoggedTypeCustomNameAndBase(TYPE, NAME, BASE);
+
+} // namespace mozilla
+
+// Some essential types that live outside of the media stack.
+class nsPIDOMWindowInner;
+class nsPIDOMWindowOuter;
+
+namespace mozilla {
+
+namespace dom {
+class Document;
+class HTMLAudioElement;
+class HTMLMediaElement;
+class HTMLVideoElement;
+} // namespace dom
+
+DDLoggedTypeName(nsPIDOMWindowInner);
+DDLoggedTypeName(nsPIDOMWindowOuter);
+DDLoggedTypeName(dom::Document);
+DDLoggedTypeName(dom::HTMLMediaElement);
+DDLoggedTypeNameAndBase(dom::HTMLAudioElement, dom::HTMLMediaElement);
+DDLoggedTypeNameAndBase(dom::HTMLVideoElement, dom::HTMLMediaElement);
+
+} // namespace mozilla
+
+#endif // DDLoggedTypeTraits_h_
diff --git a/dom/media/doctor/DDMediaLog.cpp b/dom/media/doctor/DDMediaLog.cpp
new file mode 100644
index 0000000000..786d1096ff
--- /dev/null
+++ b/dom/media/doctor/DDMediaLog.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "DDMediaLog.h"
+
+namespace mozilla {
+
+size_t DDMediaLog::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t size = mMessages.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const DDLogMessage& message : mMessages) {
+ if (message.mValue.is<const nsCString>()) {
+ size +=
+ message.mValue.as<const nsCString>().SizeOfExcludingThisIfUnshared(
+ aMallocSizeOf);
+ } else if (message.mValue.is<MediaResult>()) {
+ size += message.mValue.as<MediaResult>()
+ .Message()
+ .SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+ }
+ return size;
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDMediaLog.h b/dom/media/doctor/DDMediaLog.h
new file mode 100644
index 0000000000..78037d1975
--- /dev/null
+++ b/dom/media/doctor/DDMediaLog.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DDMediaLog_h_
+#define DDMediaLog_h_
+
+#include "DDLogMessage.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+namespace dom {
+class HTMLMediaElement;
+} // namespace dom
+
+class DDLifetimes;
+
+// Container of processed messages corresponding to an HTMLMediaElement (or
+// not yet).
+struct DDMediaLog {
+ // Associated HTMLMediaElement, or nullptr for the DDMediaLog containing
+ // messages for yet-unassociated objects.
+ // TODO: Should use a DDLogObject instead, to distinguish between elements
+ // at the same address.
+ // Not critical: At worst we will combine logs for two elements.
+ const dom::HTMLMediaElement* mMediaElement;
+
+ // Number of lifetimes associated with this log. Managed by DDMediaLogs.
+ int32_t mLifetimeCount = 0;
+
+ using LogMessages = nsTArray<DDLogMessage>;
+ LogMessages mMessages;
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
+};
+
+} // namespace mozilla
+
+#endif // DDMediaLog_h_
diff --git a/dom/media/doctor/DDMediaLogs.cpp b/dom/media/doctor/DDMediaLogs.cpp
new file mode 100644
index 0000000000..442e03d3fa
--- /dev/null
+++ b/dom/media/doctor/DDMediaLogs.cpp
@@ -0,0 +1,667 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "DDMediaLogs.h"
+
+#include "DDLogUtils.h"
+#include "nsIThread.h"
+#include "nsIThreadManager.h"
+#include "mozilla/JSONStringWriteFuncs.h"
+
+namespace mozilla {
+
+/* static */ DDMediaLogs::ConstructionResult DDMediaLogs::New() {
+ nsCOMPtr<nsIThread> mThread;
+ nsresult rv =
+ NS_NewNamedThread("DDMediaLogs", getter_AddRefs(mThread), nullptr,
+ {.stackSize = nsIThreadManager::kThreadPoolStackSize});
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return {rv, nullptr};
+ }
+
+ return {rv, new DDMediaLogs(std::move(mThread))};
+}
+
+DDMediaLogs::DDMediaLogs(nsCOMPtr<nsIThread>&& aThread)
+ : mMediaLogs(1), mMutex("DDMediaLogs"), mThread(std::move(aThread)) {
+ mMediaLogs.SetLength(1);
+ mMediaLogs[0].mMediaElement = nullptr;
+ DDL_INFO("DDMediaLogs constructed, processing thread: %p", mThread.get());
+}
+
+DDMediaLogs::~DDMediaLogs() {
+ // Perform end-of-life processing, ensure the processing thread is shutdown.
+ Shutdown(/* aPanic = */ false);
+}
+
+void DDMediaLogs::Panic() { Shutdown(/* aPanic = */ true); }
+
+void DDMediaLogs::Shutdown(bool aPanic) {
+ nsCOMPtr<nsIThread> thread;
+ {
+ MutexAutoLock lock(mMutex);
+ thread.swap(mThread);
+ }
+ if (!thread) {
+ // Already shutdown, nothing more to do.
+ return;
+ }
+
+ DDL_INFO("DDMediaLogs::Shutdown will shutdown thread: %p", thread.get());
+ // Will block until pending tasks have completed, and thread is dead.
+ thread->Shutdown();
+
+ if (aPanic) {
+ mMessagesQueue.PopAll([](const DDLogMessage&) {});
+ MutexAutoLock lock(mMutex);
+ mLifetimes.Clear();
+ mMediaLogs.Clear();
+ mObjectLinks.Clear();
+ mPendingPromises.Clear();
+ return;
+ }
+
+ // Final processing is only necessary to output to MOZ_LOG=DDLoggerEnd,
+ // so there's no point doing any of it if that MOZ_LOG is not enabled.
+ if (MOZ_LOG_TEST(sDecoderDoctorLoggerEndLog, mozilla::LogLevel::Info)) {
+ DDL_DEBUG("Perform final DDMediaLogs processing...");
+ // The processing thread is dead, so we can safely call ProcessLog()
+ // directly from this thread.
+ ProcessLog();
+
+ for (const DDMediaLog& mediaLog : mMediaLogs) {
+ if (mediaLog.mMediaElement) {
+ DDLE_INFO("---");
+ }
+ DDLE_INFO("--- Log for HTMLMediaElement[%p] ---", mediaLog.mMediaElement);
+ for (const DDLogMessage& message : mediaLog.mMessages) {
+ DDLE_LOG(message.mCategory <= DDLogCategory::_Unlink
+ ? mozilla::LogLevel::Debug
+ : mozilla::LogLevel::Info,
+ "%s", message.Print(mLifetimes).get());
+ }
+ DDLE_DEBUG("--- End log for HTMLMediaElement[%p] ---",
+ mediaLog.mMediaElement);
+ }
+ }
+}
+
+DDMediaLog& DDMediaLogs::LogForUnassociatedMessages() {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ return mMediaLogs[0];
+}
+const DDMediaLog& DDMediaLogs::LogForUnassociatedMessages() const {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ return mMediaLogs[0];
+}
+
+DDMediaLog* DDMediaLogs::GetLogFor(const dom::HTMLMediaElement* aMediaElement) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ if (!aMediaElement) {
+ return &LogForUnassociatedMessages();
+ }
+ for (DDMediaLog& log : mMediaLogs) {
+ if (log.mMediaElement == aMediaElement) {
+ return &log;
+ }
+ }
+ return nullptr;
+}
+
+DDMediaLog& DDMediaLogs::LogFor(const dom::HTMLMediaElement* aMediaElement) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ DDMediaLog* log = GetLogFor(aMediaElement);
+ if (!log) {
+ log = mMediaLogs.AppendElement();
+ log->mMediaElement = aMediaElement;
+ }
+ return *log;
+}
+
+void DDMediaLogs::SetMediaElement(DDLifetime& aLifetime,
+ const dom::HTMLMediaElement* aMediaElement) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ DDMediaLog& log = LogFor(aMediaElement);
+
+ // List of lifetimes that are to be linked to aMediaElement.
+ nsTArray<DDLifetime*> lifetimes;
+ // We start with the given lifetime.
+ lifetimes.AppendElement(&aLifetime);
+ for (size_t i = 0; i < lifetimes.Length(); ++i) {
+ DDLifetime& lifetime = *lifetimes[i];
+ // Link the lifetime to aMediaElement.
+ lifetime.mMediaElement = aMediaElement;
+ // Classified lifetime's tag is a positive index from the DDMediaLog.
+ lifetime.mTag = ++log.mLifetimeCount;
+ DDL_DEBUG("%s -> HTMLMediaElement[%p]", lifetime.Printf().get(),
+ aMediaElement);
+
+ // Go through the lifetime's existing linked lifetimes, if any is not
+ // already linked to aMediaElement, add it to the list so it will get
+ // linked in a later loop.
+ for (auto& link : mObjectLinks) {
+ if (lifetime.IsAliveAt(link.mLinkingIndex)) {
+ if (lifetime.mObject == link.mParent) {
+ DDLifetime* childLifetime =
+ mLifetimes.FindLifetime(link.mChild, link.mLinkingIndex);
+ if (childLifetime && !childLifetime->mMediaElement &&
+ !lifetimes.Contains(childLifetime)) {
+ lifetimes.AppendElement(childLifetime);
+ }
+ } else if (lifetime.mObject == link.mChild) {
+ DDLifetime* parentLifetime =
+ mLifetimes.FindLifetime(link.mParent, link.mLinkingIndex);
+ if (parentLifetime && !parentLifetime->mMediaElement &&
+ !lifetimes.Contains(parentLifetime)) {
+ lifetimes.AppendElement(parentLifetime);
+ }
+ }
+ }
+ }
+ }
+
+ // Now we need to move yet-unclassified messages related to the just-set
+ // elements, to the appropriate MediaElement list.
+ DDMediaLog::LogMessages& messages = log.mMessages;
+ DDMediaLog::LogMessages& messages0 = LogForUnassociatedMessages().mMessages;
+ for (size_t i = 0; i < messages0.Length();
+ /* increment inside the loop */) {
+ DDLogMessage& message = messages0[i];
+ bool found = false;
+ for (const DDLifetime* lifetime : lifetimes) {
+ if (lifetime->mObject == message.mObject) {
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ messages.AppendElement(std::move(message));
+ messages0.RemoveElementAt(i);
+ // No increment, as we've removed this element; next element is now at
+ // the same index.
+ } else {
+ // Not touching this element, increment index to go to next element.
+ ++i;
+ }
+ }
+}
+
+DDLifetime& DDMediaLogs::FindOrCreateLifetime(const DDLogObject& aObject,
+ DDMessageIndex aIndex,
+ const DDTimeStamp& aTimeStamp) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ // Try to find lifetime corresponding to message object.
+ DDLifetime* lifetime = mLifetimes.FindLifetime(aObject, aIndex);
+ if (!lifetime) {
+ // No lifetime yet, create one.
+ lifetime = &(mLifetimes.CreateLifetime(aObject, aIndex, aTimeStamp));
+ if (MOZ_UNLIKELY(aObject.TypeName() ==
+ DDLoggedTypeTraits<dom::HTMLMediaElement>::Name())) {
+ const dom::HTMLMediaElement* mediaElement =
+ static_cast<const dom::HTMLMediaElement*>(aObject.Pointer());
+ SetMediaElement(*lifetime, mediaElement);
+ DDL_DEBUG("%s -> new lifetime: %s with MediaElement %p",
+ aObject.Printf().get(), lifetime->Printf().get(), mediaElement);
+ } else {
+ DDL_DEBUG("%s -> new lifetime: %s", aObject.Printf().get(),
+ lifetime->Printf().get());
+ }
+ }
+
+ return *lifetime;
+}
+
+void DDMediaLogs::LinkLifetimes(DDLifetime& aParentLifetime,
+ const char* aLinkName,
+ DDLifetime& aChildLifetime,
+ DDMessageIndex aIndex) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ mObjectLinks.AppendElement(DDObjectLink{
+ aParentLifetime.mObject, aChildLifetime.mObject, aLinkName, aIndex});
+ if (aParentLifetime.mMediaElement) {
+ if (!aChildLifetime.mMediaElement) {
+ SetMediaElement(aChildLifetime, aParentLifetime.mMediaElement);
+ }
+ } else if (aChildLifetime.mMediaElement) {
+ if (!aParentLifetime.mMediaElement) {
+ SetMediaElement(aParentLifetime, aChildLifetime.mMediaElement);
+ }
+ }
+}
+
+void DDMediaLogs::UnlinkLifetime(DDLifetime& aLifetime, DDMessageIndex aIndex) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ for (DDObjectLink& link : mObjectLinks) {
+ if ((link.mParent == aLifetime.mObject ||
+ link.mChild == aLifetime.mObject) &&
+ aLifetime.IsAliveAt(link.mLinkingIndex) && !link.mUnlinkingIndex) {
+ link.mUnlinkingIndex = Some(aIndex);
+ }
+ }
+};
+
+void DDMediaLogs::UnlinkLifetimes(DDLifetime& aParentLifetime,
+ DDLifetime& aChildLifetime,
+ DDMessageIndex aIndex) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ for (DDObjectLink& link : mObjectLinks) {
+ if ((link.mParent == aParentLifetime.mObject &&
+ link.mChild == aChildLifetime.mObject) &&
+ aParentLifetime.IsAliveAt(link.mLinkingIndex) &&
+ aChildLifetime.IsAliveAt(link.mLinkingIndex) && !link.mUnlinkingIndex) {
+ link.mUnlinkingIndex = Some(aIndex);
+ }
+ }
+}
+
+void DDMediaLogs::DestroyLifetimeLinks(const DDLifetime& aLifetime) {
+ mObjectLinks.RemoveElementsBy([&](DDObjectLink& link) {
+ return (link.mParent == aLifetime.mObject ||
+ link.mChild == aLifetime.mObject) &&
+ aLifetime.IsAliveAt(link.mLinkingIndex);
+ });
+}
+
+size_t DDMediaLogs::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t size = aMallocSizeOf(this) +
+ // This will usually be called after processing, so negligible
+ // external data should still be present in the queue.
+ mMessagesQueue.ShallowSizeOfExcludingThis(aMallocSizeOf) +
+ mLifetimes.SizeOfExcludingThis(aMallocSizeOf) +
+ mMediaLogs.ShallowSizeOfExcludingThis(aMallocSizeOf) +
+ mObjectLinks.ShallowSizeOfExcludingThis(aMallocSizeOf) +
+ mPendingPromises.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const DDMediaLog& log : mMediaLogs) {
+ size += log.SizeOfExcludingThis(aMallocSizeOf);
+ }
+ return size;
+}
+
+void DDMediaLogs::ProcessBuffer() {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+
+ mMessagesQueue.PopAll([this](const DDLogMessage& message) {
+ DDL_DEBUG("Processing: %s", message.Print().Data());
+
+ // Either this message will carry a new object for which to create a
+ // lifetime, or we'll find an existing one.
+ DDLifetime& lifetime = FindOrCreateLifetime(message.mObject, message.mIndex,
+ message.mTimeStamp);
+
+ // Copy the message contents (without the mValid flag) to the
+ // appropriate MediaLog corresponding to the message's object lifetime.
+ LogFor(lifetime.mMediaElement)
+ .mMessages.AppendElement(static_cast<const DDLogMessage&>(message));
+
+ switch (message.mCategory) {
+ case DDLogCategory::_Construction:
+ // The FindOrCreateLifetime above will have set a construction time,
+ // so there's nothing more we need to do here.
+ MOZ_ASSERT(lifetime.mConstructionTimeStamp);
+ break;
+
+ case DDLogCategory::_DerivedConstruction:
+ // The FindOrCreateLifetime above will have set a construction time.
+ MOZ_ASSERT(lifetime.mConstructionTimeStamp);
+ // A derived construction must come with the base object.
+ MOZ_ASSERT(message.mValue.is<DDLogObject>());
+ {
+ const DDLogObject& base = message.mValue.as<DDLogObject>();
+ DDLifetime& baseLifetime =
+ FindOrCreateLifetime(base, message.mIndex, message.mTimeStamp);
+ // FindOrCreateLifetime could have moved `lifetime`.
+ DDLifetime* lifetime2 =
+ mLifetimes.FindLifetime(message.mObject, message.mIndex);
+ MOZ_ASSERT(lifetime2);
+ // Assume there's no multiple-inheritance (at least for the types
+ // we're watching.)
+ if (baseLifetime.mDerivedObject.Pointer()) {
+ DDL_WARN(
+ "base '%s' was already derived as '%s', now deriving as '%s'",
+ baseLifetime.Printf().get(),
+ baseLifetime.mDerivedObject.Printf().get(),
+ lifetime2->Printf().get());
+ }
+ baseLifetime.mDerivedObject = lifetime2->mObject;
+ baseLifetime.mDerivedObjectLinkingIndex = message.mIndex;
+ // Link the base and derived objects, to ensure they go to the same
+ // log.
+ LinkLifetimes(*lifetime2, "is-a", baseLifetime, message.mIndex);
+ }
+ break;
+
+ case DDLogCategory::_Destruction:
+ lifetime.mDestructionIndex = message.mIndex;
+ lifetime.mDestructionTimeStamp = message.mTimeStamp;
+ UnlinkLifetime(lifetime, message.mIndex);
+ break;
+
+ case DDLogCategory::_Link:
+ MOZ_ASSERT(message.mValue.is<DDLogObject>());
+ {
+ const DDLogObject& child = message.mValue.as<DDLogObject>();
+ DDLifetime& childLifetime =
+ FindOrCreateLifetime(child, message.mIndex, message.mTimeStamp);
+ // FindOrCreateLifetime could have moved `lifetime`.
+ DDLifetime* lifetime2 =
+ mLifetimes.FindLifetime(message.mObject, message.mIndex);
+ MOZ_ASSERT(lifetime2);
+ LinkLifetimes(*lifetime2, message.mLabel, childLifetime,
+ message.mIndex);
+ }
+ break;
+
+ case DDLogCategory::_Unlink:
+ MOZ_ASSERT(message.mValue.is<DDLogObject>());
+ {
+ const DDLogObject& child = message.mValue.as<DDLogObject>();
+ DDLifetime& childLifetime =
+ FindOrCreateLifetime(child, message.mIndex, message.mTimeStamp);
+ // FindOrCreateLifetime could have moved `lifetime`.
+ DDLifetime* lifetime2 =
+ mLifetimes.FindLifetime(message.mObject, message.mIndex);
+ MOZ_ASSERT(lifetime2);
+ UnlinkLifetimes(*lifetime2, childLifetime, message.mIndex);
+ }
+ break;
+
+ default:
+ // Anything else: Nothing more to do.
+ break;
+ }
+ });
+}
+
+void DDMediaLogs::FulfillPromises() {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+
+ MozPromiseHolder<LogMessagesPromise> promiseHolder;
+ const dom::HTMLMediaElement* mediaElement = nullptr;
+ {
+ // Grab the first pending promise (if any).
+ // Note that we don't pop it yet, so we don't potentially leave the list
+ // empty and therefore allow another processing task to be dispatched.
+ MutexAutoLock lock(mMutex);
+ if (mPendingPromises.IsEmpty()) {
+ return;
+ }
+ promiseHolder = std::move(mPendingPromises[0].mPromiseHolder);
+ mediaElement = mPendingPromises[0].mMediaElement;
+ }
+ for (;;) {
+ DDMediaLog* log = GetLogFor(mediaElement);
+ if (!log) {
+ // No such media element -> Reject this promise.
+ DDL_INFO("Rejecting promise for HTMLMediaElement[%p] - Cannot find log",
+ mediaElement);
+ promiseHolder.Reject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR, __func__);
+ // Pop this rejected promise, fetch next one.
+ MutexAutoLock lock(mMutex);
+ mPendingPromises.RemoveElementAt(0);
+ if (mPendingPromises.IsEmpty()) {
+ break;
+ }
+ promiseHolder = std::move(mPendingPromises[0].mPromiseHolder);
+ mediaElement = mPendingPromises[0].mMediaElement;
+ continue;
+ }
+
+ JSONStringWriteFunc<nsCString> json;
+ JSONWriter jw{json};
+ jw.Start();
+ jw.StartArrayProperty("messages");
+ for (const DDLogMessage& message : log->mMessages) {
+ jw.StartObjectElement(JSONWriter::SingleLineStyle);
+ jw.IntProperty("i", message.mIndex.Value());
+ jw.DoubleProperty("ts", ToSeconds(message.mTimeStamp));
+ DDLifetime* lifetime =
+ mLifetimes.FindLifetime(message.mObject, message.mIndex);
+ if (lifetime) {
+ jw.IntProperty("ob", lifetime->mTag);
+ } else {
+ jw.StringProperty(
+ "ob", nsPrintfCString(R"("%s[%p]")", message.mObject.TypeName(),
+ message.mObject.Pointer()));
+ }
+ jw.StringProperty("cat",
+ MakeStringSpan(ToShortString(message.mCategory)));
+ if (message.mLabel && message.mLabel[0] != '\0') {
+ jw.StringProperty("lbl", MakeStringSpan(message.mLabel));
+ }
+ if (!message.mValue.is<DDNoValue>()) {
+ if (message.mValue.is<DDLogObject>()) {
+ const DDLogObject& ob2 = message.mValue.as<DDLogObject>();
+ DDLifetime* lifetime2 = mLifetimes.FindLifetime(ob2, message.mIndex);
+ if (lifetime2) {
+ jw.IntProperty("val", lifetime2->mTag);
+ } else {
+ ToJSON(message.mValue, jw, "val");
+ }
+ } else {
+ ToJSON(message.mValue, jw, "val");
+ }
+ }
+ jw.EndObject();
+ }
+ jw.EndArray();
+ jw.StartObjectProperty("objects");
+ mLifetimes.Visit(
+ mediaElement,
+ [&](const DDLifetime& lifetime) {
+ jw.StartObjectProperty(nsPrintfCString("%" PRIi32, lifetime.mTag),
+ JSONWriter::SingleLineStyle);
+ jw.IntProperty("tag", lifetime.mTag);
+ jw.StringProperty("cls", MakeStringSpan(lifetime.mObject.TypeName()));
+ jw.StringProperty("ptr",
+ nsPrintfCString("%p", lifetime.mObject.Pointer()));
+ jw.IntProperty("con", lifetime.mConstructionIndex.Value());
+ jw.DoubleProperty("con_ts",
+ ToSeconds(lifetime.mConstructionTimeStamp));
+ if (lifetime.mDestructionTimeStamp) {
+ jw.IntProperty("des", lifetime.mDestructionIndex.Value());
+ jw.DoubleProperty("des_ts",
+ ToSeconds(lifetime.mDestructionTimeStamp));
+ }
+ if (lifetime.mDerivedObject.Pointer()) {
+ DDLifetime* derived = mLifetimes.FindLifetime(
+ lifetime.mDerivedObject, lifetime.mDerivedObjectLinkingIndex);
+ if (derived) {
+ jw.IntProperty("drvd", derived->mTag);
+ }
+ }
+ jw.EndObject();
+ },
+ // If there were no (new) messages, only give the main HTMLMediaElement
+ // object (used to identify this log against the correct element.)
+ log->mMessages.IsEmpty());
+ jw.EndObject();
+ jw.End();
+ DDL_DEBUG("RetrieveMessages(%p) ->\n%s", mediaElement,
+ json.StringCRef().get());
+
+ // This log exists (new messages or not) -> Resolve this promise.
+ DDL_INFO("Resolving promise for HTMLMediaElement[%p] with messages %" PRImi
+ "-%" PRImi,
+ mediaElement,
+ log->mMessages.IsEmpty() ? 0 : log->mMessages[0].mIndex.Value(),
+ log->mMessages.IsEmpty()
+ ? 0
+ : log->mMessages[log->mMessages.Length() - 1].mIndex.Value());
+ promiseHolder.Resolve(std::move(json).StringRRef(), __func__);
+
+ // Remove exported messages.
+ log->mMessages.Clear();
+
+ // Pop this resolved promise, fetch next one.
+ MutexAutoLock lock(mMutex);
+ mPendingPromises.RemoveElementAt(0);
+ if (mPendingPromises.IsEmpty()) {
+ break;
+ }
+ promiseHolder = std::move(mPendingPromises[0].mPromiseHolder);
+ mediaElement = mPendingPromises[0].mMediaElement;
+ }
+}
+
+void DDMediaLogs::CleanUpLogs() {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+
+ DDTimeStamp now = DDNow();
+
+ // Keep up to 30s of unclassified messages (if a message doesn't get
+ // classified this quickly, it probably never will be.)
+ static const double sMaxAgeUnclassifiedMessages_s = 30.0;
+ // Keep "dead" log (video element and dependents were destroyed) for up to
+ // 2 minutes, in case the user wants to look at it after the facts.
+ static const double sMaxAgeDeadLog_s = 120.0;
+ // Keep old messages related to a live video for up to 5 minutes.
+ static const double sMaxAgeClassifiedMessages_s = 300.0;
+
+ for (size_t logIndexPlus1 = mMediaLogs.Length(); logIndexPlus1 != 0;
+ --logIndexPlus1) {
+ DDMediaLog& log = mMediaLogs[logIndexPlus1 - 1];
+ if (log.mMediaElement) {
+ // Remove logs for which no lifetime still existed some time ago.
+ bool used = mLifetimes.VisitBreakable(
+ log.mMediaElement, [&](const DDLifetime& lifetime) {
+ // Do we still have a lifetime that existed recently enough?
+ return !lifetime.mDestructionTimeStamp ||
+ (now - lifetime.mDestructionTimeStamp).ToSeconds() <=
+ sMaxAgeDeadLog_s;
+ });
+ if (!used) {
+ DDL_INFO("Removed old log for media element %p", log.mMediaElement);
+ mLifetimes.Visit(log.mMediaElement, [&](const DDLifetime& lifetime) {
+ DestroyLifetimeLinks(lifetime);
+ });
+ mLifetimes.RemoveLifetimesFor(log.mMediaElement);
+ mMediaLogs.RemoveElementAt(logIndexPlus1 - 1);
+ continue;
+ }
+ }
+
+ // Remove old messages.
+ size_t old = 0;
+ const size_t len = log.mMessages.Length();
+ while (old < len &&
+ (now - log.mMessages[old].mTimeStamp).ToSeconds() >
+ (log.mMediaElement ? sMaxAgeClassifiedMessages_s
+ : sMaxAgeUnclassifiedMessages_s)) {
+ ++old;
+ }
+ if (old != 0) {
+ // We are going to remove `old` messages.
+ // First, remove associated destroyed lifetimes that are not used after
+ // these old messages. (We want to keep non-destroyed lifetimes, in
+ // case they get used later on.)
+ size_t removedLifetimes = 0;
+ for (size_t i = 0; i < old; ++i) {
+ auto RemoveDestroyedUnusedLifetime = [&](DDLifetime* lifetime) {
+ if (!lifetime->mDestructionTimeStamp) {
+ // Lifetime is still alive, keep it.
+ return;
+ }
+ bool used = false;
+ for (size_t after = old; after < len; ++after) {
+ const DDLogMessage message = log.mMessages[i];
+ if (!lifetime->IsAliveAt(message.mIndex)) {
+ // Lifetime is already dead, and not used yet -> kill it.
+ break;
+ }
+ const DDLogObject& ob = message.mObject;
+ if (lifetime->mObject == ob) {
+ used = true;
+ break;
+ }
+ if (message.mValue.is<DDLogObject>()) {
+ if (lifetime->mObject == message.mValue.as<DDLogObject>()) {
+ used = true;
+ break;
+ }
+ }
+ }
+ if (!used) {
+ DestroyLifetimeLinks(*lifetime);
+ mLifetimes.RemoveLifetime(lifetime);
+ ++removedLifetimes;
+ }
+ };
+
+ const DDLogMessage message = log.mMessages[i];
+ const DDLogObject& ob = message.mObject;
+
+ DDLifetime* lifetime1 = mLifetimes.FindLifetime(ob, message.mIndex);
+ if (lifetime1) {
+ RemoveDestroyedUnusedLifetime(lifetime1);
+ }
+
+ if (message.mValue.is<DDLogObject>()) {
+ DDLifetime* lifetime2 = mLifetimes.FindLifetime(
+ message.mValue.as<DDLogObject>(), message.mIndex);
+ if (lifetime2) {
+ RemoveDestroyedUnusedLifetime(lifetime2);
+ }
+ }
+ }
+ DDL_INFO("Removed %zu messages (#%" PRImi " %f - #%" PRImi
+ " %f) and %zu lifetimes from log for media element %p",
+ old, log.mMessages[0].mIndex.Value(),
+ ToSeconds(log.mMessages[0].mTimeStamp),
+ log.mMessages[old - 1].mIndex.Value(),
+ ToSeconds(log.mMessages[old - 1].mTimeStamp), removedLifetimes,
+ log.mMediaElement);
+ log.mMessages.RemoveElementsAt(0, old);
+ }
+ }
+}
+
+void DDMediaLogs::ProcessLog() {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ ProcessBuffer();
+ FulfillPromises();
+ CleanUpLogs();
+ DDL_INFO("ProcessLog() completed - DDMediaLog size: %zu",
+ SizeOfIncludingThis(moz_malloc_size_of));
+}
+
+nsresult DDMediaLogs::DispatchProcessLog(const MutexAutoLock& aProofOfLock) {
+ if (!mThread) {
+ return NS_ERROR_SERVICE_NOT_AVAILABLE;
+ }
+ return mThread->Dispatch(
+ NS_NewRunnableFunction("ProcessLog", [this] { ProcessLog(); }),
+ NS_DISPATCH_NORMAL);
+}
+
+nsresult DDMediaLogs::DispatchProcessLog() {
+ DDL_INFO("DispatchProcessLog() - Yet-unprocessed message buffers: %d",
+ mMessagesQueue.LiveBuffersStats().mCount);
+ MutexAutoLock lock(mMutex);
+ return DispatchProcessLog(lock);
+}
+
+RefPtr<DDMediaLogs::LogMessagesPromise> DDMediaLogs::RetrieveMessages(
+ const dom::HTMLMediaElement* aMediaElement) {
+ MozPromiseHolder<LogMessagesPromise> holder;
+ RefPtr<LogMessagesPromise> promise = holder.Ensure(__func__);
+ {
+ MutexAutoLock lock(mMutex);
+ // If there were unfulfilled promises, we know processing has already
+ // been requested.
+ if (mPendingPromises.IsEmpty()) {
+ // But if we're the first one, start processing.
+ nsresult rv = DispatchProcessLog(lock);
+ if (NS_FAILED(rv)) {
+ holder.Reject(rv, __func__);
+ }
+ }
+ mPendingPromises.AppendElement(
+ PendingPromise{std::move(holder), aMediaElement});
+ }
+ return promise;
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDMediaLogs.h b/dom/media/doctor/DDMediaLogs.h
new file mode 100644
index 0000000000..ef5bbe98f9
--- /dev/null
+++ b/dom/media/doctor/DDMediaLogs.h
@@ -0,0 +1,193 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DDMediaLogs_h_
+#define DDMediaLogs_h_
+
+#include "DDLifetimes.h"
+#include "DDMediaLog.h"
+#include "mozilla/MozPromise.h"
+#include "MultiWriterQueue.h"
+
+namespace mozilla {
+
+// Main object managing all processed logs, and yet-unprocessed messages.
+struct DDMediaLogs {
+ public:
+ // Construct a DDMediaLogs object if possible.
+ struct ConstructionResult {
+ nsresult mRv;
+ DDMediaLogs* mMediaLogs;
+ };
+ static ConstructionResult New();
+
+ // If not already shutdown, performs normal end-of-life processing, and shuts
+ // down the processing thread (blocking).
+ ~DDMediaLogs();
+
+ // Shutdown the processing thread (blocking), and free as much memory as
+ // possible.
+ void Panic();
+
+ inline void Log(const char* aSubjectTypeName, const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ DDLogValue&& aValue) {
+ if (mMessagesQueue.PushF(
+ [&](DDLogMessage& aMessage, MessagesQueue::Index i) {
+ aMessage.mIndex = i;
+ aMessage.mTimeStamp = DDNow();
+ aMessage.mObject.Set(aSubjectTypeName, aSubjectPointer);
+ aMessage.mCategory = aCategory;
+ aMessage.mLabel = aLabel;
+ aMessage.mValue = std::move(aValue);
+ })) {
+ // Filled a buffer-full of messages, process it in another thread.
+ DispatchProcessLog();
+ }
+ }
+
+ // Process the log right now; should only be used on the processing thread,
+ // or after shutdown for end-of-life log retrieval. Work includes:
+ // - Processing incoming buffers, to update object lifetimes and links;
+ // - Resolve pending promises that requested logs;
+ // - Clean-up old logs from memory.
+ void ProcessLog();
+
+ using LogMessagesPromise =
+ MozPromise<nsCString, nsresult, /* IsExclusive = */ true>;
+
+ // Retrieve all messages associated with an HTMLMediaElement.
+ // This will trigger an async processing run (to ensure most recent messages
+ // get retrieved too), and the returned promise will be resolved with all
+ // found log messages.
+ RefPtr<LogMessagesPromise> RetrieveMessages(
+ const dom::HTMLMediaElement* aMediaElement);
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ // Constructor, takes the given thread to use for log processing.
+ explicit DDMediaLogs(nsCOMPtr<nsIThread>&& aThread);
+
+ // Shutdown the processing thread, blocks until that thread exits.
+ // If aPanic is true, just free as much memory as possible.
+ // Otherwise, perform a final processing run, output end-logs (if enabled).
+ void Shutdown(bool aPanic);
+
+ // Get the log of yet-unassociated messages.
+ DDMediaLog& LogForUnassociatedMessages();
+ const DDMediaLog& LogForUnassociatedMessages() const;
+
+ // Get the log for the given HTMLMediaElement. Returns nullptr if there is no
+ // such log yet.
+ DDMediaLog* GetLogFor(const dom::HTMLMediaElement* aMediaElement);
+
+ // Get the log for the given HTMLMediaElement.
+ // A new log is created if that element didn't already have one.
+ DDMediaLog& LogFor(const dom::HTMLMediaElement* aMediaElement);
+
+ // Associate a lifetime, and all its already-linked lifetimes, with an
+ // HTMLMediaElement.
+ // All messages involving the modified lifetime(s) are moved to the
+ // corresponding log.
+ void SetMediaElement(DDLifetime& aLifetime,
+ const dom::HTMLMediaElement* aMediaElement);
+
+ // Find the lifetime corresponding to an object (known type and pointer) that
+ // was known to be alive at aIndex.
+ // If there is no such lifetime yet, create it with aTimeStamp as implicit
+ // construction timestamp.
+ // If the object is of type HTMLMediaElement, run SetMediaElement() on it.
+ DDLifetime& FindOrCreateLifetime(const DDLogObject& aObject,
+ DDMessageIndex aIndex,
+ const DDTimeStamp& aTimeStamp);
+
+ // Link two lifetimes together (at a given time corresponding to aIndex).
+ // If only one is associated with an HTMLMediaElement, run SetMediaElement on
+ // the other one.
+ void LinkLifetimes(DDLifetime& aParentLifetime, const char* aLinkName,
+ DDLifetime& aChildLifetime, DDMessageIndex aIndex);
+
+ // Unlink all lifetimes linked to aLifetime; only used to know when links
+ // expire, so that they won't be used after this time.
+ void UnlinkLifetime(DDLifetime& aLifetime, DDMessageIndex aIndex);
+
+ // Unlink two lifetimes; only used to know when a link expires, so that it
+ // won't be used after this time.
+ void UnlinkLifetimes(DDLifetime& aParentLifetime, DDLifetime& aChildLifetime,
+ DDMessageIndex aIndex);
+
+ // Remove all links involving aLifetime from the database.
+ void DestroyLifetimeLinks(const DDLifetime& aLifetime);
+
+ // Process all incoming log messages.
+ // This will create the appropriate DDLifetime and links objects, and then
+ // move processed messages to logs associated with different
+ // HTMLMediaElements.
+ void ProcessBuffer();
+
+ // Pending promises (added by RetrieveMessages) are resolved with all new
+ // log messages corresponding to requested HTMLMediaElements -- These
+ // messages are removed from our logs.
+ void FulfillPromises();
+
+ // Remove processed messages that have a low chance of being requested,
+ // based on the assumption that users/scripts will regularly call
+ // RetrieveMessages for HTMLMediaElements they are interested in.
+ void CleanUpLogs();
+
+ // Request log-processing on the processing thread. Thread-safe.
+ nsresult DispatchProcessLog();
+
+ // Request log-processing on the processing thread.
+ nsresult DispatchProcessLog(const MutexAutoLock& aProofOfLock);
+
+ using MessagesQueue =
+ MultiWriterQueue<DDLogMessage, MultiWriterQueueDefaultBufferSize,
+ MultiWriterQueueReaderLocking_None>;
+ MessagesQueue mMessagesQueue;
+
+ DDLifetimes mLifetimes;
+
+ // mMediaLogs[0] contains unsorted message (with mMediaElement=nullptr).
+ // mMediaLogs[1+] contains sorted messages for each media element.
+ nsTArray<DDMediaLog> mMediaLogs;
+
+ struct DDObjectLink {
+ const DDLogObject mParent;
+ const DDLogObject mChild;
+ const char* const mLinkName;
+ const DDMessageIndex mLinkingIndex;
+ Maybe<DDMessageIndex> mUnlinkingIndex;
+
+ DDObjectLink(DDLogObject aParent, DDLogObject aChild, const char* aLinkName,
+ DDMessageIndex aLinkingIndex)
+ : mParent(aParent),
+ mChild(aChild),
+ mLinkName(aLinkName),
+ mLinkingIndex(aLinkingIndex),
+ mUnlinkingIndex(Nothing{}) {}
+ };
+ // Links between live objects, updated while messages are processed.
+ nsTArray<DDObjectLink> mObjectLinks;
+
+ // Protects members below.
+ Mutex mMutex MOZ_UNANNOTATED;
+
+ // Processing thread.
+ nsCOMPtr<nsIThread> mThread;
+
+ struct PendingPromise {
+ MozPromiseHolder<LogMessagesPromise> mPromiseHolder;
+ const dom::HTMLMediaElement* mMediaElement;
+ };
+ // Most cases should have 1 media panel requesting 1 promise at a time.
+ AutoTArray<PendingPromise, 2> mPendingPromises;
+};
+
+} // namespace mozilla
+
+#endif // DDMediaLogs_h_
diff --git a/dom/media/doctor/DDMessageIndex.h b/dom/media/doctor/DDMessageIndex.h
new file mode 100644
index 0000000000..e2b5c7274c
--- /dev/null
+++ b/dom/media/doctor/DDMessageIndex.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DDMessageIndex_h_
+#define DDMessageIndex_h_
+
+#include "RollingNumber.h"
+
+namespace mozilla {
+
+// Type used to uniquely identify and sort log messages.
+// We assume that a given media element won't live for more than the time taken
+// to log 2^31 messages (per process); for 10,000 messages per seconds, that's
+// about 2.5 days
+using DDMessageIndex = RollingNumber<uint32_t>;
+
+// Printf string constant to use when printing a DDMessageIndex, e.g.:
+// `printf("index=%" PRImi, index);`
+#define PRImi PRIu32
+
+} // namespace mozilla
+
+#endif // DDMessageIndex_h_
diff --git a/dom/media/doctor/DDTimeStamp.cpp b/dom/media/doctor/DDTimeStamp.cpp
new file mode 100644
index 0000000000..b440c559d7
--- /dev/null
+++ b/dom/media/doctor/DDTimeStamp.cpp
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "DDTimeStamp.h"
+
+namespace mozilla {
+
+double ToSeconds(const DDTimeStamp& aTimeStamp) {
+ // Timestamp at first call, used internally to convert log timestamps
+ // to a duration from this timestamp.
+ // What's important is the relative time between log messages.
+ static const DDTimeStamp sInitialTimeStamp = TimeStamp::Now();
+
+ return (aTimeStamp - sInitialTimeStamp).ToSeconds();
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDTimeStamp.h b/dom/media/doctor/DDTimeStamp.h
new file mode 100644
index 0000000000..71cbfb8101
--- /dev/null
+++ b/dom/media/doctor/DDTimeStamp.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DDTimeStamp_h_
+#define DDTimeStamp_h_
+
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+
+// Simply using mozilla::TimeStamp as our timestamp type.
+using DDTimeStamp = TimeStamp;
+
+inline DDTimeStamp DDNow() { return TimeStamp::Now(); }
+
+// Convert a timestamp to the number of seconds since the process start.
+double ToSeconds(const DDTimeStamp& aTimeStamp);
+
+} // namespace mozilla
+
+#endif // DDTimeStamp_h_
diff --git a/dom/media/doctor/DecoderDoctorDiagnostics.cpp b/dom/media/doctor/DecoderDoctorDiagnostics.cpp
new file mode 100644
index 0000000000..164c614547
--- /dev/null
+++ b/dom/media/doctor/DecoderDoctorDiagnostics.cpp
@@ -0,0 +1,1319 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 <string.h>
+
+#include "VideoUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/dom/Document.h"
+#include "nsContentUtils.h"
+#include "nsGkAtoms.h"
+#include "nsIObserverService.h"
+#include "nsIScriptError.h"
+#include "nsITimer.h"
+#include "nsPluginHost.h"
+#include "nsPrintfCString.h"
+
+#if defined(MOZ_FFMPEG)
+# include "FFmpegRuntimeLinker.h"
+#endif
+
+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.
+//
+// Referenced by the document through a nsINode property, mTimer, and
+// inter-task captures.
+// When notified that the document is dead, or when the timer expires but
+// nothing new happened, StopWatching() will remove the document property and
+// timer (if present), so no more work will happen and the watcher will be
+// destroyed once all references are gone.
+class DecoderDoctorDocumentWatcher : public nsITimerCallback, public nsINamed {
+ public:
+ static already_AddRefed<DecoderDoctorDocumentWatcher> RetrieveOrCreate(
+ dom::Document* aDocument);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ void AddDiagnostics(DecoderDoctorDiagnostics&& aDiagnostics,
+ const char* aCallSite);
+
+ private:
+ explicit DecoderDoctorDocumentWatcher(dom::Document* aDocument);
+ virtual ~DecoderDoctorDocumentWatcher();
+
+ // This will prevent further work from happening, watcher will deregister
+ // itself from document (if requested) and cancel any timer, and soon die.
+ void StopWatching(bool aRemoveProperty);
+
+ // Remove property from document; will call DestroyPropertyCallback.
+ void RemovePropertyFromDocument();
+ // Callback for property destructor, will be automatically called when the
+ // document (in aObject) is being destroyed.
+ static void DestroyPropertyCallback(void* aObject, nsAtom* aPropertyName,
+ void* aPropertyValue, void* aData);
+
+ static const uint32_t sAnalysisPeriod_ms = 1000;
+ void EnsureTimerIsStarted();
+
+ void SynthesizeAnalysis();
+ // This is used for testing and will generate an analysis based on the value
+ // set in `media.decoder-doctor.testing.fake-error`.
+ void SynthesizeFakeAnalysis();
+ bool ShouldSynthesizeFakeAnalysis() const;
+
+ // Raw pointer to a Document.
+ // Must be non-null during construction.
+ // Nulled when we want to stop watching, because either:
+ // 1. The document has been destroyed (notified through
+ // DestroyPropertyCallback).
+ // 2. We have not received new diagnostic information within a short time
+ // period, so we just stop watching.
+ // Once nulled, no more actual work will happen, and the watcher will be
+ // destroyed soon.
+ dom::Document* mDocument;
+
+ struct Diagnostics {
+ Diagnostics(DecoderDoctorDiagnostics&& aDiagnostics, const char* aCallSite,
+ mozilla::TimeStamp aTimeStamp)
+ : mDecoderDoctorDiagnostics(std::move(aDiagnostics)),
+ mCallSite(aCallSite),
+ mTimeStamp(aTimeStamp) {}
+ Diagnostics(const Diagnostics&) = delete;
+ Diagnostics(Diagnostics&& aOther)
+ : mDecoderDoctorDiagnostics(
+ std::move(aOther.mDecoderDoctorDiagnostics)),
+ mCallSite(std::move(aOther.mCallSite)),
+ mTimeStamp(aOther.mTimeStamp) {}
+
+ const DecoderDoctorDiagnostics mDecoderDoctorDiagnostics;
+ const nsCString mCallSite;
+ const mozilla::TimeStamp mTimeStamp;
+ };
+ typedef nsTArray<Diagnostics> DiagnosticsSequence;
+ DiagnosticsSequence mDiagnosticsSequence;
+
+ nsCOMPtr<nsITimer> mTimer; // Keep timer alive until we run.
+ DiagnosticsSequence::size_type mDiagnosticsHandled = 0;
+};
+
+NS_IMPL_ISUPPORTS(DecoderDoctorDocumentWatcher, nsITimerCallback, nsINamed)
+
+// static
+already_AddRefed<DecoderDoctorDocumentWatcher>
+DecoderDoctorDocumentWatcher::RetrieveOrCreate(dom::Document* aDocument) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aDocument);
+ RefPtr<DecoderDoctorDocumentWatcher> watcher =
+ static_cast<DecoderDoctorDocumentWatcher*>(
+ aDocument->GetProperty(nsGkAtoms::decoderDoctor));
+ if (!watcher) {
+ watcher = new DecoderDoctorDocumentWatcher(aDocument);
+ if (NS_WARN_IF(NS_FAILED(aDocument->SetProperty(
+ nsGkAtoms::decoderDoctor, watcher.get(), DestroyPropertyCallback,
+ /*transfer*/ false)))) {
+ DD_WARN(
+ "DecoderDoctorDocumentWatcher::RetrieveOrCreate(doc=%p) - Could not "
+ "set property in document, will destroy new watcher[%p]",
+ aDocument, watcher.get());
+ return nullptr;
+ }
+ // Document owns watcher through this property.
+ // Released in DestroyPropertyCallback().
+ NS_ADDREF(watcher.get());
+ }
+ return watcher.forget();
+}
+
+DecoderDoctorDocumentWatcher::DecoderDoctorDocumentWatcher(
+ dom::Document* aDocument)
+ : mDocument(aDocument) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDocument);
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p]::DecoderDoctorDocumentWatcher(doc=%p)",
+ this, mDocument);
+}
+
+DecoderDoctorDocumentWatcher::~DecoderDoctorDocumentWatcher() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p <- expect "
+ "0]::~DecoderDoctorDocumentWatcher()",
+ this, mDocument);
+ // mDocument should have been reset through StopWatching()!
+ MOZ_ASSERT(!mDocument);
+}
+
+void DecoderDoctorDocumentWatcher::RemovePropertyFromDocument() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DecoderDoctorDocumentWatcher* watcher =
+ static_cast<DecoderDoctorDocumentWatcher*>(
+ mDocument->GetProperty(nsGkAtoms::decoderDoctor));
+ if (!watcher) {
+ return;
+ }
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, "
+ "doc=%p]::RemovePropertyFromDocument()\n",
+ watcher, watcher->mDocument);
+ // This will call our DestroyPropertyCallback.
+ mDocument->RemoveProperty(nsGkAtoms::decoderDoctor);
+}
+
+// Callback for property destructors. |aObject| is the object
+// the property is being removed for, |aPropertyName| is the property
+// being removed, |aPropertyValue| is the value of the property, and |aData|
+// is the opaque destructor data that was passed to SetProperty().
+// static
+void DecoderDoctorDocumentWatcher::DestroyPropertyCallback(
+ void* aObject, nsAtom* aPropertyName, void* aPropertyValue, void*) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPropertyName == nsGkAtoms::decoderDoctor);
+ DecoderDoctorDocumentWatcher* watcher =
+ static_cast<DecoderDoctorDocumentWatcher*>(aPropertyValue);
+ MOZ_ASSERT(watcher);
+#ifdef DEBUG
+ auto* document = static_cast<dom::Document*>(aObject);
+ MOZ_ASSERT(watcher->mDocument == document);
+#endif
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::DestroyPropertyCallback()\n",
+ watcher, watcher->mDocument);
+ // 'false': StopWatching should not try and remove the property.
+ watcher->StopWatching(false);
+ NS_RELEASE(watcher);
+}
+
+void DecoderDoctorDocumentWatcher::StopWatching(bool aRemoveProperty) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // StopWatching() shouldn't be called twice.
+ MOZ_ASSERT(mDocument);
+
+ if (aRemoveProperty) {
+ RemovePropertyFromDocument();
+ }
+
+ // Forget document now, this will prevent more work from being started.
+ mDocument = nullptr;
+
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+}
+
+void DecoderDoctorDocumentWatcher::EnsureTimerIsStarted() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mTimer) {
+ NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, sAnalysisPeriod_ms,
+ nsITimer::TYPE_ONE_SHOT);
+ }
+}
+
+enum class ReportParam : uint8_t {
+ // Marks the end of the parameter list.
+ // Keep this zero! (For implicit zero-inits when used in definitions below.)
+ None = 0,
+
+ Formats,
+ DecodeIssue,
+ DocURL,
+ ResourceURL
+};
+
+struct NotificationAndReportStringId {
+ // Notification type, handled by DecoderDoctorChild.sys.mjs and
+ // DecoderDoctorParent.sys.mjs.
+ dom::DecoderDoctorNotificationType mNotificationType;
+ // Console message id. Key in dom/locales/.../chrome/dom/dom.properties.
+ const char* mReportStringId;
+ static const int maxReportParams = 4;
+ ReportParam mReportParams[maxReportParams];
+};
+
+// Note: ReportStringIds are limited to alphanumeric only.
+static const NotificationAndReportStringId sMediaWidevineNoWMF = {
+ dom::DecoderDoctorNotificationType::Platform_decoder_not_found,
+ "MediaWidevineNoWMF",
+ {ReportParam::None}};
+static const NotificationAndReportStringId sMediaWMFNeeded = {
+ dom::DecoderDoctorNotificationType::Platform_decoder_not_found,
+ "MediaWMFNeeded",
+ {ReportParam::Formats}};
+static const NotificationAndReportStringId sMediaFFMpegNotFound = {
+ dom::DecoderDoctorNotificationType::Platform_decoder_not_found,
+ "MediaPlatformDecoderNotFound",
+ {ReportParam::Formats}};
+static const NotificationAndReportStringId sMediaCannotPlayNoDecoders = {
+ dom::DecoderDoctorNotificationType::Cannot_play,
+ "MediaCannotPlayNoDecoders",
+ {ReportParam::Formats}};
+static const NotificationAndReportStringId sMediaNoDecoders = {
+ dom::DecoderDoctorNotificationType::Can_play_but_some_missing_decoders,
+ "MediaNoDecoders",
+ {ReportParam::Formats}};
+static const NotificationAndReportStringId sCannotInitializePulseAudio = {
+ dom::DecoderDoctorNotificationType::Cannot_initialize_pulseaudio,
+ "MediaCannotInitializePulseAudio",
+ {ReportParam::None}};
+static const NotificationAndReportStringId sUnsupportedLibavcodec = {
+ dom::DecoderDoctorNotificationType::Unsupported_libavcodec,
+ "MediaUnsupportedLibavcodec",
+ {ReportParam::None}};
+static const NotificationAndReportStringId sMediaDecodeError = {
+ dom::DecoderDoctorNotificationType::Decode_error,
+ "MediaDecodeError",
+ {ReportParam::ResourceURL, ReportParam::DecodeIssue}};
+static const NotificationAndReportStringId sMediaDecodeWarning = {
+ dom::DecoderDoctorNotificationType::Decode_warning,
+ "MediaDecodeWarning",
+ {ReportParam::ResourceURL, ReportParam::DecodeIssue}};
+
+static const NotificationAndReportStringId* const
+ sAllNotificationsAndReportStringIds[] = {
+ &sMediaWidevineNoWMF, &sMediaWMFNeeded,
+ &sMediaFFMpegNotFound, &sMediaCannotPlayNoDecoders,
+ &sMediaNoDecoders, &sCannotInitializePulseAudio,
+ &sUnsupportedLibavcodec, &sMediaDecodeError,
+ &sMediaDecodeWarning};
+
+// Create a webcompat-friendly description of a MediaResult.
+static nsString MediaResultDescription(const MediaResult& aResult,
+ bool aIsError) {
+ nsCString name;
+ GetErrorName(aResult.Code(), name);
+ return NS_ConvertUTF8toUTF16(nsPrintfCString(
+ "%s Code: %s (0x%08" PRIx32 ")%s%s", aIsError ? "Error" : "Warning",
+ name.get(), static_cast<uint32_t>(aResult.Code()),
+ aResult.Message().IsEmpty() ? "" : "\nDetails: ",
+ aResult.Message().get()));
+}
+
+static bool IsNotificationAllowedOnPlatform(
+ const NotificationAndReportStringId& aNotification) {
+ // Allow all notifications during testing.
+ if (StaticPrefs::media_decoder_doctor_testing()) {
+ return true;
+ }
+ // These notifications are platform independent.
+ if (aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Cannot_play ||
+ aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::
+ Can_play_but_some_missing_decoders ||
+ aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Decode_error ||
+ aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Decode_warning) {
+ return true;
+ }
+#if defined(XP_WIN)
+ if (aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Platform_decoder_not_found) {
+ return strcmp(sMediaWMFNeeded.mReportStringId,
+ aNotification.mReportStringId) == 0 ||
+ strcmp(sMediaWidevineNoWMF.mReportStringId,
+ aNotification.mReportStringId) == 0;
+ }
+#endif
+#if defined(MOZ_FFMPEG)
+ if (aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Platform_decoder_not_found) {
+ return strcmp(sMediaFFMpegNotFound.mReportStringId,
+ aNotification.mReportStringId) == 0;
+ }
+ if (aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Unsupported_libavcodec) {
+ return strcmp(sUnsupportedLibavcodec.mReportStringId,
+ aNotification.mReportStringId) == 0;
+ }
+#endif
+#ifdef MOZ_PULSEAUDIO
+ if (aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Cannot_initialize_pulseaudio) {
+ return strcmp(sCannotInitializePulseAudio.mReportStringId,
+ aNotification.mReportStringId) == 0;
+ }
+#endif
+ return false;
+}
+
+static void DispatchNotification(
+ nsISupports* aSubject, const NotificationAndReportStringId& aNotification,
+ bool aIsSolved, const nsAString& aFormats, const nsAString& aDecodeIssue,
+ const nsACString& aDocURL, const nsAString& aResourceURL) {
+ if (!aSubject) {
+ return;
+ }
+ dom::DecoderDoctorNotification data;
+ data.mType = aNotification.mNotificationType;
+ data.mIsSolved = aIsSolved;
+ data.mDecoderDoctorReportId.Assign(
+ NS_ConvertUTF8toUTF16(aNotification.mReportStringId));
+ if (!aFormats.IsEmpty()) {
+ data.mFormats.Construct(aFormats);
+ }
+ if (!aDecodeIssue.IsEmpty()) {
+ data.mDecodeIssue.Construct(aDecodeIssue);
+ }
+ if (!aDocURL.IsEmpty()) {
+ data.mDocURL.Construct(NS_ConvertUTF8toUTF16(aDocURL));
+ }
+ if (!aResourceURL.IsEmpty()) {
+ data.mResourceURL.Construct(aResourceURL);
+ }
+ nsAutoString json;
+ data.ToJSON(json);
+ if (json.IsEmpty()) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics/DispatchEvent() - Could not create json for "
+ "dispatch");
+ // No point in dispatching this notification without data, the front-end
+ // wouldn't know what to display.
+ return;
+ }
+ DD_DEBUG("DecoderDoctorDiagnostics/DispatchEvent() %s",
+ NS_ConvertUTF16toUTF8(json).get());
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(aSubject, "decoder-doctor-notification", json.get());
+ }
+}
+
+static void ReportToConsole(dom::Document* aDocument,
+ const char* aConsoleStringId,
+ const nsTArray<nsString>& aParams) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aDocument);
+
+ DD_DEBUG(
+ "DecoderDoctorDiagnostics.cpp:ReportToConsole(doc=%p) ReportToConsole"
+ " - aMsg='%s' params={%s%s%s%s}",
+ aDocument, aConsoleStringId,
+ aParams.IsEmpty() ? "<no params>"
+ : NS_ConvertUTF16toUTF8(aParams[0]).get(),
+ (aParams.Length() < 1 || aParams[0].IsEmpty()) ? "" : ", ",
+ (aParams.Length() < 1 || aParams[0].IsEmpty())
+ ? ""
+ : NS_ConvertUTF16toUTF8(aParams[0]).get(),
+ aParams.Length() < 2 ? "" : ", ...");
+ if (StaticPrefs::media_decoder_doctor_testing()) {
+ Unused << nsContentUtils::DispatchTrustedEvent(
+ aDocument, ToSupports(aDocument), u"mozreportmediaerror"_ns,
+ CanBubble::eNo, Cancelable::eNo);
+ }
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Media"_ns,
+ aDocument, nsContentUtils::eDOM_PROPERTIES,
+ aConsoleStringId, aParams);
+}
+
+static bool AllowNotification(
+ const NotificationAndReportStringId& aNotification) {
+ // "media.decoder-doctor.notifications-allowed" controls which notifications
+ // may be dispatched to the front-end. It either contains:
+ // - '*' -> Allow everything.
+ // - Comma-separater list of ids -> Allow if aReportStringId (from
+ // dom.properties) is one of them.
+ // - Nothing (missing or empty) -> Disable everything.
+ nsAutoCString filter;
+ Preferences::GetCString("media.decoder-doctor.notifications-allowed", filter);
+ return filter.EqualsLiteral("*") ||
+ StringListContains(filter, aNotification.mReportStringId);
+}
+
+static bool AllowDecodeIssue(const MediaResult& aDecodeIssue,
+ bool aDecodeIssueIsError) {
+ if (aDecodeIssue == NS_OK) {
+ // 'NS_OK' means we are not actually reporting a decode issue, so we
+ // allow the report.
+ return true;
+ }
+
+ // "media.decoder-doctor.decode-{errors,warnings}-allowed" controls which
+ // decode issues may be dispatched to the front-end. It either contains:
+ // - '*' -> Allow everything.
+ // - Comma-separater list of ids -> Allow if the issue name is one of them.
+ // - Nothing (missing or empty) -> Disable everything.
+ nsAutoCString filter;
+ Preferences::GetCString(aDecodeIssueIsError
+ ? "media.decoder-doctor.decode-errors-allowed"
+ : "media.decoder-doctor.decode-warnings-allowed",
+ filter);
+ if (filter.EqualsLiteral("*")) {
+ return true;
+ }
+
+ nsCString decodeIssueName;
+ GetErrorName(aDecodeIssue.Code(), static_cast<nsACString&>(decodeIssueName));
+ return StringListContains(filter, decodeIssueName);
+}
+
+static void ReportAnalysis(dom::Document* aDocument,
+ const NotificationAndReportStringId& aNotification,
+ bool aIsSolved, const nsAString& aFormats = u""_ns,
+ const MediaResult& aDecodeIssue = NS_OK,
+ bool aDecodeIssueIsError = true,
+ const nsACString& aDocURL = ""_ns,
+ const nsAString& aResourceURL = u""_ns) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aDocument) {
+ return;
+ }
+
+ // Some errors should only appear on the specific platform. Eg. WMF related
+ // error only happens on Windows.
+ if (!IsNotificationAllowedOnPlatform(aNotification)) {
+ DD_WARN("Platform doesn't support '%s'!", aNotification.mReportStringId);
+ return;
+ }
+
+ nsString decodeIssueDescription;
+ if (aDecodeIssue != NS_OK) {
+ decodeIssueDescription.Assign(
+ MediaResultDescription(aDecodeIssue, aDecodeIssueIsError));
+ }
+
+ // Report non-solved issues to console.
+ if (!aIsSolved) {
+ // Build parameter array needed by console message.
+ AutoTArray<nsString, NotificationAndReportStringId::maxReportParams> params;
+ for (int i = 0; i < NotificationAndReportStringId::maxReportParams; ++i) {
+ if (aNotification.mReportParams[i] == ReportParam::None) {
+ break;
+ }
+ switch (aNotification.mReportParams[i]) {
+ case ReportParam::Formats:
+ params.AppendElement(aFormats);
+ break;
+ case ReportParam::DecodeIssue:
+ params.AppendElement(decodeIssueDescription);
+ break;
+ case ReportParam::DocURL:
+ params.AppendElement(NS_ConvertUTF8toUTF16(aDocURL));
+ break;
+ case ReportParam::ResourceURL:
+ params.AppendElement(aResourceURL);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Bad notification parameter choice");
+ break;
+ }
+ }
+ ReportToConsole(aDocument, aNotification.mReportStringId, params);
+ }
+
+ const bool allowNotification = AllowNotification(aNotification);
+ const bool allowDecodeIssue =
+ AllowDecodeIssue(aDecodeIssue, aDecodeIssueIsError);
+ DD_INFO(
+ "ReportAnalysis for %s (decodeResult=%s) [AllowNotification=%d, "
+ "AllowDecodeIssue=%d]",
+ aNotification.mReportStringId, aDecodeIssue.ErrorName().get(),
+ allowNotification, allowDecodeIssue);
+ if (allowNotification && allowDecodeIssue) {
+ DispatchNotification(aDocument->GetInnerWindow(), aNotification, aIsSolved,
+ aFormats, decodeIssueDescription, aDocURL,
+ aResourceURL);
+ }
+}
+
+static nsString CleanItemForFormatsList(const nsAString& aItem) {
+ nsString item(aItem);
+ // Remove commas from item, as commas are used to separate items. It's fine
+ // to have a one-way mapping, it's only used for comparisons and in
+ // console display (where formats shouldn't contain commas in the first place)
+ item.ReplaceChar(',', ' ');
+ item.CompressWhitespace();
+ return item;
+}
+
+static void AppendToFormatsList(nsAString& aList, const nsAString& aItem) {
+ if (!aList.IsEmpty()) {
+ aList += u", "_ns;
+ }
+ aList += CleanItemForFormatsList(aItem);
+}
+
+static bool FormatsListContains(const nsAString& aList,
+ const nsAString& aItem) {
+ return StringListContains(aList, CleanItemForFormatsList(aItem));
+}
+
+static const char* GetLinkStatusLibraryName() {
+#if defined(MOZ_FFMPEG)
+ return FFmpegRuntimeLinker::LinkStatusLibraryName();
+#else
+ return "no library (ffmpeg disabled during build)";
+#endif
+}
+
+static const char* GetLinkStatusString() {
+#if defined(MOZ_FFMPEG)
+ return FFmpegRuntimeLinker::LinkStatusString();
+#else
+ return "no link (ffmpeg disabled during build)";
+#endif
+}
+
+void DecoderDoctorDocumentWatcher::SynthesizeFakeAnalysis() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoCString errorType;
+ Preferences::GetCString("media.decoder-doctor.testing.fake-error", errorType);
+ MOZ_ASSERT(!errorType.IsEmpty());
+ for (const auto& id : sAllNotificationsAndReportStringIds) {
+ if (strcmp(id->mReportStringId, errorType.get()) == 0) {
+ if (id->mNotificationType ==
+ dom::DecoderDoctorNotificationType::Decode_error) {
+ ReportAnalysis(mDocument, *id, false /* isSolved */, u"*"_ns,
+ NS_ERROR_DOM_MEDIA_DECODE_ERR, true /* IsDecodeError */);
+ } else {
+ ReportAnalysis(mDocument, *id, false /* isSolved */, u"*"_ns, NS_OK,
+ false /* IsDecodeError */);
+ }
+ return;
+ }
+ }
+}
+
+void DecoderDoctorDocumentWatcher::SynthesizeAnalysis() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoString playableFormats;
+ nsAutoString unplayableFormats;
+ // Subsets of unplayableFormats that require a specific platform decoder:
+ nsAutoString formatsRequiringWMF;
+ nsAutoString formatsRequiringFFMpeg;
+ nsAutoString formatsLibAVCodecUnsupported;
+ nsAutoString supportedKeySystems;
+ nsAutoString unsupportedKeySystems;
+ DecoderDoctorDiagnostics::KeySystemIssue lastKeySystemIssue =
+ DecoderDoctorDiagnostics::eUnset;
+ // Only deal with one decode error per document (the first one found).
+ const MediaResult* firstDecodeError = nullptr;
+ const nsString* firstDecodeErrorMediaSrc = nullptr;
+ // Only deal with one decode warning per document (the first one found).
+ const MediaResult* firstDecodeWarning = nullptr;
+ const nsString* firstDecodeWarningMediaSrc = nullptr;
+
+ for (const auto& diag : mDiagnosticsSequence) {
+ switch (diag.mDecoderDoctorDiagnostics.Type()) {
+ case DecoderDoctorDiagnostics::eFormatSupportCheck:
+ if (diag.mDecoderDoctorDiagnostics.CanPlay()) {
+ AppendToFormatsList(playableFormats,
+ diag.mDecoderDoctorDiagnostics.Format());
+ } else {
+ AppendToFormatsList(unplayableFormats,
+ diag.mDecoderDoctorDiagnostics.Format());
+ if (diag.mDecoderDoctorDiagnostics.DidWMFFailToLoad()) {
+ AppendToFormatsList(formatsRequiringWMF,
+ diag.mDecoderDoctorDiagnostics.Format());
+ } else if (diag.mDecoderDoctorDiagnostics.DidFFmpegNotFound()) {
+ AppendToFormatsList(formatsRequiringFFMpeg,
+ diag.mDecoderDoctorDiagnostics.Format());
+ } else if (diag.mDecoderDoctorDiagnostics.IsLibAVCodecUnsupported()) {
+ AppendToFormatsList(formatsLibAVCodecUnsupported,
+ diag.mDecoderDoctorDiagnostics.Format());
+ }
+ }
+ break;
+ case DecoderDoctorDiagnostics::eMediaKeySystemAccessRequest:
+ if (diag.mDecoderDoctorDiagnostics.IsKeySystemSupported()) {
+ AppendToFormatsList(supportedKeySystems,
+ diag.mDecoderDoctorDiagnostics.KeySystem());
+ } else {
+ AppendToFormatsList(unsupportedKeySystems,
+ diag.mDecoderDoctorDiagnostics.KeySystem());
+ DecoderDoctorDiagnostics::KeySystemIssue issue =
+ diag.mDecoderDoctorDiagnostics.GetKeySystemIssue();
+ if (issue != DecoderDoctorDiagnostics::eUnset) {
+ lastKeySystemIssue = issue;
+ }
+ }
+ break;
+ case DecoderDoctorDiagnostics::eEvent:
+ MOZ_ASSERT_UNREACHABLE("Events shouldn't be stored for processing.");
+ break;
+ case DecoderDoctorDiagnostics::eDecodeError:
+ if (!firstDecodeError) {
+ firstDecodeError = &diag.mDecoderDoctorDiagnostics.DecodeIssue();
+ firstDecodeErrorMediaSrc =
+ &diag.mDecoderDoctorDiagnostics.DecodeIssueMediaSrc();
+ }
+ break;
+ case DecoderDoctorDiagnostics::eDecodeWarning:
+ if (!firstDecodeWarning) {
+ firstDecodeWarning = &diag.mDecoderDoctorDiagnostics.DecodeIssue();
+ firstDecodeWarningMediaSrc =
+ &diag.mDecoderDoctorDiagnostics.DecodeIssueMediaSrc();
+ }
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unhandled DecoderDoctorDiagnostics type");
+ break;
+ }
+ }
+
+ // Check if issues have been solved, by finding if some now-playable
+ // key systems or formats were previously recorded as having issues.
+ if (!supportedKeySystems.IsEmpty() || !playableFormats.IsEmpty()) {
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "supported key systems '%s', playable formats '%s'; See if they show "
+ "issues have been solved...",
+ this, mDocument, NS_ConvertUTF16toUTF8(supportedKeySystems).Data(),
+ NS_ConvertUTF16toUTF8(playableFormats).get());
+ const nsAString* workingFormatsArray[] = {&supportedKeySystems,
+ &playableFormats};
+ // For each type of notification, retrieve the pref that contains formats/
+ // key systems with issues.
+ for (const NotificationAndReportStringId* id :
+ sAllNotificationsAndReportStringIds) {
+ nsAutoCString formatsPref("media.decoder-doctor.");
+ formatsPref += id->mReportStringId;
+ formatsPref += ".formats";
+ nsAutoString formatsWithIssues;
+ Preferences::GetString(formatsPref.Data(), formatsWithIssues);
+ if (formatsWithIssues.IsEmpty()) {
+ continue;
+ }
+ // See if that list of formats-with-issues contains any formats that are
+ // now playable/supported.
+ bool solved = false;
+ for (const nsAString* workingFormats : workingFormatsArray) {
+ for (const auto& workingFormat : MakeStringListRange(*workingFormats)) {
+ if (FormatsListContains(formatsWithIssues, workingFormat)) {
+ // This now-working format used not to work -> Report solved issue.
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, "
+ "doc=%p]::SynthesizeAnalysis() - %s solved ('%s' now works, it "
+ "was in pref(%s)='%s')",
+ this, mDocument, id->mReportStringId,
+ NS_ConvertUTF16toUTF8(workingFormat).get(), formatsPref.Data(),
+ NS_ConvertUTF16toUTF8(formatsWithIssues).get());
+ ReportAnalysis(mDocument, *id, true, workingFormat);
+ // This particular Notification&ReportId has been solved, no need
+ // to keep looking at other keysys/formats that might solve it too.
+ solved = true;
+ break;
+ }
+ }
+ if (solved) {
+ break;
+ }
+ }
+ if (!solved) {
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "%s not solved (pref(%s)='%s')",
+ this, mDocument, id->mReportStringId, formatsPref.Data(),
+ NS_ConvertUTF16toUTF8(formatsWithIssues).get());
+ }
+ }
+ }
+
+ // Look at Key System issues first, as they take precedence over format
+ // checks.
+ if (!unsupportedKeySystems.IsEmpty() && supportedKeySystems.IsEmpty()) {
+ // No supported key systems!
+ switch (lastKeySystemIssue) {
+ case DecoderDoctorDiagnostics::eWidevineWithNoWMF:
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "unsupported key systems: %s, Widevine without WMF",
+ this, mDocument,
+ NS_ConvertUTF16toUTF8(unsupportedKeySystems).get());
+ ReportAnalysis(mDocument, sMediaWidevineNoWMF, false,
+ unsupportedKeySystems);
+ return;
+ default:
+ break;
+ }
+ }
+
+ // Next, check playability of requested formats.
+ if (!unplayableFormats.IsEmpty()) {
+ // Some requested formats cannot be played.
+ if (playableFormats.IsEmpty()) {
+ // No requested formats can be played. See if we can help the user, by
+ // going through expected decoders from most to least desirable.
+ if (!formatsRequiringWMF.IsEmpty()) {
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "unplayable formats: %s -> Cannot play media because WMF was not "
+ "found",
+ this, mDocument, NS_ConvertUTF16toUTF8(formatsRequiringWMF).get());
+ ReportAnalysis(mDocument, sMediaWMFNeeded, false, formatsRequiringWMF);
+ return;
+ }
+ if (!formatsRequiringFFMpeg.IsEmpty()) {
+ MOZ_DIAGNOSTIC_ASSERT(formatsLibAVCodecUnsupported.IsEmpty());
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, "
+ "doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> "
+ "Cannot play media because ffmpeg was not found (Reason: %s)",
+ this, mDocument,
+ NS_ConvertUTF16toUTF8(formatsRequiringFFMpeg).get(),
+ GetLinkStatusString());
+ ReportAnalysis(mDocument, sMediaFFMpegNotFound, false,
+ formatsRequiringFFMpeg);
+ return;
+ }
+ if (!formatsLibAVCodecUnsupported.IsEmpty()) {
+ MOZ_DIAGNOSTIC_ASSERT(formatsRequiringFFMpeg.IsEmpty());
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, "
+ "doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> "
+ "Cannot play media because of unsupported %s (Reason: %s)",
+ this, mDocument,
+ NS_ConvertUTF16toUTF8(formatsLibAVCodecUnsupported).get(),
+ GetLinkStatusLibraryName(), GetLinkStatusString());
+ ReportAnalysis(mDocument, sUnsupportedLibavcodec, false,
+ formatsLibAVCodecUnsupported);
+ return;
+ }
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "Cannot play media, unplayable formats: %s",
+ this, mDocument, NS_ConvertUTF16toUTF8(unplayableFormats).get());
+ ReportAnalysis(mDocument, sMediaCannotPlayNoDecoders, false,
+ unplayableFormats);
+ return;
+ }
+
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Can "
+ "play media, but no decoders for some requested formats: %s",
+ this, mDocument, NS_ConvertUTF16toUTF8(unplayableFormats).get());
+ if (Preferences::GetBool("media.decoder-doctor.verbose", false)) {
+ ReportAnalysis(mDocument, sMediaNoDecoders, false, unplayableFormats);
+ }
+ return;
+ }
+
+ if (firstDecodeError) {
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "Decode error: %s",
+ this, mDocument, firstDecodeError->Description().get());
+ ReportAnalysis(mDocument, sMediaDecodeError, false, u""_ns,
+ *firstDecodeError,
+ true, // aDecodeIssueIsError=true
+ mDocument->GetDocumentURI()->GetSpecOrDefault(),
+ *firstDecodeErrorMediaSrc);
+ return;
+ }
+
+ if (firstDecodeWarning) {
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "Decode warning: %s",
+ this, mDocument, firstDecodeWarning->Description().get());
+ ReportAnalysis(mDocument, sMediaDecodeWarning, false, u""_ns,
+ *firstDecodeWarning,
+ false, // aDecodeIssueIsError=false
+ mDocument->GetDocumentURI()->GetSpecOrDefault(),
+ *firstDecodeWarningMediaSrc);
+ return;
+ }
+
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Can "
+ "play media, decoders available for all requested formats",
+ this, mDocument);
+}
+
+void DecoderDoctorDocumentWatcher::AddDiagnostics(
+ DecoderDoctorDiagnostics&& aDiagnostics, const char* aCallSite) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aDiagnostics.Type() != DecoderDoctorDiagnostics::eEvent);
+
+ if (!mDocument) {
+ return;
+ }
+
+ const mozilla::TimeStamp now = mozilla::TimeStamp::Now();
+
+ constexpr size_t MaxDiagnostics = 128;
+ constexpr double MaxSeconds = 10.0;
+ while (
+ mDiagnosticsSequence.Length() > MaxDiagnostics ||
+ (!mDiagnosticsSequence.IsEmpty() &&
+ (now - mDiagnosticsSequence[0].mTimeStamp).ToSeconds() > MaxSeconds)) {
+ // Too many, or too old.
+ mDiagnosticsSequence.RemoveElementAt(0);
+ if (mDiagnosticsHandled != 0) {
+ // Make sure Notify picks up the new element added below.
+ --mDiagnosticsHandled;
+ }
+ }
+
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, "
+ "doc=%p]::AddDiagnostics(DecoderDoctorDiagnostics{%s}, call site '%s')",
+ this, mDocument, aDiagnostics.GetDescription().Data(), aCallSite);
+ mDiagnosticsSequence.AppendElement(
+ Diagnostics(std::move(aDiagnostics), aCallSite, now));
+ EnsureTimerIsStarted();
+}
+
+bool DecoderDoctorDocumentWatcher::ShouldSynthesizeFakeAnalysis() const {
+ if (!StaticPrefs::media_decoder_doctor_testing()) {
+ return false;
+ }
+ nsAutoCString errorType;
+ Preferences::GetCString("media.decoder-doctor.testing.fake-error", errorType);
+ return !errorType.IsEmpty();
+}
+
+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;
+
+ if (!mDocument) {
+ return NS_OK;
+ }
+
+ if (mDiagnosticsSequence.Length() > mDiagnosticsHandled) {
+ // We have new diagnostic data.
+ mDiagnosticsHandled = mDiagnosticsSequence.Length();
+
+ if (ShouldSynthesizeFakeAnalysis()) {
+ SynthesizeFakeAnalysis();
+ } else {
+ 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, mDocument);
+ // 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. 'true' to remove the property from the document.
+ StopWatching(true);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DecoderDoctorDocumentWatcher::GetName(nsACString& aName) {
+ aName.AssignLiteral("DecoderDoctorDocumentWatcher_timer");
+ return NS_OK;
+}
+
+void DecoderDoctorDiagnostics::StoreFormatDiagnostics(dom::Document* aDocument,
+ const nsAString& aFormat,
+ bool aCanPlay,
+ const char* aCallSite) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Make sure we only store once.
+ MOZ_ASSERT(mDiagnosticsType == eUnsaved);
+ mDiagnosticsType = eFormatSupportCheck;
+
+ if (NS_WARN_IF(aFormat.Length() > 2048)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(Document* "
+ "aDocument=%p, format= TOO LONG! '%s', can-play=%d, call site '%s')",
+ aDocument, this, NS_ConvertUTF16toUTF8(aFormat).get(), aCanPlay,
+ aCallSite);
+ return;
+ }
+
+ if (NS_WARN_IF(!aDocument)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(Document* "
+ "aDocument=nullptr, format='%s', can-play=%d, call site '%s')",
+ this, NS_ConvertUTF16toUTF8(aFormat).get(), aCanPlay, aCallSite);
+ return;
+ }
+ if (NS_WARN_IF(aFormat.IsEmpty())) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(Document* "
+ "aDocument=%p, format=<empty>, can-play=%d, call site '%s')",
+ this, aDocument, aCanPlay, aCallSite);
+ return;
+ }
+
+ RefPtr<DecoderDoctorDocumentWatcher> watcher =
+ DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument);
+
+ if (NS_WARN_IF(!watcher)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(Document* "
+ "aDocument=%p, format='%s', can-play=%d, call site '%s') - Could not "
+ "create document watcher",
+ this, aDocument, NS_ConvertUTF16toUTF8(aFormat).get(), aCanPlay,
+ aCallSite);
+ return;
+ }
+
+ mFormat = aFormat;
+ if (aCanPlay) {
+ mFlags += Flags::CanPlay;
+ } else {
+ mFlags -= Flags::CanPlay;
+ }
+
+ // StoreDiagnostics should only be called once, after all data is available,
+ // so it is safe to std::move() from this object.
+ watcher->AddDiagnostics(std::move(*this), aCallSite);
+ // Even though it's moved-from, the type should stay set
+ // (Only used to ensure that we do store only once.)
+ MOZ_ASSERT(mDiagnosticsType == eFormatSupportCheck);
+}
+
+void DecoderDoctorDiagnostics::StoreMediaKeySystemAccess(
+ dom::Document* aDocument, const nsAString& aKeySystem, bool aIsSupported,
+ const char* aCallSite) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Make sure we only store once.
+ MOZ_ASSERT(mDiagnosticsType == eUnsaved);
+ mDiagnosticsType = eMediaKeySystemAccessRequest;
+
+ if (NS_WARN_IF(aKeySystem.Length() > 2048)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(Document* "
+ "aDocument=%p, keysystem= TOO LONG! '%s', supported=%d, call site "
+ "'%s')",
+ aDocument, this, NS_ConvertUTF16toUTF8(aKeySystem).get(), aIsSupported,
+ aCallSite);
+ return;
+ }
+
+ if (NS_WARN_IF(!aDocument)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(Document* "
+ "aDocument=nullptr, keysystem='%s', supported=%d, call site '%s')",
+ this, NS_ConvertUTF16toUTF8(aKeySystem).get(), aIsSupported, aCallSite);
+ return;
+ }
+ if (NS_WARN_IF(aKeySystem.IsEmpty())) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(Document* "
+ "aDocument=%p, keysystem=<empty>, supported=%d, call site '%s')",
+ this, aDocument, aIsSupported, aCallSite);
+ return;
+ }
+
+ RefPtr<DecoderDoctorDocumentWatcher> watcher =
+ DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument);
+
+ if (NS_WARN_IF(!watcher)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(Document* "
+ "aDocument=%p, keysystem='%s', supported=%d, call site '%s') - Could "
+ "not create document watcher",
+ this, aDocument, NS_ConvertUTF16toUTF8(aKeySystem).get(), aIsSupported,
+ aCallSite);
+ return;
+ }
+
+ mKeySystem = aKeySystem;
+ mIsKeySystemSupported = aIsSupported;
+
+ // StoreMediaKeySystemAccess should only be called once, after all data is
+ // available, so it is safe to std::move() from this object.
+ watcher->AddDiagnostics(std::move(*this), aCallSite);
+ // Even though it's moved-from, the type should stay set
+ // (Only used to ensure that we do store only once.)
+ MOZ_ASSERT(mDiagnosticsType == eMediaKeySystemAccessRequest);
+}
+
+void DecoderDoctorDiagnostics::StoreEvent(dom::Document* aDocument,
+ const DecoderDoctorEvent& aEvent,
+ const char* aCallSite) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Make sure we only store once.
+ MOZ_ASSERT(mDiagnosticsType == eUnsaved);
+ mDiagnosticsType = eEvent;
+ mEvent = aEvent;
+
+ if (NS_WARN_IF(!aDocument)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreEvent(Document* "
+ "aDocument=nullptr, aEvent=%s, call site '%s')",
+ this, GetDescription().get(), aCallSite);
+ return;
+ }
+
+ // Don't keep events for later processing, just handle them now.
+ switch (aEvent.mDomain) {
+ case DecoderDoctorEvent::eAudioSinkStartup:
+ if (aEvent.mResult == NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR) {
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics() - "
+ "unable to initialize PulseAudio",
+ this, aDocument);
+ ReportAnalysis(aDocument, sCannotInitializePulseAudio, false, u"*"_ns);
+ } else if (aEvent.mResult == NS_OK) {
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics() - now "
+ "able to initialize PulseAudio",
+ this, aDocument);
+ ReportAnalysis(aDocument, sCannotInitializePulseAudio, true, u"*"_ns);
+ }
+ break;
+ }
+}
+
+void DecoderDoctorDiagnostics::StoreDecodeError(dom::Document* aDocument,
+ const MediaResult& aError,
+ const nsString& aMediaSrc,
+ const char* aCallSite) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Make sure we only store once.
+ MOZ_ASSERT(mDiagnosticsType == eUnsaved);
+ mDiagnosticsType = eDecodeError;
+
+ if (NS_WARN_IF(aError.Message().Length() > 2048)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreDecodeError(Document* "
+ "aDocument=%p, aError= TOO LONG! '%s', aMediaSrc=<provided>, call site "
+ "'%s')",
+ aDocument, this, aError.Description().get(), aCallSite);
+ return;
+ }
+
+ if (NS_WARN_IF(aMediaSrc.Length() > 2048)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreDecodeError(Document* "
+ "aDocument=%p, aError=%s, aMediaSrc= TOO LONG! <provided>, call site "
+ "'%s')",
+ aDocument, this, aError.Description().get(), aCallSite);
+ return;
+ }
+
+ if (NS_WARN_IF(!aDocument)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreDecodeError("
+ "Document* aDocument=nullptr, aError=%s,"
+ " aMediaSrc=<provided>, call site '%s')",
+ this, aError.Description().get(), aCallSite);
+ return;
+ }
+
+ RefPtr<DecoderDoctorDocumentWatcher> watcher =
+ DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument);
+
+ if (NS_WARN_IF(!watcher)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreDecodeError("
+ "Document* aDocument=%p, aError='%s', aMediaSrc=<provided>,"
+ " call site '%s') - Could not create document watcher",
+ this, aDocument, aError.Description().get(), aCallSite);
+ return;
+ }
+
+ mDecodeIssue = aError;
+ mDecodeIssueMediaSrc = aMediaSrc;
+
+ // StoreDecodeError should only be called once, after all data is
+ // available, so it is safe to std::move() from this object.
+ watcher->AddDiagnostics(std::move(*this), aCallSite);
+ // Even though it's moved-from, the type should stay set
+ // (Only used to ensure that we do store only once.)
+ MOZ_ASSERT(mDiagnosticsType == eDecodeError);
+}
+
+void DecoderDoctorDiagnostics::StoreDecodeWarning(dom::Document* aDocument,
+ const MediaResult& aWarning,
+ const nsString& aMediaSrc,
+ const char* aCallSite) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Make sure we only store once.
+ MOZ_ASSERT(mDiagnosticsType == eUnsaved);
+ mDiagnosticsType = eDecodeWarning;
+
+ if (NS_WARN_IF(!aDocument)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreDecodeWarning("
+ "Document* aDocument=nullptr, aWarning=%s,"
+ " aMediaSrc=<provided>, call site '%s')",
+ this, aWarning.Description().get(), aCallSite);
+ return;
+ }
+
+ RefPtr<DecoderDoctorDocumentWatcher> watcher =
+ DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument);
+
+ if (NS_WARN_IF(!watcher)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreDecodeWarning("
+ "Document* aDocument=%p, aWarning='%s', aMediaSrc=<provided>,"
+ " call site '%s') - Could not create document watcher",
+ this, aDocument, aWarning.Description().get(), aCallSite);
+ return;
+ }
+
+ mDecodeIssue = aWarning;
+ mDecodeIssueMediaSrc = aMediaSrc;
+
+ // StoreDecodeWarning should only be called once, after all data is
+ // available, so it is safe to std::move() from this object.
+ watcher->AddDiagnostics(std::move(*this), aCallSite);
+ // Even though it's moved-from, the type should stay set
+ // (Only used to ensure that we do store only once.)
+ MOZ_ASSERT(mDiagnosticsType == eDecodeWarning);
+}
+
+static const char* EventDomainString(DecoderDoctorEvent::Domain aDomain) {
+ switch (aDomain) {
+ case DecoderDoctorEvent::eAudioSinkStartup:
+ return "audio-sink-startup";
+ }
+ return "?";
+}
+
+nsCString DecoderDoctorDiagnostics::GetDescription() const {
+ nsCString s;
+ switch (mDiagnosticsType) {
+ case eUnsaved:
+ s = "Unsaved diagnostics, cannot get accurate description";
+ break;
+ case eFormatSupportCheck:
+ s = "format='";
+ s += NS_ConvertUTF16toUTF8(mFormat).get();
+ s += mFlags.contains(Flags::CanPlay) ? "', can play" : "', cannot play";
+ if (mFlags.contains(Flags::VideoNotSupported)) {
+ s += ", but video format not supported";
+ }
+ if (mFlags.contains(Flags::AudioNotSupported)) {
+ s += ", but audio format not supported";
+ }
+ if (mFlags.contains(Flags::WMFFailedToLoad)) {
+ s += ", Windows platform decoder failed to load";
+ }
+ if (mFlags.contains(Flags::FFmpegNotFound)) {
+ s += ", Linux platform decoder not found";
+ }
+ if (mFlags.contains(Flags::GMPPDMFailedToStartup)) {
+ s += ", GMP PDM failed to startup";
+ } else if (!mGMP.IsEmpty()) {
+ s += ", Using GMP '";
+ s += mGMP;
+ s += "'";
+ }
+ break;
+ case eMediaKeySystemAccessRequest:
+ s = "key system='";
+ s += NS_ConvertUTF16toUTF8(mKeySystem).get();
+ s += mIsKeySystemSupported ? "', supported" : "', not supported";
+ switch (mKeySystemIssue) {
+ case eUnset:
+ break;
+ case eWidevineWithNoWMF:
+ s += ", Widevine with no WMF";
+ break;
+ }
+ break;
+ case eEvent:
+ s = nsPrintfCString("event domain %s result=%" PRIu32,
+ EventDomainString(mEvent.mDomain),
+ static_cast<uint32_t>(mEvent.mResult));
+ break;
+ case eDecodeError:
+ s = "decode error: ";
+ s += mDecodeIssue.Description();
+ s += ", src='";
+ s += mDecodeIssueMediaSrc.IsEmpty() ? "<none>" : "<provided>";
+ s += "'";
+ break;
+ case eDecodeWarning:
+ s = "decode warning: ";
+ s += mDecodeIssue.Description();
+ s += ", src='";
+ s += mDecodeIssueMediaSrc.IsEmpty() ? "<none>" : "<provided>";
+ s += "'";
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected DiagnosticsType");
+ s = "?";
+ break;
+ }
+ return s;
+}
+
+static const char* ToDecoderDoctorReportTypeStr(
+ const dom::DecoderDoctorReportType& aType) {
+ switch (aType) {
+ case dom::DecoderDoctorReportType::Mediawidevinenowmf:
+ return sMediaWidevineNoWMF.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediawmfneeded:
+ return sMediaWMFNeeded.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediaplatformdecodernotfound:
+ return sMediaFFMpegNotFound.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediacannotplaynodecoders:
+ return sMediaCannotPlayNoDecoders.mReportStringId;
+ case dom::DecoderDoctorReportType::Medianodecoders:
+ return sMediaNoDecoders.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediacannotinitializepulseaudio:
+ return sCannotInitializePulseAudio.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediaunsupportedlibavcodec:
+ return sUnsupportedLibavcodec.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediadecodeerror:
+ return sMediaDecodeError.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediadecodewarning:
+ return sMediaDecodeWarning.mReportStringId;
+ default:
+ DD_DEBUG("Invalid report type to str");
+ return "invalid-report-type";
+ }
+}
+
+void DecoderDoctorDiagnostics::SetDecoderDoctorReportType(
+ const dom::DecoderDoctorReportType& aType) {
+ DD_INFO("Set report type %s", ToDecoderDoctorReportTypeStr(aType));
+ switch (aType) {
+ case dom::DecoderDoctorReportType::Mediawmfneeded:
+ SetWMFFailedToLoad();
+ return;
+ case dom::DecoderDoctorReportType::Mediaplatformdecodernotfound:
+ SetFFmpegNotFound();
+ return;
+ case dom::DecoderDoctorReportType::Mediaunsupportedlibavcodec:
+ SetLibAVCodecUnsupported();
+ return;
+ case dom::DecoderDoctorReportType::Mediacannotplaynodecoders:
+ case dom::DecoderDoctorReportType::Medianodecoders:
+ // Do nothing, because these type are related with can-play, which would
+ // be handled in `StoreFormatDiagnostics()` when sending `false` in the
+ // parameter for the canplay.
+ return;
+ default:
+ DD_DEBUG("Not supported type");
+ return;
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DecoderDoctorDiagnostics.h b/dom/media/doctor/DecoderDoctorDiagnostics.h
new file mode 100644
index 0000000000..dee63a6f1a
--- /dev/null
+++ b/dom/media/doctor/DecoderDoctorDiagnostics.h
@@ -0,0 +1,167 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DecoderDoctorDiagnostics_h_
+#define DecoderDoctorDiagnostics_h_
+
+#include "MediaResult.h"
+#include "mozilla/DefineEnum.h"
+#include "mozilla/EnumSet.h"
+#include "mozilla/EnumTypeTraits.h"
+#include "mozilla/dom/DecoderDoctorNotificationBinding.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+namespace dom {
+class Document;
+}
+
+struct DecoderDoctorEvent {
+ enum Domain {
+ eAudioSinkStartup,
+ } mDomain;
+ nsresult mResult;
+};
+
+// DecoderDoctorDiagnostics class, used to gather data from PDMs/DecoderTraits,
+// and then notify the user about issues preventing (or worsening) playback.
+//
+// The expected usage is:
+// 1. Instantiate a DecoderDoctorDiagnostics in a function (close to the point
+// where a webpage is trying to know whether some MIME types can be played,
+// or trying to play a media file).
+// 2. Pass a pointer to the DecoderDoctorDiagnostics structure to one of the
+// CanPlayStatus/IsTypeSupported/(others?). During that call, some PDMs may
+// add relevant diagnostic information.
+// 3. Analyze the collected diagnostics, and optionally dispatch an event to the
+// UX, to notify the user about potential playback issues and how to resolve
+// them.
+//
+// This class' methods must be called from the main thread.
+
+class DecoderDoctorDiagnostics {
+ friend struct IPC::ParamTraits<mozilla::DecoderDoctorDiagnostics>;
+
+ public:
+ // Store the diagnostic information collected so far on a document for a
+ // given format. All diagnostics for a document will be analyzed together
+ // within a short timeframe.
+ // Should only be called once.
+ void StoreFormatDiagnostics(dom::Document* aDocument,
+ const nsAString& aFormat, bool aCanPlay,
+ const char* aCallSite);
+
+ void StoreMediaKeySystemAccess(dom::Document* aDocument,
+ const nsAString& aKeySystem, bool aIsSupported,
+ const char* aCallSite);
+
+ void StoreEvent(dom::Document* aDocument, const DecoderDoctorEvent& aEvent,
+ const char* aCallSite);
+
+ void StoreDecodeError(dom::Document* aDocument, const MediaResult& aError,
+ const nsString& aMediaSrc, const char* aCallSite);
+
+ void StoreDecodeWarning(dom::Document* aDocument, const MediaResult& aWarning,
+ const nsString& aMediaSrc, const char* aCallSite);
+
+ enum DiagnosticsType {
+ eUnsaved,
+ eFormatSupportCheck,
+ eMediaKeySystemAccessRequest,
+ eEvent,
+ eDecodeError,
+ eDecodeWarning
+ };
+ DiagnosticsType Type() const { return mDiagnosticsType; }
+
+ // Description string, for logging purposes; only call on stored diags.
+ nsCString GetDescription() const;
+
+ // Methods to record diagnostic information:
+
+ MOZ_DEFINE_ENUM_CLASS_AT_CLASS_SCOPE(
+ Flags, (CanPlay, WMFFailedToLoad, FFmpegNotFound, LibAVCodecUnsupported,
+ GMPPDMFailedToStartup, VideoNotSupported, AudioNotSupported));
+ using FlagsSet = mozilla::EnumSet<Flags>;
+
+ const nsAString& Format() const { return mFormat; }
+ bool CanPlay() const { return mFlags.contains(Flags::CanPlay); }
+
+ void SetFailureFlags(const FlagsSet& aFlags) { mFlags = aFlags; }
+ void SetWMFFailedToLoad() { mFlags += Flags::WMFFailedToLoad; }
+ bool DidWMFFailToLoad() const {
+ return mFlags.contains(Flags::WMFFailedToLoad);
+ }
+
+ void SetFFmpegNotFound() { mFlags += Flags::FFmpegNotFound; }
+ bool DidFFmpegNotFound() const {
+ return mFlags.contains(Flags::FFmpegNotFound);
+ }
+
+ void SetLibAVCodecUnsupported() { mFlags += Flags::LibAVCodecUnsupported; }
+ bool IsLibAVCodecUnsupported() const {
+ return mFlags.contains(Flags::LibAVCodecUnsupported);
+ }
+
+ void SetGMPPDMFailedToStartup() { mFlags += Flags::GMPPDMFailedToStartup; }
+ bool DidGMPPDMFailToStartup() const {
+ return mFlags.contains(Flags::GMPPDMFailedToStartup);
+ }
+
+ void SetVideoNotSupported() { mFlags += Flags::VideoNotSupported; }
+ void SetAudioNotSupported() { mFlags += Flags::AudioNotSupported; }
+
+ void SetGMP(const nsACString& aGMP) { mGMP = aGMP; }
+ const nsACString& GMP() const { return mGMP; }
+
+ const nsAString& KeySystem() const { return mKeySystem; }
+ bool IsKeySystemSupported() const { return mIsKeySystemSupported; }
+ enum KeySystemIssue { eUnset, eWidevineWithNoWMF };
+ void SetKeySystemIssue(KeySystemIssue aKeySystemIssue) {
+ mKeySystemIssue = aKeySystemIssue;
+ }
+ KeySystemIssue GetKeySystemIssue() const { return mKeySystemIssue; }
+
+ DecoderDoctorEvent event() const { return mEvent; }
+
+ const MediaResult& DecodeIssue() const { return mDecodeIssue; }
+ const nsString& DecodeIssueMediaSrc() const { return mDecodeIssueMediaSrc; }
+
+ // This method is only used for testing.
+ void SetDecoderDoctorReportType(const dom::DecoderDoctorReportType& aType);
+
+ private:
+ // Currently-known type of diagnostics. Set from one of the 'Store...'
+ // methods. This helps ensure diagnostics are only stored once, and makes it
+ // easy to know what information they contain.
+ DiagnosticsType mDiagnosticsType = eUnsaved;
+
+ nsString mFormat;
+ FlagsSet mFlags;
+ nsCString mGMP;
+
+ nsString mKeySystem;
+ bool mIsKeySystemSupported = false;
+ KeySystemIssue mKeySystemIssue = eUnset;
+
+ DecoderDoctorEvent mEvent;
+
+ MediaResult mDecodeIssue = NS_OK;
+ nsString mDecodeIssueMediaSrc;
+};
+
+// Used for IPDL serialization.
+// The 'value' have to be the biggest enum from DecoderDoctorDiagnostics::Flags.
+template <>
+struct MaxEnumValue<::mozilla::DecoderDoctorDiagnostics::Flags> {
+ static constexpr unsigned int value =
+ static_cast<unsigned int>(DecoderDoctorDiagnostics::sFlagsCount);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/doctor/DecoderDoctorLogger.cpp b/dom/media/doctor/DecoderDoctorLogger.cpp
new file mode 100644
index 0000000000..927650babc
--- /dev/null
+++ b/dom/media/doctor/DecoderDoctorLogger.cpp
@@ -0,0 +1,176 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "DecoderDoctorLogger.h"
+
+#include "DDLogUtils.h"
+#include "DDMediaLogs.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/Unused.h"
+
+namespace mozilla {
+
+/* static */ Atomic<DecoderDoctorLogger::LogState, ReleaseAcquire>
+ DecoderDoctorLogger::sLogState{DecoderDoctorLogger::scDisabled};
+
+/* static */ const char* DecoderDoctorLogger::sShutdownReason = nullptr;
+
+static DDMediaLogs* sMediaLogs;
+
+/* static */
+void DecoderDoctorLogger::Init() {
+ MOZ_ASSERT(static_cast<LogState>(sLogState) == scDisabled);
+ if (MOZ_LOG_TEST(sDecoderDoctorLoggerLog, LogLevel::Error) ||
+ MOZ_LOG_TEST(sDecoderDoctorLoggerEndLog, LogLevel::Error)) {
+ EnableLogging();
+ }
+}
+
+// First DDLogShutdowner sets sLogState to scShutdown, to prevent further
+// logging.
+struct DDLogShutdowner {
+ ~DDLogShutdowner() {
+ DDL_INFO("Shutting down");
+ // Prevent further logging, some may racily seep in, it's fine as the
+ // logging infrastructure would still be alive until DDLogDeleter runs.
+ DecoderDoctorLogger::ShutdownLogging();
+ }
+};
+static UniquePtr<DDLogShutdowner> sDDLogShutdowner;
+
+// Later DDLogDeleter will delete the message queue and media logs.
+struct DDLogDeleter {
+ ~DDLogDeleter() {
+ if (sMediaLogs) {
+ DDL_INFO("Final processing of collected logs");
+ delete sMediaLogs;
+ sMediaLogs = nullptr;
+ }
+ }
+};
+static UniquePtr<DDLogDeleter> sDDLogDeleter;
+
+/* static */
+void DecoderDoctorLogger::PanicInternal(const char* aReason, bool aDontBlock) {
+ for (;;) {
+ const LogState state = static_cast<LogState>(sLogState);
+ if (state == scEnabling && !aDontBlock) {
+ // Wait for the end of the enabling process (unless we're in it, in which
+ // case we don't want to block.)
+ continue;
+ }
+ if (state == scShutdown) {
+ // Already shutdown, nothing more to do.
+ break;
+ }
+ if (sLogState.compareExchange(state, scShutdown)) {
+ // We are the one performing the first shutdown -> Record reason.
+ sShutdownReason = aReason;
+ // Free as much memory as possible.
+ if (sMediaLogs) {
+ // Shutdown the medialogs processing thread, and free as much memory
+ // as possible.
+ sMediaLogs->Panic();
+ }
+ // sMediaLogs and sQueue will be deleted by DDLogDeleter.
+ // We don't want to delete them right now, because there could be a race
+ // where another thread started logging or retrieving logs before we
+ // changed the state to scShutdown, but has been delayed before actually
+ // trying to write or read log messages, thereby causing a UAF.
+ }
+ // If someone else changed the state, we'll just loop around, and either
+ // shutdown already happened elsewhere, or we'll try to shutdown again.
+ }
+}
+
+/* static */
+bool DecoderDoctorLogger::EnsureLogIsEnabled() {
+#ifdef RELEASE_OR_BETA
+ // Just refuse to enable DDLogger on release&beta because it makes it too easy
+ // to trigger an OOM. See bug 1571648.
+ return false;
+#else
+ for (;;) {
+ LogState state = static_cast<LogState>(sLogState);
+ switch (state) {
+ case scDisabled:
+ // Currently disabled, try to be the one to enable.
+ if (sLogState.compareExchange(scDisabled, scEnabling)) {
+ // We are the one to enable logging, state won't change (except for
+ // possible shutdown.)
+ // Create DDMediaLogs singleton, which will process the message queue.
+ DDMediaLogs::ConstructionResult mediaLogsConstruction =
+ DDMediaLogs::New();
+ if (NS_FAILED(mediaLogsConstruction.mRv)) {
+ PanicInternal("Failed to enable logging", /* aDontBlock */ true);
+ return false;
+ }
+ MOZ_ASSERT(mediaLogsConstruction.mMediaLogs);
+ sMediaLogs = mediaLogsConstruction.mMediaLogs;
+ // Setup shutdown-time clean-up.
+ MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(
+ TaskCategory::Other,
+ NS_NewRunnableFunction("DDLogger shutdown setup", [] {
+ sDDLogShutdowner = MakeUnique<DDLogShutdowner>();
+ ClearOnShutdown(&sDDLogShutdowner,
+ ShutdownPhase::XPCOMShutdown);
+ sDDLogDeleter = MakeUnique<DDLogDeleter>();
+ ClearOnShutdown(&sDDLogDeleter,
+ ShutdownPhase::XPCOMShutdownThreads);
+ })));
+
+ // Nobody else should change the state when *we* are enabling logging.
+ MOZ_ASSERT(sLogState == scEnabling);
+ sLogState = scEnabled;
+ DDL_INFO("Logging enabled");
+ return true;
+ }
+ // Someone else changed the state before our compareExchange, just loop
+ // around to examine the new situation.
+ break;
+ case scEnabled:
+ return true;
+ case scEnabling:
+ // Someone else is currently enabling logging, actively wait by just
+ // looping, until the state changes.
+ break;
+ case scShutdown:
+ // Shutdown is non-recoverable, we cannot enable logging again.
+ return false;
+ }
+ // Not returned yet, loop around to examine the new situation.
+ }
+#endif
+}
+
+/* static */
+void DecoderDoctorLogger::EnableLogging() { Unused << EnsureLogIsEnabled(); }
+
+/* static */ RefPtr<DecoderDoctorLogger::LogMessagesPromise>
+DecoderDoctorLogger::RetrieveMessages(
+ const dom::HTMLMediaElement* aMediaElement) {
+ if (MOZ_UNLIKELY(!EnsureLogIsEnabled())) {
+ DDL_WARN("Request (for %p) but there are no logs", aMediaElement);
+ return DecoderDoctorLogger::LogMessagesPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_ABORT_ERR, __func__);
+ }
+ return sMediaLogs->RetrieveMessages(aMediaElement);
+}
+
+/* static */
+void DecoderDoctorLogger::Log(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ DDLogValue&& aValue) {
+ if (IsDDLoggingEnabled()) {
+ MOZ_ASSERT(sMediaLogs);
+ sMediaLogs->Log(aSubjectTypeName, aSubjectPointer, aCategory, aLabel,
+ std::move(aValue));
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DecoderDoctorLogger.h b/dom/media/doctor/DecoderDoctorLogger.h
new file mode 100644
index 0000000000..88a8c0c87f
--- /dev/null
+++ b/dom/media/doctor/DecoderDoctorLogger.h
@@ -0,0 +1,472 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef DecoderDoctorLogger_h_
+#define DecoderDoctorLogger_h_
+
+#include "DDLoggedTypeTraits.h"
+#include "DDLogCategory.h"
+#include "DDLogValue.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/DefineEnum.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/NonDereferenceable.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+// Main class used to capture log messages from the media stack, and to
+// retrieve processed messages associated with an HTMLMediaElement.
+//
+// The logging APIs are designed to work as fast as possible (in most cases
+// only checking a couple of atomic variables, and not allocating memory), so
+// as not to introduce perceptible latency.
+// Consider using DDLOG...() macros, and IsDDLoggingEnabled(), to avoid any
+// unneeded work when logging is not enabled.
+//
+// Structural logging messages are used to determine when objects are created
+// and destroyed, and to link objects that depend on each other, ultimately
+// tying groups of objects and their messages to HTMLMediaElement objects.
+//
+// A separate thread processes log messages, and can asynchronously retrieve
+// processed messages that correspond to a given HTMLMediaElement.
+// That thread is also responsible for removing dated messages, so as not to
+// take too much memory.
+class DecoderDoctorLogger {
+ public:
+ // Called by nsLayoutStatics::Initialize() before any other media work.
+ // Pre-enables logging if MOZ_LOG requires DDLogger.
+ static void Init();
+
+ // Is logging currently enabled? This is tested anyway in all public `Log...`
+ // functions, but it may be used to prevent logging-only work in clients.
+ static inline bool IsDDLoggingEnabled() {
+ return MOZ_UNLIKELY(static_cast<LogState>(sLogState) == scEnabled);
+ }
+
+ // Shutdown logging. This will prevent more messages to be queued, but the
+ // already-queued messages may still get processed.
+ static void ShutdownLogging() { sLogState = scShutdown; }
+
+ // Something went horribly wrong, stop all logging and log processing.
+ static void Panic(const char* aReason) {
+ PanicInternal(aReason, /* aDontBlock */ false);
+ }
+
+ // Logging functions.
+ //
+ // All logging functions take:
+ // - The object that produces the message, either as a template type (for
+ // which a specialized DDLoggedTypeTraits exists), or a pointer and a type
+ // name (needed for inner classes that cannot specialize
+ // DDLoggedTypeTraits.)
+ // - A DDLogCategory defining the type of log message; some are used
+ // internally for capture the lifetime and linking of C++ objects, others
+ // are used to split messages into different domains.
+ // - A label (string literal).
+ // - An optional Variant value, see DDLogValue for the accepted types.
+ //
+ // The following `EagerLog...` functions always cause their arguments to be
+ // pre-evaluated even if logging is disabled, in which case runtime could be
+ // wasted. Consider using `DDLOG...` macros instead, or test
+ // `IsDDLoggingEnabled()` first.
+
+ template <typename Value>
+ static void EagerLogValue(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ Value&& aValue) {
+ Log(aSubjectTypeName, aSubjectPointer, aCategory, aLabel,
+ DDLogValue{std::forward<Value>(aValue)});
+ }
+
+ template <typename Subject, typename Value>
+ static void EagerLogValue(const Subject* aSubject, DDLogCategory aCategory,
+ const char* aLabel, Value&& aValue) {
+ EagerLogValue(DDLoggedTypeTraits<Subject>::Name(), aSubject, aCategory,
+ aLabel, std::forward<Value>(aValue));
+ }
+
+ // EagerLogValue that can explicitly take strings, as the templated function
+ // above confuses Variant when forwarding string literals.
+ static void EagerLogValue(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ const char* aValue) {
+ Log(aSubjectTypeName, aSubjectPointer, aCategory, aLabel,
+ DDLogValue{aValue});
+ }
+
+ template <typename Subject>
+ static void EagerLogValue(const Subject* aSubject, DDLogCategory aCategory,
+ const char* aLabel, const char* aValue) {
+ EagerLogValue(DDLoggedTypeTraits<Subject>::Name(), aSubject, aCategory,
+ aLabel, aValue);
+ }
+
+ static void EagerLogPrintf(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ const char* aString) {
+ Log(aSubjectTypeName, aSubjectPointer, aCategory, aLabel,
+ DDLogValue{nsCString{aString}});
+ }
+
+ template <typename... Args>
+ static void EagerLogPrintf(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ const char* aFormat, Args&&... aArgs) {
+ Log(aSubjectTypeName, aSubjectPointer, aCategory, aLabel,
+ DDLogValue{
+ nsCString{nsPrintfCString(aFormat, std::forward<Args>(aArgs)...)}});
+ }
+
+ template <typename Subject>
+ static void EagerLogPrintf(const Subject* aSubject, DDLogCategory aCategory,
+ const char* aLabel, const char* aString) {
+ EagerLogPrintf(DDLoggedTypeTraits<Subject>::Name(), aSubject, aCategory,
+ aLabel, aString);
+ }
+
+ template <typename Subject, typename... Args>
+ static void EagerLogPrintf(const Subject* aSubject, DDLogCategory aCategory,
+ const char* aLabel, const char* aFormat,
+ Args&&... aArgs) {
+ EagerLogPrintf(DDLoggedTypeTraits<Subject>::Name(), aSubject, aCategory,
+ aLabel, aFormat, std::forward<Args>(aArgs)...);
+ }
+
+ static void MozLogPrintf(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ const LogModule* aLogModule, LogLevel aLogLevel,
+ const char* aString) {
+ Log(aSubjectTypeName, aSubjectPointer, CategoryForMozLogLevel(aLogLevel),
+ aLogModule->Name(), // LogModule name as label.
+ DDLogValue{nsCString{aString}});
+ MOZ_LOG(aLogModule, aLogLevel,
+ ("%s[%p] %s", aSubjectTypeName, aSubjectPointer, aString));
+ }
+
+ template <typename... Args>
+ static void MozLogPrintf(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ const LogModule* aLogModule, LogLevel aLogLevel,
+ const char* aFormat, Args&&... aArgs) {
+ nsCString printed = nsPrintfCString(aFormat, std::forward<Args>(aArgs)...);
+ Log(aSubjectTypeName, aSubjectPointer, CategoryForMozLogLevel(aLogLevel),
+ aLogModule->Name(), // LogModule name as label.
+ DDLogValue{printed});
+ MOZ_LOG(aLogModule, aLogLevel,
+ ("%s[%p] %s", aSubjectTypeName, aSubjectPointer, printed.get()));
+ }
+
+ template <typename Subject>
+ static void MozLogPrintf(const Subject* aSubject, const LogModule* aLogModule,
+ LogLevel aLogLevel, const char* aString) {
+ MozLogPrintf(DDLoggedTypeTraits<Subject>::Name(), aSubject, aLogModule,
+ aLogLevel, aString);
+ }
+
+ template <typename Subject, typename... Args>
+ static void MozLogPrintf(const Subject* aSubject, const LogModule* aLogModule,
+ LogLevel aLogLevel, const char* aFormat,
+ Args&&... aArgs) {
+ MozLogPrintf(DDLoggedTypeTraits<Subject>::Name(), aSubject, aLogModule,
+ aLogLevel, aFormat, std::forward<Args>(aArgs)...);
+ }
+
+ // Special logging functions. Consider using DecoderDoctorLifeLogger to
+ // automatically capture constructions & destructions.
+
+ static void LogConstruction(const char* aSubjectTypeName,
+ const void* aSubjectPointer) {
+ Log(aSubjectTypeName, aSubjectPointer, DDLogCategory::_Construction, "",
+ DDLogValue{DDNoValue{}});
+ }
+
+ static void LogConstructionAndBase(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ const char* aBaseTypeName,
+ const void* aBasePointer) {
+ Log(aSubjectTypeName, aSubjectPointer, DDLogCategory::_DerivedConstruction,
+ "", DDLogValue{DDLogObject{aBaseTypeName, aBasePointer}});
+ }
+
+ template <typename B>
+ static void LogConstructionAndBase(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ const B* aBase) {
+ Log(aSubjectTypeName, aSubjectPointer, DDLogCategory::_DerivedConstruction,
+ "", DDLogValue{DDLogObject{DDLoggedTypeTraits<B>::Name(), aBase}});
+ }
+
+ template <typename Subject>
+ static void LogConstruction(NonDereferenceable<const Subject> aSubject) {
+ using Traits = DDLoggedTypeTraits<Subject>;
+ if (!Traits::HasBase::value) {
+ Log(DDLoggedTypeTraits<Subject>::Name(),
+ reinterpret_cast<const void*>(aSubject.value()),
+ DDLogCategory::_Construction, "", DDLogValue{DDNoValue{}});
+ } else {
+ Log(DDLoggedTypeTraits<Subject>::Name(),
+ reinterpret_cast<const void*>(aSubject.value()),
+ DDLogCategory::_DerivedConstruction, "",
+ DDLogValue{DDLogObject{
+ DDLoggedTypeTraits<typename Traits::BaseType>::Name(),
+ reinterpret_cast<const void*>(
+ NonDereferenceable<const typename Traits::BaseType>(aSubject)
+ .value())}});
+ }
+ }
+
+ template <typename Subject>
+ static void LogConstruction(const Subject* aSubject) {
+ LogConstruction(NonDereferenceable<const Subject>(aSubject));
+ }
+
+ static void LogDestruction(const char* aSubjectTypeName,
+ const void* aSubjectPointer) {
+ Log(aSubjectTypeName, aSubjectPointer, DDLogCategory::_Destruction, "",
+ DDLogValue{DDNoValue{}});
+ }
+
+ template <typename Subject>
+ static void LogDestruction(NonDereferenceable<const Subject> aSubject) {
+ Log(DDLoggedTypeTraits<Subject>::Name(),
+ reinterpret_cast<const void*>(aSubject.value()),
+ DDLogCategory::_Destruction, "", DDLogValue{DDNoValue{}});
+ }
+
+ template <typename Subject>
+ static void LogDestruction(const Subject* aSubject) {
+ LogDestruction(NonDereferenceable<const Subject>(aSubject));
+ }
+
+ template <typename P, typename C>
+ static void LinkParentAndChild(const P* aParent, const char* aLinkName,
+ const C* aChild) {
+ if (aChild) {
+ Log(DDLoggedTypeTraits<P>::Name(), aParent, DDLogCategory::_Link,
+ aLinkName,
+ DDLogValue{DDLogObject{DDLoggedTypeTraits<C>::Name(), aChild}});
+ }
+ }
+
+ template <typename C>
+ static void LinkParentAndChild(const char* aParentTypeName,
+ const void* aParentPointer,
+ const char* aLinkName, const C* aChild) {
+ if (aChild) {
+ Log(aParentTypeName, aParentPointer, DDLogCategory::_Link, aLinkName,
+ DDLogValue{DDLogObject{DDLoggedTypeTraits<C>::Name(), aChild}});
+ }
+ }
+
+ template <typename P>
+ static void LinkParentAndChild(const P* aParent, const char* aLinkName,
+ const char* aChildTypeName,
+ const void* aChildPointer) {
+ if (aChildPointer) {
+ Log(DDLoggedTypeTraits<P>::Name(), aParent, DDLogCategory::_Link,
+ aLinkName, DDLogValue{DDLogObject{aChildTypeName, aChildPointer}});
+ }
+ }
+
+ template <typename C>
+ static void UnlinkParentAndChild(const char* aParentTypeName,
+ const void* aParentPointer,
+ const C* aChild) {
+ if (aChild) {
+ Log(aParentTypeName, aParentPointer, DDLogCategory::_Unlink, "",
+ DDLogValue{DDLogObject{DDLoggedTypeTraits<C>::Name(), aChild}});
+ }
+ }
+
+ template <typename P, typename C>
+ static void UnlinkParentAndChild(const P* aParent, const C* aChild) {
+ if (aChild) {
+ Log(DDLoggedTypeTraits<P>::Name(), aParent, DDLogCategory::_Unlink, "",
+ DDLogValue{DDLogObject{DDLoggedTypeTraits<C>::Name(), aChild}});
+ }
+ }
+
+ // Retrieval functions.
+
+ // Enable logging, if not done already. No effect otherwise.
+ static void EnableLogging();
+
+ using LogMessagesPromise =
+ MozPromise<nsCString, nsresult, /* IsExclusive = */ true>;
+
+ // Retrieve all messages related to a given HTMLMediaElement object.
+ // This call will trigger a processing run (to ensure the most recent data
+ // will be returned), and the returned promise will be resolved with all
+ // relevant log messages and object lifetimes in a JSON string.
+ // The first call will enable logging, until shutdown.
+ static RefPtr<LogMessagesPromise> RetrieveMessages(
+ const dom::HTMLMediaElement* aMediaElement);
+
+ private:
+ // If logging is not enabled yet, initiate it, return true.
+ // If logging has been shutdown, don't start it, return false.
+ // Otherwise return true.
+ static bool EnsureLogIsEnabled();
+
+ // Note that this call may block while the state is scEnabling;
+ // set aDontBlock to true to avoid blocking, most importantly when the
+ // caller is the one doing the enabling, this would cause an endless loop.
+ static void PanicInternal(const char* aReason, bool aDontBlock);
+
+ static void Log(const char* aSubjectTypeName, const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ DDLogValue&& aValue);
+
+ static void Log(const char* aSubjectTypeName, const void* aSubjectPointer,
+ const LogModule* aLogModule, LogLevel aLogLevel,
+ DDLogValue&& aValue);
+
+ static DDLogCategory CategoryForMozLogLevel(LogLevel aLevel) {
+ switch (aLevel) {
+ default:
+ case LogLevel::Error:
+ return DDLogCategory::MozLogError;
+ case LogLevel::Warning:
+ return DDLogCategory::MozLogWarning;
+ case LogLevel::Info:
+ return DDLogCategory::MozLogInfo;
+ case LogLevel::Debug:
+ return DDLogCategory::MozLogDebug;
+ case LogLevel::Verbose:
+ return DDLogCategory::MozLogVerbose;
+ }
+ }
+
+ using LogState = int;
+ // Currently disabled, may be enabled on request.
+ static constexpr LogState scDisabled = 0;
+ // Currently enabled (logging happens), may be shutdown.
+ static constexpr LogState scEnabled = 1;
+ // Still disabled, but one thread is working on enabling it, nobody else
+ // should interfere during this time.
+ static constexpr LogState scEnabling = 2;
+ // Shutdown, cannot be re-enabled.
+ static constexpr LogState scShutdown = 3;
+ // Current state.
+ // "ReleaseAcquire" because when changing to scEnabled, the just-created
+ // sMediaLogs must be accessible to consumers that see scEnabled.
+ static Atomic<LogState, ReleaseAcquire> sLogState;
+
+ // If non-null, reason for an abnormal shutdown.
+ static const char* sShutdownReason;
+};
+
+// Base class to automatically record a class lifetime. Usage:
+// class SomeClass : public DecoderDoctorLifeLogger<SomeClass>
+// {
+// ...
+template <typename T>
+class DecoderDoctorLifeLogger {
+ protected:
+ DecoderDoctorLifeLogger() {
+ DecoderDoctorLogger::LogConstruction(NonDereferenceable<const T>(this));
+ }
+ ~DecoderDoctorLifeLogger() {
+ DecoderDoctorLogger::LogDestruction(NonDereferenceable<const T>(this));
+ }
+};
+
+// Macros to help lazily-evaluate arguments, only after we have checked that
+// logging is enabled.
+
+// Log a single value; see DDLogValue for allowed types.
+#define DDLOG(_category, _label, _arg) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DecoderDoctorLogger::EagerLogValue(this, _category, _label, _arg); \
+ } \
+ } while (0)
+// Log a single value, with an EXplicit `this`.
+#define DDLOGEX(_this, _category, _label, _arg) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DecoderDoctorLogger::EagerLogValue(_this, _category, _label, _arg); \
+ } \
+ } while (0)
+// Log a single value, with EXplicit type name and `this`.
+#define DDLOGEX2(_typename, _this, _category, _label, _arg) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DecoderDoctorLogger::EagerLogValue(_typename, _this, _category, _label, \
+ _arg); \
+ } \
+ } while (0)
+
+#ifdef DEBUG
+// Do a printf format check in DEBUG, with the downside that side-effects (from
+// evaluating the arguments) may happen twice! Who would do that anyway?
+static void inline MOZ_FORMAT_PRINTF(1, 2) DDLOGPRCheck(const char*, ...) {}
+# define DDLOGPR_CHECK(_fmt, ...) DDLOGPRCheck(_fmt, ##__VA_ARGS__)
+#else
+# define DDLOGPR_CHECK(_fmt, ...)
+#endif
+
+// Log a printf'd string. Discouraged, please try using DDLOG instead.
+#define DDLOGPR(_category, _label, _format, ...) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DDLOGPR_CHECK(_format, ##__VA_ARGS__); \
+ DecoderDoctorLogger::EagerLogPrintf(this, _category, _label, _format, \
+ ##__VA_ARGS__); \
+ } \
+ } while (0)
+
+// Link a child object.
+#define DDLINKCHILD(...) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DecoderDoctorLogger::LinkParentAndChild(this, __VA_ARGS__); \
+ } \
+ } while (0)
+
+// Unlink a child object.
+#define DDUNLINKCHILD(...) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DecoderDoctorLogger::UnlinkParentAndChild(this, __VA_ARGS__); \
+ } \
+ } while (0)
+
+// Log a printf'd string to DDLogger and/or MOZ_LOG, with an EXplicit `this`.
+// Don't even call MOZ_LOG on Android non-release/beta; See Logging.h.
+#if !defined(ANDROID) || !defined(RELEASE_OR_BETA)
+# define DDMOZ_LOGEX(_this, _logModule, _logLevel, _format, ...) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled() || \
+ MOZ_LOG_TEST(_logModule, _logLevel)) { \
+ DDLOGPR_CHECK(_format, ##__VA_ARGS__); \
+ DecoderDoctorLogger::MozLogPrintf(_this, _logModule, _logLevel, \
+ _format, ##__VA_ARGS__); \
+ } \
+ } while (0)
+#else
+# define DDMOZ_LOGEX(_this, _logModule, _logLevel, _format, ...) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DDLOGPR_CHECK(_format, ##__VA_ARGS__); \
+ DecoderDoctorLogger::MozLogPrintf(_this, _logModule, _logLevel, \
+ _format, ##__VA_ARGS__); \
+ } \
+ } while (0)
+#endif
+
+// Log a printf'd string to DDLogger and/or MOZ_LOG.
+#define DDMOZ_LOG(_logModule, _logLevel, _format, ...) \
+ DDMOZ_LOGEX(this, _logModule, _logLevel, _format, ##__VA_ARGS__)
+
+} // namespace mozilla
+
+#endif // DecoderDoctorLogger_h_
diff --git a/dom/media/doctor/MultiWriterQueue.h b/dom/media/doctor/MultiWriterQueue.h
new file mode 100644
index 0000000000..b19c0039ba
--- /dev/null
+++ b/dom/media/doctor/MultiWriterQueue.h
@@ -0,0 +1,523 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_MultiWriterQueue_h_
+#define mozilla_MultiWriterQueue_h_
+
+#include <cstdint>
+#include <utility>
+
+#include "RollingNumber.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Mutex.h"
+#include "prthread.h"
+
+namespace mozilla {
+
+// Default reader locking strategy, using a mutex to ensure that concurrent
+// PopAll calls won't overlap.
+class MOZ_CAPABILITY("mutex") MultiWriterQueueReaderLocking_Mutex {
+ public:
+ MultiWriterQueueReaderLocking_Mutex()
+ : mMutex("MultiWriterQueueReaderLocking_Mutex") {}
+ void Lock() MOZ_CAPABILITY_ACQUIRE(mMutex) { mMutex.Lock(); };
+ void Unlock() MOZ_CAPABILITY_RELEASE(mMutex) { mMutex.Unlock(); };
+
+ private:
+ Mutex mMutex;
+};
+
+// Reader non-locking strategy, trusting that PopAll will never be called
+// concurrently (e.g., by only calling it from a specific thread).
+class MOZ_CAPABILITY("dummy lock") MultiWriterQueueReaderLocking_None {
+ public:
+#ifndef DEBUG
+ void Lock() MOZ_CAPABILITY_ACQUIRE(){};
+ void Unlock() MOZ_CAPABILITY_RELEASE(){};
+#else
+ // DEBUG-mode checks to catch concurrent misuses.
+ void Lock() MOZ_CAPABILITY_ACQUIRE() {
+ MOZ_ASSERT(mLocked.compareExchange(false, true));
+ };
+ void Unlock() MOZ_CAPABILITY_RELEASE() {
+ MOZ_ASSERT(mLocked.compareExchange(true, false));
+ };
+
+ private:
+ Atomic<bool> mLocked{false};
+#endif
+};
+
+static constexpr uint32_t MultiWriterQueueDefaultBufferSize = 8192;
+
+// Multi-writer, single-reader queue of elements of type `T`.
+// Elements are bunched together in buffers of `BufferSize` elements.
+//
+// This queue is heavily optimized for pushing. In most cases pushes will only
+// cost a couple of atomic reads and a few non-atomic reads. Worst cases:
+// - Once per buffer, a push will allocate or reuse a buffer for later pushes;
+// - During the above new-buffer push, other pushes will be blocked.
+//
+// By default, popping is protected by mutex; it may be disabled if popping is
+// guaranteed never to be concurrent.
+// In any case, popping will never negatively impact pushes.
+// (However, *not* popping will add runtime costs, as unread buffers will not
+// be freed, or made available to future pushes; Push functions provide
+// feedback as to when popping would be most efficient.)
+template <typename T, uint32_t BufferSize = MultiWriterQueueDefaultBufferSize,
+ typename ReaderLocking = MultiWriterQueueReaderLocking_Mutex>
+class MultiWriterQueue {
+ static_assert(BufferSize > 0, "0-sized MultiWriterQueue buffer");
+
+ public:
+ // Constructor.
+ // Allocates the initial buffer that will receive the first `BufferSize`
+ // elements. Also allocates one reusable buffer, which will definitely be
+ // needed after the first `BufferSize` elements have been pushed.
+ // Ideally (if the reader can process each buffer quickly enough), there
+ // won't be a need for more buffer allocations.
+ MultiWriterQueue()
+ : mBuffersCoverAtLeastUpTo(BufferSize - 1),
+ mMostRecentBuffer(new Buffer{}),
+ mReusableBuffers(new Buffer{}),
+ mOldestBuffer(static_cast<Buffer*>(mMostRecentBuffer)),
+ mLiveBuffersStats(1),
+ mReusableBuffersStats(1),
+ mAllocatedBuffersStats(2) {}
+
+ ~MultiWriterQueue() {
+ auto DestroyList = [](Buffer* aBuffer) {
+ while (aBuffer) {
+ Buffer* older = aBuffer->Older();
+ delete aBuffer;
+ aBuffer = older;
+ }
+ };
+ DestroyList(mMostRecentBuffer);
+ DestroyList(mReusableBuffers);
+ }
+
+ // We need the index to be order-resistant to overflow, i.e., numbers before
+ // an overflow should test smaller-than numbers after the overflow.
+ // This is because we keep pushing elements with increasing Index, and this
+ // Index is used to find the appropriate buffer based on a range; and this
+ // need to work smoothly when crossing the overflow boundary.
+ using Index = RollingNumber<uint32_t>;
+
+ // Pushes indicate whether they have just reached the end of a buffer.
+ using DidReachEndOfBuffer = bool;
+
+ // Push new element and call aF on it.
+ // Element may be in just-created state, or recycled after a PopAll call.
+ // Atomically thread-safe; in the worst case some pushes may be blocked
+ // while a new buffer is created/reused for them.
+ // Returns whether that push reached the end of a buffer; useful if caller
+ // wants to trigger processing regularly at the most efficient time.
+ template <typename F>
+ DidReachEndOfBuffer PushF(F&& aF) {
+ // Atomically claim ownership of the next available element.
+ const Index index{mNextElementToWrite++};
+ // And now go and set that element.
+ for (;;) {
+ Index lastIndex{mBuffersCoverAtLeastUpTo};
+
+ if (MOZ_UNLIKELY(index == lastIndex)) {
+ // We have claimed the last element in the current head -> Allocate a
+ // new head in advance of more pushes. Make it point at the current
+ // most-recent buffer.
+ // This whole process is effectively guarded:
+ // - Later pushes will wait until mBuffersCoverAtLeastUpTo changes to
+ // one that can accept their claimed index.
+ // - Readers will stop until the last element is marked as valid.
+ Buffer* ourBuffer = mMostRecentBuffer;
+ Buffer* newBuffer = NewBuffer(ourBuffer, index + 1);
+ // Because we have claimed this very specific index, we should be the
+ // only one touching the most-recent buffer pointer.
+ MOZ_ASSERT(mMostRecentBuffer == ourBuffer);
+ // Just pivot the most-recent buffer pointer to our new buffer.
+ mMostRecentBuffer = newBuffer;
+ // Because we have claimed this very specific index, we should be the
+ // only one touching the buffer coverage watermark.
+ MOZ_ASSERT(mBuffersCoverAtLeastUpTo == lastIndex.Value());
+ // Update it to include the just-added most-recent buffer.
+ mBuffersCoverAtLeastUpTo = index.Value() + BufferSize;
+ // We know for sure that `ourBuffer` is the correct one for this index.
+ ourBuffer->SetAndValidateElement(aF, index);
+ // And indicate that we have reached the end of a buffer.
+ return true;
+ }
+
+ if (MOZ_UNLIKELY(index > lastIndex)) {
+ // We have claimed an element in a yet-unavailable buffer, wait for our
+ // target buffer to be created (see above).
+ while (Index(mBuffersCoverAtLeastUpTo) < index) {
+ PR_Sleep(PR_INTERVAL_NO_WAIT); // Yield
+ }
+ // Then loop to examine the new situation.
+ continue;
+ }
+
+ // Here, we have claimed a number that is covered by current buffers.
+ // These buffers cannot be destroyed, because our buffer is not filled
+ // yet (we haven't written in it yet), therefore the reader thread will
+ // have to stop there (or before) and won't destroy our buffer or more
+ // recent ones.
+ MOZ_ASSERT(index < lastIndex);
+ Buffer* ourBuffer = mMostRecentBuffer;
+
+ // In rare situations, another thread may have had the time to create a
+ // new more-recent buffer, in which case we need to find our older buffer.
+ while (MOZ_UNLIKELY(index < ourBuffer->Origin())) {
+ // We assume that older buffers with still-invalid elements (e.g., the
+ // one we have just claimed) cannot be destroyed.
+ MOZ_ASSERT(ourBuffer->Older());
+ ourBuffer = ourBuffer->Older();
+ }
+
+ // Now we can set&validate the claimed element, and indicate that we have
+ // not reached the end of a buffer.
+ ourBuffer->SetAndValidateElement(aF, index);
+ return false;
+ }
+ }
+
+ // Push new element and assign it a value.
+ // Atomically thread-safe; in the worst case some pushes may be blocked
+ // while a new buffer is created/reused for them.
+ // Returns whether that push reached the end of a buffer; useful if caller
+ // wants to trigger processing regularly at the most efficient time.
+ DidReachEndOfBuffer Push(const T& aT) {
+ return PushF([&aT](T& aElement, Index) { aElement = aT; });
+ }
+
+ // Push new element and move-assign it a value.
+ // Atomically thread-safe; in the worst case some pushes may be blocked
+ // while a new buffer is created/reused for them.
+ // Returns whether that push reached the end of a buffer; useful if caller
+ // wants to trigger processing regularly at the most efficient time.
+ DidReachEndOfBuffer Push(T&& aT) {
+ return PushF([&aT](T& aElement, Index) { aElement = std::move(aT); });
+ }
+
+ // Pop all elements before the first invalid one, running aF on each of them
+ // in FIFO order.
+ // Thread-safety with other PopAll calls is controlled by the `Locking`
+ // template argument.
+ // Concurrent pushes are always allowed, because:
+ // - PopAll won't read elements until valid,
+ // - Pushes do not interfere with pop-related members -- except for
+ // mReusableBuffers, which is accessed atomically.
+ template <typename F>
+ void PopAll(F&& aF) {
+ mReaderLocking.Lock();
+ // Destroy every second fully-read buffer.
+ // TODO: Research a better algorithm, probably based on stats.
+ bool destroy = false;
+ for (;;) {
+ Buffer* b = mOldestBuffer;
+ MOZ_ASSERT(!b->Older());
+ // The next element to pop must be in that oldest buffer.
+ MOZ_ASSERT(mNextElementToPop >= b->Origin());
+ MOZ_ASSERT(mNextElementToPop < b->Origin() + BufferSize);
+
+ // Start reading each element.
+ if (!b->ReadAndInvalidateAll(aF, mNextElementToPop)) {
+ // Found an invalid element, stop popping.
+ mReaderLocking.Unlock();
+ return;
+ }
+
+ // Reached the end of this oldest buffer
+ MOZ_ASSERT(mNextElementToPop == b->Origin() + BufferSize);
+ // Delete this oldest buffer.
+ // Since the last element was valid, it must mean that there is a newer
+ // buffer.
+ MOZ_ASSERT(b->Newer());
+ MOZ_ASSERT(mNextElementToPop == b->Newer()->Origin());
+ StopUsing(b, destroy);
+ destroy = !destroy;
+
+ // We will loop and start reading the now-oldest buffer.
+ }
+ }
+
+ // Size of all buffers (used, or recyclable), excluding external data.
+ size_t ShallowSizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ return mAllocatedBuffersStats.Count() * sizeof(Buffer);
+ }
+
+ struct CountAndWatermark {
+ int mCount;
+ int mWatermark;
+ };
+
+ CountAndWatermark LiveBuffersStats() const { return mLiveBuffersStats.Get(); }
+ CountAndWatermark ReusableBuffersStats() const {
+ return mReusableBuffersStats.Get();
+ }
+ CountAndWatermark AllocatedBuffersStats() const {
+ return mAllocatedBuffersStats.Get();
+ }
+
+ private:
+ // Structure containing the element to be stored, and a validity-marker.
+ class BufferedElement {
+ public:
+ // Run aF on an invalid element, and mark it as valid.
+ template <typename F>
+ void SetAndValidate(F&& aF, Index aIndex) {
+ MOZ_ASSERT(!mValid);
+ aF(mT, aIndex);
+ mValid = true;
+ }
+
+ // Run aF on a valid element and mark it as invalid, return true.
+ // Return false if element was invalid.
+ template <typename F>
+ bool ReadAndInvalidate(F&& aF) {
+ if (!mValid) {
+ return false;
+ }
+ aF(mT);
+ mValid = false;
+ return true;
+ }
+
+ private:
+ T mT;
+ // mValid should be atomically changed to true *after* mT has been written,
+ // so that the reader can only see valid data.
+ // ReleaseAcquire, because when set to `true`, we want the just-written mT
+ // to be visible to the thread reading this `true`; and when set to `false`,
+ // we want the previous reads to have completed.
+ Atomic<bool, ReleaseAcquire> mValid{false};
+ };
+
+ // Buffer contains a sequence of BufferedElements starting at a specific
+ // index, and it points to the next-older buffer (if any).
+ class Buffer {
+ public:
+ // Constructor of the very first buffer.
+ Buffer() : mOlder(nullptr), mNewer(nullptr), mOrigin(0) {}
+
+ // Constructor of later buffers.
+ Buffer(Buffer* aOlder, Index aOrigin)
+ : mOlder(aOlder), mNewer(nullptr), mOrigin(aOrigin) {
+ MOZ_ASSERT(aOlder);
+ aOlder->mNewer = this;
+ }
+
+ Buffer* Older() const { return mOlder; }
+ void SetOlder(Buffer* aOlder) { mOlder = aOlder; }
+
+ Buffer* Newer() const { return mNewer; }
+ void SetNewer(Buffer* aNewer) { mNewer = aNewer; }
+
+ Index Origin() const { return mOrigin; }
+ void SetOrigin(Index aOrigin) { mOrigin = aOrigin; }
+
+ // Run aF on a yet-invalid element.
+ // Not thread-safe by itself, but nothing else should write this element,
+ // and reader won't access it until after it becomes valid.
+ template <typename F>
+ void SetAndValidateElement(F&& aF, Index aIndex) {
+ MOZ_ASSERT(aIndex >= Origin());
+ MOZ_ASSERT(aIndex < Origin() + BufferSize);
+ mElements[aIndex - Origin()].SetAndValidate(aF, aIndex);
+ }
+
+ using DidReadLastElement = bool;
+
+ // Read all valid elements starting at aIndex, marking them invalid and
+ // updating aIndex.
+ // Returns true if we ended up reading the last element in this buffer.
+ // Accessing the validity bit is thread-safe (as it's atomic), but once
+ // an element is valid, the reading itself is not thread-safe and should be
+ // guarded.
+ template <typename F>
+ DidReadLastElement ReadAndInvalidateAll(F&& aF, Index& aIndex) {
+ MOZ_ASSERT(aIndex >= Origin());
+ MOZ_ASSERT(aIndex < Origin() + BufferSize);
+ for (; aIndex < Origin() + BufferSize; ++aIndex) {
+ if (!mElements[aIndex - Origin()].ReadAndInvalidate(aF)) {
+ // Found an invalid element, stop here. (aIndex will not be updated
+ // past it, so we will start from here next time.)
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private:
+ Buffer* mOlder;
+ Buffer* mNewer;
+ Index mOrigin;
+ BufferedElement mElements[BufferSize];
+ };
+
+ // Reuse a buffer, or create a new one.
+ // All buffered elements will be invalid.
+ Buffer* NewBuffer(Buffer* aOlder, Index aOrigin) {
+ MOZ_ASSERT(aOlder);
+ for (;;) {
+ Buffer* head = mReusableBuffers;
+ if (!head) {
+ ++mAllocatedBuffersStats;
+ ++mLiveBuffersStats;
+ Buffer* buffer = new Buffer(aOlder, aOrigin);
+ return buffer;
+ }
+ Buffer* older = head->Older();
+ // Try to pivot the reusable-buffer pointer from the current head to the
+ // next buffer in line.
+ if (mReusableBuffers.compareExchange(head, older)) {
+ // Success! The reusable-buffer pointer now points at the older buffer,
+ // so we can recycle this ex-head.
+ --mReusableBuffersStats;
+ ++mLiveBuffersStats;
+ head->SetOlder(aOlder);
+ aOlder->SetNewer(head);
+ // We will be the newest; newer-pointer should already be null.
+ MOZ_ASSERT(!head->Newer());
+ head->SetOrigin(aOrigin);
+ return head;
+ }
+ // Failure, someone else must have touched the list, loop to try again.
+ }
+ }
+
+ // Discard a fully-read buffer.
+ // If aDestroy is true, delete it.
+ // If aDestroy is false, move the buffer to a reusable-buffer stack.
+ void StopUsing(Buffer* aBuffer, bool aDestroy) {
+ --mLiveBuffersStats;
+
+ // We should only stop using the oldest buffer.
+ MOZ_ASSERT(!aBuffer->Older());
+ // The newest buffer should not be modified here.
+ MOZ_ASSERT(aBuffer->Newer());
+ MOZ_ASSERT(aBuffer->Newer()->Older() == aBuffer);
+ // Detach from the second-oldest buffer.
+ aBuffer->Newer()->SetOlder(nullptr);
+ // Make the second-oldest buffer the now-oldest buffer.
+ mOldestBuffer = aBuffer->Newer();
+
+ if (aDestroy) {
+ --mAllocatedBuffersStats;
+ delete aBuffer;
+ } else {
+ ++mReusableBuffersStats;
+ // The recycling stack only uses mOlder; mNewer is not needed.
+ aBuffer->SetNewer(nullptr);
+
+ // Make the given buffer the new head of reusable buffers.
+ for (;;) {
+ Buffer* head = mReusableBuffers;
+ aBuffer->SetOlder(head);
+ if (mReusableBuffers.compareExchange(head, aBuffer)) {
+ break;
+ }
+ }
+ }
+ }
+
+ // Index of the next element to write. Modified when an element index is
+ // claimed for a push. If the last element of a buffer is claimed, that push
+ // will be responsible for adding a new head buffer.
+ // Relaxed, because there is no synchronization based on this variable, each
+ // thread just needs to get a different value, and will then write different
+ // things (which themselves have some atomic validation before they may be
+ // read elsewhere, independent of this `mNextElementToWrite`.)
+ Atomic<Index::ValueType, Relaxed> mNextElementToWrite{0};
+
+ // Index that a live recent buffer reaches. If a push claims a lesser-or-
+ // equal number, the corresponding buffer is guaranteed to still be alive:
+ // - It will have been created before this index was updated,
+ // - It will not be destroyed until all its values have been written,
+ // including the one that just claimed a position within it.
+ // Also, the push that claims this exact number is responsible for adding the
+ // next buffer and updating this value accordingly.
+ // ReleaseAcquire, because when set to a certain value, the just-created
+ // buffer covering the new range must be visible to readers.
+ Atomic<Index::ValueType, ReleaseAcquire> mBuffersCoverAtLeastUpTo;
+
+ // Pointer to the most recent buffer. Never null.
+ // This is the most recent of a deque of yet-unread buffers.
+ // Only modified when adding a new head buffer.
+ // ReleaseAcquire, because when modified, the just-created new buffer must be
+ // visible to readers.
+ Atomic<Buffer*, ReleaseAcquire> mMostRecentBuffer;
+
+ // Stack of reusable buffers.
+ // ReleaseAcquire, because when modified, the just-added buffer must be
+ // visible to readers.
+ Atomic<Buffer*, ReleaseAcquire> mReusableBuffers;
+
+ // Template-provided locking mechanism to protect PopAll()-only member
+ // variables below.
+ ReaderLocking mReaderLocking;
+
+ // Pointer to the oldest buffer, which contains the new element to be popped.
+ // Never null.
+ Buffer* mOldestBuffer;
+
+ // Index of the next element to be popped.
+ Index mNextElementToPop{0};
+
+ // Stats.
+ class AtomicCountAndWatermark {
+ public:
+ explicit AtomicCountAndWatermark(int aCount)
+ : mCount(aCount), mWatermark(aCount) {}
+
+ int Count() const { return int(mCount); }
+
+ CountAndWatermark Get() const {
+ return CountAndWatermark{int(mCount), int(mWatermark)};
+ }
+
+ int operator++() {
+ int count = int(++mCount);
+ // Update watermark.
+ for (;;) {
+ int watermark = int(mWatermark);
+ if (watermark >= count) {
+ // printf("++[%p] -=> %d-%d\n", this, count, watermark);
+ break;
+ }
+ if (mWatermark.compareExchange(watermark, count)) {
+ // printf("++[%p] -x> %d-(was %d now %d)\n", this, count, watermark,
+ // count);
+ break;
+ }
+ }
+ return count;
+ }
+
+ int operator--() {
+ int count = int(--mCount);
+ // printf("--[%p] -> %d\n", this, count);
+ return count;
+ }
+
+ private:
+ // Relaxed, as these are just gathering stats, so consistency is not
+ // critical.
+ Atomic<int, Relaxed> mCount;
+ Atomic<int, Relaxed> mWatermark;
+ };
+ // All buffers in the mMostRecentBuffer deque.
+ AtomicCountAndWatermark mLiveBuffersStats;
+ // All buffers in the mReusableBuffers stack.
+ AtomicCountAndWatermark mReusableBuffersStats;
+ // All allocated buffers (sum of above).
+ AtomicCountAndWatermark mAllocatedBuffersStats;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_MultiWriterQueue_h_
diff --git a/dom/media/doctor/RollingNumber.h b/dom/media/doctor/RollingNumber.h
new file mode 100644
index 0000000000..a04296f8bc
--- /dev/null
+++ b/dom/media/doctor/RollingNumber.h
@@ -0,0 +1,163 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_RollingNumber_h_
+#define mozilla_RollingNumber_h_
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include <limits>
+
+namespace mozilla {
+
+// Unsigned number suited to index elements in a never-ending queue, as
+// order-comparison behaves nicely around the overflow.
+//
+// Additive operators work the same as for the underlying value type, but
+// expect "small" jumps, as should normally happen when manipulating indices.
+//
+// Comparison functions are different, they keep the ordering based on the
+// distance between numbers, modulo the value type range:
+// If the distance is less than half the range of the value type, the usual
+// ordering stays.
+// 0 < 1, 2^23 < 2^24
+// However if the distance is more than half the range, we assume that we are
+// continuing along the queue, and therefore consider the smaller number to
+// actually be greater!
+// uint(-1) < 0.
+//
+// The expected usage is to always work on nearby rolling numbers: slowly
+// incrementing/decrementing them, and translating&comparing them within a
+// small window.
+// To enforce this usage during development, debug-build assertions catch API
+// calls involving distances of more than a *quarter* of the type range.
+// In non-debug builds, all APIs will still work as consistently as possible
+// without crashing, but performing operations on "distant" nunbers could lead
+// to unexpected results.
+template <typename T>
+class RollingNumber {
+ static_assert(!std::numeric_limits<T>::is_signed,
+ "RollingNumber only accepts unsigned number types");
+
+ public:
+ using ValueType = T;
+
+ RollingNumber() : mIndex(0) {}
+
+ explicit RollingNumber(ValueType aIndex) : mIndex(aIndex) {}
+
+ RollingNumber(const RollingNumber&) = default;
+ RollingNumber& operator=(const RollingNumber&) = default;
+
+ ValueType Value() const { return mIndex; }
+
+ // Normal increments/decrements.
+
+ RollingNumber& operator++() {
+ ++mIndex;
+ return *this;
+ }
+
+ RollingNumber operator++(int) { return RollingNumber{mIndex++}; }
+
+ RollingNumber& operator--() {
+ --mIndex;
+ return *this;
+ }
+
+ RollingNumber operator--(int) { return RollingNumber{mIndex--}; }
+
+ RollingNumber& operator+=(const ValueType& aIncrement) {
+ MOZ_ASSERT(aIncrement <= MaxDiff);
+ mIndex += aIncrement;
+ return *this;
+ }
+
+ RollingNumber operator+(const ValueType& aIncrement) const {
+ RollingNumber n = *this;
+ return n += aIncrement;
+ }
+
+ RollingNumber& operator-=(const ValueType& aDecrement) {
+ MOZ_ASSERT(aDecrement <= MaxDiff);
+ mIndex -= aDecrement;
+ return *this;
+ }
+
+ // Translate a RollingNumber by a negative value.
+ RollingNumber operator-(const ValueType& aDecrement) const {
+ RollingNumber n = *this;
+ return n -= aDecrement;
+ }
+
+ // Distance between two RollingNumbers, giving a value.
+ ValueType operator-(const RollingNumber& aOther) const {
+ ValueType diff = mIndex - aOther.mIndex;
+ MOZ_ASSERT(diff <= MaxDiff);
+ return diff;
+ }
+
+ // Normal (in)equality operators.
+
+ bool operator==(const RollingNumber& aOther) const {
+ return mIndex == aOther.mIndex;
+ }
+ bool operator!=(const RollingNumber& aOther) const {
+ return !(*this == aOther);
+ }
+
+ // Modified comparison operators.
+
+ bool operator<(const RollingNumber& aOther) const {
+ const T& a = mIndex;
+ const T& b = aOther.mIndex;
+ // static_cast needed because of possible integer promotion
+ // (e.g., from uint8_t to int, which would make the test useless).
+ const bool lessThanOther = static_cast<ValueType>(a - b) > MidWay;
+ MOZ_ASSERT((lessThanOther ? (b - a) : (a - b)) <= MaxDiff);
+ return lessThanOther;
+ }
+
+ bool operator<=(const RollingNumber& aOther) const {
+ const T& a = mIndex;
+ const T& b = aOther.mIndex;
+ const bool lessishThanOther = static_cast<ValueType>(b - a) <= MidWay;
+ MOZ_ASSERT((lessishThanOther ? (b - a) : (a - b)) <= MaxDiff);
+ return lessishThanOther;
+ }
+
+ bool operator>=(const RollingNumber& aOther) const {
+ const T& a = mIndex;
+ const T& b = aOther.mIndex;
+ const bool greaterishThanOther = static_cast<ValueType>(a - b) <= MidWay;
+ MOZ_ASSERT((greaterishThanOther ? (a - b) : (b - a)) <= MaxDiff);
+ return greaterishThanOther;
+ }
+
+ bool operator>(const RollingNumber& aOther) const {
+ const T& a = mIndex;
+ const T& b = aOther.mIndex;
+ const bool greaterThanOther = static_cast<ValueType>(b - a) > MidWay;
+ MOZ_ASSERT((greaterThanOther ? (a - b) : (b - a)) <= MaxDiff);
+ return greaterThanOther;
+ }
+
+ private:
+ // MidWay is used to split the type range in two, to decide how two numbers
+ // are ordered.
+ static const T MidWay = std::numeric_limits<T>::max() / 2;
+#ifdef DEBUG
+ // MaxDiff is the expected maximum difference between two numbers, either
+ // during comparisons, or when adding/subtracting.
+ // This is only used during debugging, to highlight algorithmic issues.
+ static const T MaxDiff = std::numeric_limits<T>::max() / 4;
+#endif
+ ValueType mIndex;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_RollingNumber_h_
diff --git a/dom/media/doctor/moz.build b/dom/media/doctor/moz.build
new file mode 100644
index 0000000000..75ea07e8f6
--- /dev/null
+++ b/dom/media/doctor/moz.build
@@ -0,0 +1,40 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+TEST_DIRS += [
+ "test/gtest",
+]
+
+# Needed for plugin IPC types required by nsPluginHost
+include("/ipc/chromium/chromium-config.mozbuild")
+
+EXPORTS += [
+ "DDLogCategory.h",
+ "DDLoggedTypeTraits.h",
+ "DDLogObject.h",
+ "DDLogValue.h",
+ "DecoderDoctorDiagnostics.h",
+ "DecoderDoctorLogger.h",
+]
+
+UNIFIED_SOURCES += [
+ "DDLifetime.cpp",
+ "DDLifetimes.cpp",
+ "DDLogCategory.cpp",
+ "DDLogMessage.cpp",
+ "DDLogObject.cpp",
+ "DDLogUtils.cpp",
+ "DDLogValue.cpp",
+ "DDMediaLog.cpp",
+ "DDMediaLogs.cpp",
+ "DDTimeStamp.cpp",
+ "DecoderDoctorDiagnostics.cpp",
+ "DecoderDoctorLogger.cpp",
+]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/doctor/test/browser/browser.ini b/dom/media/doctor/test/browser/browser.ini
new file mode 100644
index 0000000000..3dda42c4e5
--- /dev/null
+++ b/dom/media/doctor/test/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+subsuite = media-bc
+tags = decoderdoctor
+support-files =
+
+[browser_decoderDoctor.js]
+[browser_doctor_notification.js]
diff --git a/dom/media/doctor/test/browser/browser_decoderDoctor.js b/dom/media/doctor/test/browser/browser_decoderDoctor.js
new file mode 100644
index 0000000000..c502131fec
--- /dev/null
+++ b/dom/media/doctor/test/browser/browser_decoderDoctor.js
@@ -0,0 +1,356 @@
+"use strict";
+
+// 'data' contains the notification data object:
+// - data.type must be provided.
+// - data.isSolved and data.decoderDoctorReportId will be added if not provided
+// (false and "testReportId" resp.)
+// - Other fields (e.g.: data.formats) may be provided as needed.
+// 'notificationMessage': Expected message in the notification bar.
+// Falsy if nothing is expected after the notification is sent, in which case
+// we won't have further checks, so the following parameters are not needed.
+// 'label': Expected button label. Falsy if no button is expected, in which case
+// we won't have further checks, so the following parameters are not needed.
+// 'accessKey': Expected access key for the button.
+// 'tabChecker': function(openedTab) called with the opened tab that resulted
+// from clicking the button.
+async function test_decoder_doctor_notification(
+ data,
+ notificationMessage,
+ label,
+ accessKey,
+ isLink,
+ tabChecker
+) {
+ const TEST_URL = "https://example.org";
+ // A helper closure to test notifications in same or different origins.
+ // 'test_cross_origin' is used to determine if the observers used in the test
+ // are notified in the same frame (when false) or in a cross origin iframe
+ // (when true).
+ async function create_tab_and_test(test_cross_origin) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: TEST_URL },
+ async function (browser) {
+ let awaitNotificationBar;
+ if (notificationMessage) {
+ awaitNotificationBar = BrowserTestUtils.waitForNotificationBar(
+ gBrowser,
+ browser,
+ "decoder-doctor-notification"
+ );
+ }
+
+ await SpecialPowers.spawn(
+ browser,
+ [data, test_cross_origin],
+ /* eslint-disable-next-line no-shadow */
+ async function (data, test_cross_origin) {
+ if (!test_cross_origin) {
+ // Notify in the same origin.
+ Services.obs.notifyObservers(
+ content.window,
+ "decoder-doctor-notification",
+ JSON.stringify(data)
+ );
+ return;
+ // Done notifying in the same origin.
+ }
+
+ // Notify in a different origin.
+ const CROSS_ORIGIN_URL = "https://example.com";
+ let frame = content.document.createElement("iframe");
+ frame.src = CROSS_ORIGIN_URL;
+ await new Promise(resolve => {
+ frame.addEventListener("load", () => {
+ resolve();
+ });
+ content.document.body.appendChild(frame);
+ });
+
+ await content.SpecialPowers.spawn(
+ frame,
+ [data],
+ async function (
+ /* eslint-disable-next-line no-shadow */
+ data
+ ) {
+ Services.obs.notifyObservers(
+ content.window,
+ "decoder-doctor-notification",
+ JSON.stringify(data)
+ );
+ }
+ );
+ // Done notifying in a different origin.
+ }
+ );
+
+ if (!notificationMessage) {
+ ok(
+ true,
+ "Tested notifying observers with a nonsensical message, no effects expected"
+ );
+ return;
+ }
+
+ let notification;
+ try {
+ notification = await awaitNotificationBar;
+ } catch (ex) {
+ ok(false, ex);
+ return;
+ }
+ ok(notification, "Got decoder-doctor-notification notification");
+ if (label?.l10nId) {
+ // Without the following statement, the
+ // test_cannot_initialize_pulseaudio
+ // will permanently fail on Linux.
+ if (label.l10nId === "moz-support-link-text") {
+ MozXULElement.insertFTLIfNeeded(
+ "browser/components/mozSupportLink.ftl"
+ );
+ }
+ label = await document.l10n.formatValue(label.l10nId);
+ }
+ if (isLink) {
+ let link = notification.messageText.querySelector("a");
+ if (link) {
+ // Seems to be a Windows specific quirk, but without this
+ // mutation observer the notification.messageText.textContent
+ // will not be updated. This will cause consistent failures
+ // on Windows.
+ await BrowserTestUtils.waitForMutationCondition(
+ link,
+ { childList: true },
+ () => link.textContent.trim()
+ );
+ }
+ }
+ is(
+ notification.messageText.textContent,
+ notificationMessage + (isLink && label ? ` ${label}` : ""),
+ "notification message should match expectation"
+ );
+
+ let button = notification.buttonContainer.querySelector("button");
+ let link = notification.messageText.querySelector("a");
+ if (!label) {
+ ok(!button, "There should not be a button");
+ ok(!link, "There should not be a link");
+ return;
+ }
+
+ if (isLink) {
+ ok(!button, "There should not be a button");
+ is(link.innerText, label, `notification link should be '${label}'`);
+ ok(
+ !link.hasAttribute("accesskey"),
+ "notification link should not have accesskey"
+ );
+ } else {
+ ok(!link, "There should not be a link");
+ is(
+ button.getAttribute("label"),
+ label,
+ `notification button should be '${label}'`
+ );
+ is(
+ button.getAttribute("accesskey"),
+ accessKey,
+ "notification button should have accesskey"
+ );
+ }
+
+ if (!tabChecker) {
+ ok(false, "Test implementation error: Missing tabChecker");
+ return;
+ }
+ let awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+ if (button) {
+ button.click();
+ } else {
+ link.click();
+ }
+ let openedTab = await awaitNewTab;
+ tabChecker(openedTab);
+ BrowserTestUtils.removeTab(openedTab);
+ }
+ );
+ }
+
+ if (typeof data.type === "undefined") {
+ ok(false, "Test implementation error: data.type must be provided");
+ return;
+ }
+ data.isSolved = data.isSolved || false;
+ if (typeof data.decoderDoctorReportId === "undefined") {
+ data.decoderDoctorReportId = "testReportId";
+ }
+
+ // Test same origin.
+ await create_tab_and_test(false);
+ // Test cross origin.
+ await create_tab_and_test(true);
+}
+
+function tab_checker_for_sumo(expectedPath) {
+ return function (openedTab) {
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ let url = baseURL + expectedPath;
+ is(
+ openedTab.linkedBrowser.currentURI.spec,
+ url,
+ `Expected '${url}' in new tab`
+ );
+ };
+}
+
+function tab_checker_for_webcompat(expectedParams) {
+ return function (openedTab) {
+ let urlString = openedTab.linkedBrowser.currentURI.spec;
+ let endpoint = Services.prefs.getStringPref(
+ "media.decoder-doctor.new-issue-endpoint",
+ ""
+ );
+ ok(
+ urlString.startsWith(endpoint),
+ `Expected URL starting with '${endpoint}', got '${urlString}'`
+ );
+ let params = new URL(urlString).searchParams;
+ for (let k in expectedParams) {
+ if (!params.has(k)) {
+ ok(false, `Expected ${k} in webcompat URL`);
+ } else {
+ is(
+ params.get(k),
+ expectedParams[k],
+ `Expected ${k}='${expectedParams[k]}' in webcompat URL`
+ );
+ }
+ }
+ };
+}
+
+add_task(async function test_platform_decoder_not_found() {
+ let message = "";
+ let decoderDoctorReportId = "";
+ let isLinux = AppConstants.platform == "linux";
+ if (isLinux) {
+ message = gNavigatorBundle.getString("decoder.noCodecsLinux.message");
+ decoderDoctorReportId = "MediaPlatformDecoderNotFound";
+ } else if (AppConstants.platform == "win") {
+ message = gNavigatorBundle.getString("decoder.noHWAcceleration.message");
+ decoderDoctorReportId = "MediaWMFNeeded";
+ }
+
+ await test_decoder_doctor_notification(
+ {
+ type: "platform-decoder-not-found",
+ decoderDoctorReportId,
+ formats: "testFormat",
+ },
+ message,
+ isLinux ? "" : { l10nId: "moz-support-link-text" },
+ isLinux ? "" : gNavigatorBundle.getString("decoder.noCodecs.accesskey"),
+ true,
+ tab_checker_for_sumo("fix-video-audio-problems-firefox-windows")
+ );
+});
+
+add_task(async function test_cannot_initialize_pulseaudio() {
+ let message = "";
+ // This is only sent on Linux.
+ if (AppConstants.platform == "linux") {
+ message = gNavigatorBundle.getString("decoder.noPulseAudio.message");
+ }
+
+ await test_decoder_doctor_notification(
+ { type: "cannot-initialize-pulseaudio", formats: "testFormat" },
+ message,
+ { l10nId: "moz-support-link-text" },
+ gNavigatorBundle.getString("decoder.noCodecs.accesskey"),
+ true,
+ tab_checker_for_sumo("fix-common-audio-and-video-issues")
+ );
+});
+
+add_task(async function test_unsupported_libavcodec() {
+ let message = "";
+ // This is only sent on Linux.
+ if (AppConstants.platform == "linux") {
+ message = gNavigatorBundle.getString(
+ "decoder.unsupportedLibavcodec.message"
+ );
+ }
+
+ await test_decoder_doctor_notification(
+ { type: "unsupported-libavcodec", formats: "testFormat" },
+ message
+ );
+});
+
+add_task(async function test_decode_error() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "media.decoder-doctor.new-issue-endpoint",
+ "http://example.com/webcompat",
+ ],
+ ["browser.fixup.fallback-to-https", false],
+ ],
+ });
+ let message = gNavigatorBundle.getString("decoder.decodeError.message");
+ await test_decoder_doctor_notification(
+ {
+ type: "decode-error",
+ decodeIssue: "DecodeIssue",
+ docURL: "DocURL",
+ resourceURL: "ResURL",
+ },
+ message,
+ gNavigatorBundle.getString("decoder.decodeError.button"),
+ gNavigatorBundle.getString("decoder.decodeError.accesskey"),
+ false,
+ tab_checker_for_webcompat({
+ url: "DocURL",
+ label: "type-media",
+ problem_type: "video_bug",
+ details: JSON.stringify({
+ "Technical Information:": "DecodeIssue",
+ "Resource:": "ResURL",
+ }),
+ })
+ );
+});
+
+add_task(async function test_decode_warning() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "media.decoder-doctor.new-issue-endpoint",
+ "http://example.com/webcompat",
+ ],
+ ],
+ });
+ let message = gNavigatorBundle.getString("decoder.decodeWarning.message");
+ await test_decoder_doctor_notification(
+ {
+ type: "decode-warning",
+ decodeIssue: "DecodeIssue",
+ docURL: "DocURL",
+ resourceURL: "ResURL",
+ },
+ message,
+ gNavigatorBundle.getString("decoder.decodeError.button"),
+ gNavigatorBundle.getString("decoder.decodeError.accesskey"),
+ false,
+ tab_checker_for_webcompat({
+ url: "DocURL",
+ label: "type-media",
+ problem_type: "video_bug",
+ details: JSON.stringify({
+ "Technical Information:": "DecodeIssue",
+ "Resource:": "ResURL",
+ }),
+ })
+ );
+});
diff --git a/dom/media/doctor/test/browser/browser_doctor_notification.js b/dom/media/doctor/test/browser/browser_doctor_notification.js
new file mode 100644
index 0000000000..5789622e23
--- /dev/null
+++ b/dom/media/doctor/test/browser/browser_doctor_notification.js
@@ -0,0 +1,265 @@
+/**
+ * This test is used to test whether the decoder doctor would report the error
+ * on the notification banner (checking that by observing message) or on the web
+ * console (checking that by listening to the test event).
+ * Error should be reported after calling `DecoderDoctorDiagnostics::StoreXXX`
+ * methods.
+ * - StoreFormatDiagnostics() [for checking if type is supported]
+ * - StoreDecodeError() [when decode error occurs]
+ * - StoreEvent() [for reporting audio sink error]
+ */
+
+// Only types being listed here would be allowed to display on a
+// notification banner. Otherwise, the error would only be showed on the
+// web console.
+var gAllowedNotificationTypes =
+ "MediaWMFNeeded,MediaFFMpegNotFound,MediaUnsupportedLibavcodec,MediaDecodeError,MediaCannotInitializePulseAudio,";
+
+// Used to check if the mime type in the notification is equal to what we set
+// before. This mime type doesn't reflect the real world siutation, i.e. not
+// every error listed in this test would happen on this type. An example, ffmpeg
+// not found would only happen on H264/AAC media.
+const gMimeType = "video/mp4";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.decoder-doctor.testing", true],
+ ["media.decoder-doctor.verbose", true],
+ ["media.decoder-doctor.notifications-allowed", gAllowedNotificationTypes],
+ ],
+ });
+ // transfer types to lower cases in order to match with `DecoderDoctorReportType`
+ gAllowedNotificationTypes = gAllowedNotificationTypes.toLowerCase();
+});
+
+add_task(async function testWMFIsNeeded() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "platform-decoder-not-found",
+ decoderDoctorReportId: "mediawmfneeded",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testFFMpegNotFound() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "platform-decoder-not-found",
+ decoderDoctorReportId: "mediaplatformdecodernotfound",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testLibAVCodecUnsupported() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "unsupported-libavcodec",
+ decoderDoctorReportId: "mediaunsupportedlibavcodec",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testCanNotPlayNoDecoder() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "cannot-play",
+ decoderDoctorReportId: "mediacannotplaynodecoders",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testNoDecoder() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "can-play-but-some-missing-decoders",
+ decoderDoctorReportId: "medianodecoders",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+const gErrorList = [
+ "NS_ERROR_DOM_MEDIA_ABORT_ERR",
+ "NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR",
+ "NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR",
+ "NS_ERROR_DOM_MEDIA_DECODE_ERR",
+ "NS_ERROR_DOM_MEDIA_FATAL_ERR",
+ "NS_ERROR_DOM_MEDIA_METADATA_ERR",
+ "NS_ERROR_DOM_MEDIA_OVERFLOW_ERR",
+ "NS_ERROR_DOM_MEDIA_MEDIASINK_ERR",
+ "NS_ERROR_DOM_MEDIA_DEMUXER_ERR",
+ "NS_ERROR_DOM_MEDIA_CDM_ERR",
+ "NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR",
+];
+
+add_task(async function testDecodeError() {
+ const type = "decode-error";
+ const decoderDoctorReportId = "mediadecodeerror";
+ for (let error of gErrorList) {
+ const tab = await createTab("about:blank");
+ info(`first to try if the error is not allowed to be reported`);
+ // No error is allowed to be reported in the notification banner.
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.decoder-doctor.decode-errors-allowed", ""]],
+ });
+ await setDecodeError(tab, {
+ type,
+ decoderDoctorReportId,
+ error,
+ shouldReportNotification: false,
+ });
+
+ // If the notification type is `MediaDecodeError` and the error type is
+ // listed in the pref, then the error would be reported to the
+ // notification banner.
+ info(`Then to try if the error is allowed to be reported`);
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.decoder-doctor.decode-errors-allowed", error]],
+ });
+ await setDecodeError(tab, {
+ type,
+ decoderDoctorReportId,
+ error,
+ shouldReportNotification: true,
+ });
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function testAudioSinkFailedStartup() {
+ const tab = await createTab("about:blank");
+ await setAudioSinkFailedStartup(tab, {
+ type: "cannot-initialize-pulseaudio",
+ decoderDoctorReportId: "mediacannotinitializepulseaudio",
+ // This error comes with `*`, see `DecoderDoctorDiagnostics::StoreEvent`
+ formats: "*",
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Following are helper functions
+ */
+async function createTab(url) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(window.gBrowser, url);
+ // Create observer in the content process in order to check the decoder
+ // doctor's notification that would be sent when an error occurs.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ content._notificationName = "decoder-doctor-notification";
+ content._obs = {
+ observe(subject, topic, data) {
+ let { type, decoderDoctorReportId, formats } = JSON.parse(data);
+ decoderDoctorReportId = decoderDoctorReportId.toLowerCase();
+ info(`received '${type}:${decoderDoctorReportId}:${formats}'`);
+ if (!this._resolve) {
+ ok(false, "receive unexpected notification?");
+ }
+ if (
+ type == this._type &&
+ decoderDoctorReportId == this._decoderDoctorReportId &&
+ formats == this._formats
+ ) {
+ ok(true, `received correct notification`);
+ Services.obs.removeObserver(content._obs, content._notificationName);
+ this._resolve();
+ this._resolve = null;
+ }
+ },
+ // Return a promise that will be resolved once receiving a notification
+ // which has equal data with the input parameters.
+ waitFor({ type, decoderDoctorReportId, formats }) {
+ if (this._resolve) {
+ ok(false, "already has a pending promise!");
+ return Promise.reject();
+ }
+ Services.obs.addObserver(content._obs, content._notificationName);
+ return new Promise(resolve => {
+ info(`waiting for '${type}:${decoderDoctorReportId}:${formats}'`);
+ this._resolve = resolve;
+ this._type = type;
+ this._decoderDoctorReportId = decoderDoctorReportId;
+ this._formats = formats;
+ });
+ },
+ };
+ content._waitForReport = (params, shouldReportNotification) => {
+ const reportToConsolePromise = new Promise(r => {
+ content.document.addEventListener(
+ "mozreportmediaerror",
+ _ => {
+ r();
+ },
+ { once: true }
+ );
+ });
+ const reportToNotificationBannerPromise = shouldReportNotification
+ ? content._obs.waitFor(params)
+ : Promise.resolve();
+ info(
+ `waitForConsole=true, waitForNotificationBanner=${shouldReportNotification}`
+ );
+ return Promise.all([
+ reportToConsolePromise,
+ reportToNotificationBannerPromise,
+ ]);
+ };
+ });
+ return tab;
+}
+
+async function setFormatDiagnosticsReportForMimeType(tab, params) {
+ const shouldReportNotification = gAllowedNotificationTypes.includes(
+ params.decoderDoctorReportId
+ );
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [params, shouldReportNotification],
+ async (params, shouldReportNotification) => {
+ const video = content.document.createElement("video");
+ SpecialPowers.wrap(video).setFormatDiagnosticsReportForMimeType(
+ params.formats,
+ params.decoderDoctorReportId
+ );
+ await content._waitForReport(params, shouldReportNotification);
+ }
+ );
+ ok(true, `finished check for ${params.decoderDoctorReportId}`);
+}
+
+async function setDecodeError(tab, params) {
+ info(`start check for ${params.error}`);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [params],
+ async (params, shouldReportNotification) => {
+ const video = content.document.createElement("video");
+ SpecialPowers.wrap(video).setDecodeError(params.error);
+ await content._waitForReport(params, params.shouldReportNotification);
+ }
+ );
+ ok(true, `finished check for ${params.error}`);
+}
+
+async function setAudioSinkFailedStartup(tab, params) {
+ const shouldReportNotification = gAllowedNotificationTypes.includes(
+ params.decoderDoctorReportId
+ );
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [params, shouldReportNotification],
+ async (params, shouldReportNotification) => {
+ const video = content.document.createElement("video");
+ const waitPromise = content._waitForReport(
+ params,
+ shouldReportNotification
+ );
+ SpecialPowers.wrap(video).setAudioSinkFailedStartup();
+ await waitPromise;
+ }
+ );
+}
diff --git a/dom/media/doctor/test/gtest/TestMultiWriterQueue.cpp b/dom/media/doctor/test/gtest/TestMultiWriterQueue.cpp
new file mode 100644
index 0000000000..35a89c9267
--- /dev/null
+++ b/dom/media/doctor/test/gtest/TestMultiWriterQueue.cpp
@@ -0,0 +1,382 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "MultiWriterQueue.h"
+
+#include "DDTimeStamp.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/Assertions.h"
+#include "nsDeque.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+
+#include <gtest/gtest.h>
+#include <type_traits>
+
+using mozilla::MultiWriterQueue;
+using mozilla::MultiWriterQueueDefaultBufferSize;
+using mozilla::MultiWriterQueueReaderLocking_Mutex;
+using mozilla::MultiWriterQueueReaderLocking_None;
+
+template <size_t BufferSize>
+static void TestMultiWriterQueueST(const int loops) {
+ using Q = MultiWriterQueue<int, BufferSize>;
+ Q q;
+
+ int pushes = 0;
+ // Go through 2 cycles of pushes&pops, to exercize reusable buffers.
+ for (int max = loops; max <= loops * 2; max *= 2) {
+ // Push all numbers.
+ for (int i = 1; i <= max; ++i) {
+ bool newBuffer = q.Push(i);
+ // A new buffer should be added at the last push of each buffer.
+ EXPECT_EQ(++pushes % BufferSize == 0, newBuffer);
+ }
+
+ // Pop numbers, should be FIFO.
+ int x = 0;
+ q.PopAll([&](int& i) { EXPECT_EQ(++x, i); });
+
+ // We should have got all numbers.
+ EXPECT_EQ(max, x);
+
+ // Nothing left.
+ q.PopAll([&](int&) { EXPECT_TRUE(false); });
+ }
+}
+
+TEST(MultiWriterQueue, SingleThreaded)
+{
+ TestMultiWriterQueueST<1>(10);
+ TestMultiWriterQueueST<2>(10);
+ TestMultiWriterQueueST<4>(10);
+
+ TestMultiWriterQueueST<10>(9);
+ TestMultiWriterQueueST<10>(10);
+ TestMultiWriterQueueST<10>(11);
+ TestMultiWriterQueueST<10>(19);
+ TestMultiWriterQueueST<10>(20);
+ TestMultiWriterQueueST<10>(21);
+ TestMultiWriterQueueST<10>(999);
+ TestMultiWriterQueueST<10>(1000);
+ TestMultiWriterQueueST<10>(1001);
+
+ TestMultiWriterQueueST<8192>(8192 * 4 + 1);
+}
+
+template <typename Q>
+static void TestMultiWriterQueueMT(int aWriterThreads, int aReaderThreads,
+ int aTotalLoops, const char* aPrintPrefix) {
+ Q q;
+
+ const int threads = aWriterThreads + aReaderThreads;
+ const int loops = aTotalLoops / aWriterThreads;
+
+ nsIThread** array = new nsIThread*[threads];
+
+ mozilla::Atomic<int> pushThreadsCompleted{0};
+ int pops = 0;
+
+ nsCOMPtr<nsIRunnable> popper = NS_NewRunnableFunction("MWQPopper", [&]() {
+ // int popsBefore = pops;
+ // int allocsBefore = q.AllocatedBuffersStats().mCount;
+ q.PopAll([&pops](const int& i) { ++pops; });
+ // if (pops != popsBefore ||
+ // q.AllocatedBuffersStats().mCount != allocsBefore) {
+ // printf("%s threads=1+%d loops/thread=%d pops=%d "
+ // "buffers: live=%d (w %d) reusable=%d (w %d) "
+ // "alloc=%d (w %d)\n",
+ // aPrintPrefix,
+ // aWriterThreads,
+ // loops,
+ // pops,
+ // q.LiveBuffersStats().mCount,
+ // q.LiveBuffersStats().mWatermark,
+ // q.ReusableBuffersStats().mCount,
+ // q.ReusableBuffersStats().mWatermark,
+ // q.AllocatedBuffersStats().mCount,
+ // q.AllocatedBuffersStats().mWatermark);
+ // }
+ });
+ // Cycle through reader threads.
+ mozilla::Atomic<size_t> readerThread{0};
+
+ double start = mozilla::ToSeconds(mozilla::DDNow());
+
+ for (int k = 0; k < threads; k++) {
+ // First `aReaderThreads` threads to pop, all others to push.
+ if (k < aReaderThreads) {
+ nsCOMPtr<nsIThread> t;
+ nsresult rv = NS_NewNamedThread("MWQThread", getter_AddRefs(t));
+ EXPECT_NS_SUCCEEDED(rv);
+ NS_ADDREF(array[k] = t);
+ } else {
+ nsCOMPtr<nsIThread> t;
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction("MWQPusher", [&, k]() {
+ // Give a bit of breathing space to construct other threads.
+ PR_Sleep(PR_MillisecondsToInterval(100));
+
+ for (int i = 0; i < loops; ++i) {
+ if (q.Push(k * threads + i) && aReaderThreads != 0) {
+ // Run a popper task every time we push the last element of a
+ // buffer.
+ array[++readerThread % aReaderThreads]->Dispatch(
+ popper, nsIThread::DISPATCH_NORMAL);
+ }
+ }
+ ++pushThreadsCompleted;
+ });
+ nsresult rv = NS_NewNamedThread("MWQThread", getter_AddRefs(t), r);
+ EXPECT_NS_SUCCEEDED(rv);
+ NS_ADDREF(array[k] = t);
+ }
+ }
+
+ for (int k = threads - 1; k >= 0; k--) {
+ array[k]->Shutdown();
+ NS_RELEASE(array[k]);
+ }
+ delete[] array;
+
+ // There may be a few more elements that haven't been read yet.
+ q.PopAll([&pops](const int& i) { ++pops; });
+ const int pushes = aWriterThreads * loops;
+ EXPECT_EQ(pushes, pops);
+ q.PopAll([](const int& i) { EXPECT_TRUE(false); });
+
+ double duration = mozilla::ToSeconds(mozilla::DDNow()) - start - 0.1;
+ printf(
+ "%s threads=%dw+%dr loops/thread=%d pushes=pops=%d duration=%fs "
+ "pushes/s=%f buffers: live=%d (w %d) reusable=%d (w %d) "
+ "alloc=%d (w %d)\n",
+ aPrintPrefix, aWriterThreads, aReaderThreads, loops, pushes, duration,
+ pushes / duration, q.LiveBuffersStats().mCount,
+ q.LiveBuffersStats().mWatermark, q.ReusableBuffersStats().mCount,
+ q.ReusableBuffersStats().mWatermark, q.AllocatedBuffersStats().mCount,
+ q.AllocatedBuffersStats().mWatermark);
+}
+
+// skip test on windows10-aarch64 due to unexpected test timeout at
+// MultiWriterSingleReader, bug 1526001
+#if !defined(_M_ARM64)
+TEST(MultiWriterQueue, MultiWriterSingleReader)
+{
+ // Small BufferSize, to exercize the buffer management code.
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 1, 0, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 1, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 2, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 3, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 4, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 5, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 6, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 7, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 8, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 9, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 10, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 16, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 32, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 64, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+
+ // A more real-life buffer size.
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, MultiWriterQueueDefaultBufferSize,
+ MultiWriterQueueReaderLocking_None>>(
+ 64, 1, 2 * 1024 * 1024,
+ "MultiWriterQueue<int, DefaultBufferSize, Locking_None>");
+
+ // DEBUG-mode thread-safety checks should make the following (multi-reader
+ // with no locking) crash; uncomment to verify.
+ // TestMultiWriterQueueMT<
+ // MultiWriterQueue<int, MultiWriterQueueDefaultBufferSize,
+ // MultiWriterQueueReaderLocking_None>>(64, 2, 2*1024*1024);
+}
+#endif
+
+// skip test on windows10-aarch64 due to unexpected test timeout at
+// MultiWriterMultiReade, bug 1526001
+#if !defined(_M_ARM64)
+TEST(MultiWriterQueue, MultiWriterMultiReader)
+{
+ static_assert(
+ std::is_same_v<
+ MultiWriterQueue<int, 10>,
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>,
+ "MultiWriterQueue reader locking should use Mutex by default");
+
+ // Small BufferSize, to exercize the buffer management code.
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 1, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 2, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 3, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 4, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 5, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 6, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 7, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 8, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 9, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 10, 4, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 16, 8, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 32, 16, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 64, 32, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+
+ // A more real-life buffer size.
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, MultiWriterQueueDefaultBufferSize,
+ MultiWriterQueueReaderLocking_Mutex>>(
+ 64, 32, 1024 * 1024,
+ "MultiWriterQueue<int, DefaultBufferSize, Locking_Mutex>");
+}
+#endif
+
+// Single-threaded use only.
+struct DequeWrapperST {
+ nsDeque<void> mDQ;
+
+ bool Push(int i) {
+ mDQ.PushFront(reinterpret_cast<void*>(static_cast<uintptr_t>(i)));
+ return true;
+ }
+ template <typename F>
+ void PopAll(F&& aF) {
+ while (mDQ.GetSize() != 0) {
+ int i = static_cast<int>(reinterpret_cast<uintptr_t>(mDQ.Pop()));
+ aF(i);
+ }
+ }
+
+ struct CountAndWatermark {
+ int mCount = 0;
+ int mWatermark = 0;
+ } mLiveBuffersStats, mReusableBuffersStats, mAllocatedBuffersStats;
+
+ CountAndWatermark LiveBuffersStats() const { return mLiveBuffersStats; }
+ CountAndWatermark ReusableBuffersStats() const {
+ return mReusableBuffersStats;
+ }
+ CountAndWatermark AllocatedBuffersStats() const {
+ return mAllocatedBuffersStats;
+ }
+};
+
+// Multi-thread (atomic) writes allowed, make sure you don't pop unless writes
+// can't happen.
+struct DequeWrapperAW : DequeWrapperST {
+ mozilla::Atomic<bool> mWriting{false};
+
+ bool Push(int i) {
+ while (!mWriting.compareExchange(false, true)) {
+ }
+ mDQ.PushFront(reinterpret_cast<void*>(static_cast<uintptr_t>(i)));
+ mWriting = false;
+ return true;
+ }
+};
+
+// Multi-thread writes allowed, make sure you don't pop unless writes can't
+// happen.
+struct DequeWrapperMW : DequeWrapperST {
+ mozilla::Mutex mMutex MOZ_UNANNOTATED;
+
+ DequeWrapperMW() : mMutex("DequeWrapperMW/MT") {}
+
+ bool Push(int i) {
+ mozilla::MutexAutoLock lock(mMutex);
+ mDQ.PushFront(reinterpret_cast<void*>(static_cast<uintptr_t>(i)));
+ return true;
+ }
+};
+
+// Multi-thread read&writes allowed.
+struct DequeWrapperMT : DequeWrapperMW {
+ template <typename F>
+ void PopAll(F&& aF) {
+ while (mDQ.GetSize() != 0) {
+ int i;
+ {
+ mozilla::MutexAutoLock lock(mMutex);
+ i = static_cast<int>(reinterpret_cast<uintptr_t>(mDQ.Pop()));
+ }
+ aF(i);
+ }
+ }
+};
+
+TEST(MultiWriterQueue, nsDequeBenchmark)
+{
+ TestMultiWriterQueueMT<DequeWrapperST>(1, 0, 2 * 1024 * 1024,
+ "DequeWrapperST ");
+
+ TestMultiWriterQueueMT<DequeWrapperAW>(1, 0, 2 * 1024 * 1024,
+ "DequeWrapperAW ");
+ TestMultiWriterQueueMT<DequeWrapperMW>(1, 0, 2 * 1024 * 1024,
+ "DequeWrapperMW ");
+ TestMultiWriterQueueMT<DequeWrapperMT>(1, 0, 2 * 1024 * 1024,
+ "DequeWrapperMT ");
+ TestMultiWriterQueueMT<DequeWrapperMT>(1, 1, 2 * 1024 * 1024,
+ "DequeWrapperMT ");
+
+ TestMultiWriterQueueMT<DequeWrapperAW>(8, 0, 2 * 1024 * 1024,
+ "DequeWrapperAW ");
+ TestMultiWriterQueueMT<DequeWrapperMW>(8, 0, 2 * 1024 * 1024,
+ "DequeWrapperMW ");
+ TestMultiWriterQueueMT<DequeWrapperMT>(8, 0, 2 * 1024 * 1024,
+ "DequeWrapperMT ");
+ TestMultiWriterQueueMT<DequeWrapperMT>(8, 1, 2 * 1024 * 1024,
+ "DequeWrapperMT ");
+}
diff --git a/dom/media/doctor/test/gtest/TestRollingNumber.cpp b/dom/media/doctor/test/gtest/TestRollingNumber.cpp
new file mode 100644
index 0000000000..cce06ae9ba
--- /dev/null
+++ b/dom/media/doctor/test/gtest/TestRollingNumber.cpp
@@ -0,0 +1,146 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "RollingNumber.h"
+
+#include "mozilla/Assertions.h"
+
+#include <cstdint>
+#include <gtest/gtest.h>
+#include <type_traits>
+
+using RN8 = mozilla::RollingNumber<uint8_t>;
+
+TEST(RollingNumber, Value)
+{
+ // Value type should reflect template argument.
+ static_assert(std::is_same_v<RN8::ValueType, uint8_t>);
+
+ // Default init to 0.
+ const RN8 n;
+ // Access through Value().
+ EXPECT_EQ(0, n.Value());
+
+ // Conversion constructor.
+ RN8 n42{42};
+ EXPECT_EQ(42, n42.Value());
+
+ // Copy Constructor.
+ RN8 n42Copied{n42};
+ EXPECT_EQ(42, n42Copied.Value());
+
+ // Assignment construction.
+ RN8 n42Assigned = n42;
+ EXPECT_EQ(42, n42Assigned.Value());
+
+ // Assignment.
+ n42 = n;
+ EXPECT_EQ(0, n42.Value());
+}
+
+TEST(RollingNumber, Operations)
+{
+ RN8 n;
+ EXPECT_EQ(0, n.Value());
+
+ RN8 nPreInc = ++n;
+ EXPECT_EQ(1, n.Value());
+ EXPECT_EQ(1, nPreInc.Value());
+
+ RN8 nPostInc = n++;
+ EXPECT_EQ(2, n.Value());
+ EXPECT_EQ(1, nPostInc.Value());
+
+ RN8 nPreDec = --n;
+ EXPECT_EQ(1, n.Value());
+ EXPECT_EQ(1, nPreDec.Value());
+
+ RN8 nPostDec = n--;
+ EXPECT_EQ(0, n.Value());
+ EXPECT_EQ(1, nPostDec.Value());
+
+ RN8 nPlus = n + 10;
+ EXPECT_EQ(0, n.Value());
+ EXPECT_EQ(10, nPlus.Value());
+
+ n += 20;
+ EXPECT_EQ(20, n.Value());
+
+ RN8 nMinus = n - 2;
+ EXPECT_EQ(20, n.Value());
+ EXPECT_EQ(18, nMinus.Value());
+
+ n -= 5;
+ EXPECT_EQ(15, n.Value());
+
+ uint8_t diff = nMinus - n;
+ EXPECT_EQ(3, diff);
+
+ // Overflows.
+ n = RN8(0);
+ EXPECT_EQ(0, n.Value());
+ n--;
+ EXPECT_EQ(255, n.Value());
+ n++;
+ EXPECT_EQ(0, n.Value());
+ n -= 10;
+ EXPECT_EQ(246, n.Value());
+ n += 20;
+ EXPECT_EQ(10, n.Value());
+}
+
+TEST(RollingNumber, Comparisons)
+{
+ uint8_t i = 0;
+ do {
+ RN8 n{i};
+ EXPECT_EQ(i, n.Value());
+ EXPECT_TRUE(n == n);
+ EXPECT_FALSE(n != n);
+ EXPECT_FALSE(n < n);
+ EXPECT_TRUE(n <= n);
+ EXPECT_FALSE(n > n);
+ EXPECT_TRUE(n >= n);
+
+ RN8 same = n;
+ EXPECT_TRUE(n == same);
+ EXPECT_FALSE(n != same);
+ EXPECT_FALSE(n < same);
+ EXPECT_TRUE(n <= same);
+ EXPECT_FALSE(n > same);
+ EXPECT_TRUE(n >= same);
+
+#ifdef DEBUG
+ // In debug builds, we are only allowed a quarter of the type range.
+ const uint8_t maxDiff = 255 / 4;
+#else
+ // In non-debug builds, we can go half-way up or down the type range, and
+ // still conserve the expected ordering.
+ const uint8_t maxDiff = 255 / 2;
+#endif
+ for (uint8_t add = 1; add <= maxDiff; ++add) {
+ RN8 bigger = n + add;
+ EXPECT_FALSE(n == bigger);
+ EXPECT_TRUE(n != bigger);
+ EXPECT_TRUE(n < bigger);
+ EXPECT_TRUE(n <= bigger);
+ EXPECT_FALSE(n > bigger);
+ EXPECT_FALSE(n >= bigger);
+ }
+
+ for (uint8_t sub = 1; sub <= maxDiff; ++sub) {
+ RN8 smaller = n - sub;
+ EXPECT_FALSE(n == smaller);
+ EXPECT_TRUE(n != smaller);
+ EXPECT_FALSE(n < smaller);
+ EXPECT_FALSE(n <= smaller);
+ EXPECT_TRUE(n > smaller);
+ EXPECT_TRUE(n >= smaller);
+ }
+
+ ++i;
+ } while (i != 0);
+}
diff --git a/dom/media/doctor/test/gtest/moz.build b/dom/media/doctor/test/gtest/moz.build
new file mode 100644
index 0000000000..7ae9eae130
--- /dev/null
+++ b/dom/media/doctor/test/gtest/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+if CONFIG["OS_TARGET"] != "Android":
+ UNIFIED_SOURCES += [
+ "TestMultiWriterQueue.cpp",
+ "TestRollingNumber.cpp",
+ ]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/media/doctor",
+]
+
+FINAL_LIBRARY = "xul-gtest"