diff options
Diffstat (limited to 'dom/media/doctor')
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" |