2016-04-19 10:36:19 +03:00
/* -*- 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"
2016-04-19 10:36:19 +03:00
# include "mozilla/dom/DecoderDoctorNotificationBinding.h"
2016-04-19 10:36:19 +03:00
# include "mozilla/Logging.h"
2016-04-19 10:36:19 +03:00
# include "nsGkAtoms.h"
# include "nsIDocument.h"
2016-04-19 10:36:19 +03:00
# include "nsIObserverService.h"
2016-04-19 10:36:19 +03:00
# include "nsITimer.h"
# include "nsIWeakReference.h"
2016-04-19 10:36:19 +03:00
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__)
2016-04-19 10:36:19 +03:00
# define DD_INFO(arg, ...) DD_LOG(mozilla::LogLevel::Info, arg, ##__VA_ARGS__)
2016-04-19 10:36:19 +03:00
# define DD_WARN(arg, ...) DD_LOG(mozilla::LogLevel::Warning, arg, ##__VA_ARGS__)
namespace mozilla
{
2016-04-19 10:36:19 +03:00
// 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 :
2016-04-22 06:42:11 +03:00
static already_AddRefed < DecoderDoctorDocumentWatcher >
2016-04-19 10:36:19 +03:00
RetrieveOrCreate ( nsIDocument * aDocument ) ;
NS_DECL_ISUPPORTS
NS_DECL_NSITIMERCALLBACK
2016-04-22 06:42:11 +03:00
void AddDiagnostics ( DecoderDoctorDiagnostics & & aDiagnostics ,
const char * aCallSite ) ;
2016-04-19 10:36:19 +03:00
private :
explicit DecoderDoctorDocumentWatcher ( nsIDocument * 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 ,
nsIAtom * aPropertyName ,
void * aPropertyValue ,
void * aData ) ;
static const uint32_t sAnalysisPeriod_ms = 1000 ;
void EnsureTimerIsStarted ( ) ;
2016-04-19 10:36:19 +03:00
void ReportAnalysis ( dom : : DecoderDoctorNotificationType aNotificationType ,
const char * aReportStringId ,
2016-04-19 10:36:19 +03:00
const nsAString & aFormats ) ;
void SynthesizeAnalysis ( ) ;
// Raw pointer to an nsIDocument.
// 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.
nsIDocument * mDocument ;
struct Diagnostics
{
Diagnostics ( DecoderDoctorDiagnostics & & aDiagnostics ,
const char * aCallSite )
: mDecoderDoctorDiagnostics ( Move ( aDiagnostics ) )
, mCallSite ( aCallSite )
{ }
Diagnostics ( const Diagnostics & ) = delete ;
Diagnostics ( Diagnostics & & aOther )
: mDecoderDoctorDiagnostics ( Move ( aOther . mDecoderDoctorDiagnostics ) )
, mCallSite ( Move ( aOther . mCallSite ) )
{ }
const DecoderDoctorDiagnostics mDecoderDoctorDiagnostics ;
const nsCString mCallSite ;
} ;
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 )
// static
2016-04-22 06:42:11 +03:00
already_AddRefed < DecoderDoctorDocumentWatcher >
2016-04-19 10:36:19 +03:00
DecoderDoctorDocumentWatcher : : RetrieveOrCreate ( nsIDocument * 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 ( ) ) ;
}
2016-04-22 06:42:11 +03:00
return watcher . forget ( ) ;
2016-04-19 10:36:19 +03:00
}
DecoderDoctorDocumentWatcher : : DecoderDoctorDocumentWatcher ( nsIDocument * 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 remove the property and call our DestroyPropertyCallback.
mDocument - > DeleteProperty ( 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 ,
nsIAtom * aPropertyName ,
void * aPropertyValue ,
void * )
{
MOZ_ASSERT ( NS_IsMainThread ( ) ) ;
# ifdef DEBUG
nsIDocument * document = static_cast < nsIDocument * > ( aObject ) ;
# endif
MOZ_ASSERT ( aPropertyName = = nsGkAtoms : : decoderDoctor ) ;
DecoderDoctorDocumentWatcher * watcher =
static_cast < DecoderDoctorDocumentWatcher * > ( aPropertyValue ) ;
MOZ_ASSERT ( watcher ) ;
MOZ_ASSERT ( watcher - > mDocument = = document ) ;
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 ) {
mTimer = do_CreateInstance ( NS_TIMER_CONTRACTID ) ;
if ( NS_WARN_IF ( ! mTimer ) ) {
return ;
}
if ( NS_WARN_IF ( NS_FAILED (
mTimer - > InitWithCallback (
this , sAnalysisPeriod_ms , nsITimer : : TYPE_ONE_SHOT ) ) ) ) {
mTimer = nullptr ;
}
}
}
2016-04-19 10:36:19 +03:00
static void
DispatchNotification ( nsISupports * aSubject ,
dom : : DecoderDoctorNotificationType aNotificationType ,
const nsAString & aFormats )
{
if ( ! aSubject ) {
return ;
}
dom : : DecoderDoctorNotification data ;
data . mType = aNotificationType ;
if ( ! aFormats . IsEmpty ( ) ) {
data . mFormats . Construct ( aFormats ) ;
}
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 ( ) ) ;
}
}
2016-04-19 10:36:19 +03:00
void
2016-04-19 10:36:19 +03:00
DecoderDoctorDocumentWatcher : : ReportAnalysis (
dom : : DecoderDoctorNotificationType aNotificationType ,
const char * aReportStringId ,
const nsAString & aFormats )
2016-04-19 10:36:19 +03:00
{
MOZ_ASSERT ( NS_IsMainThread ( ) ) ;
if ( ! mDocument ) {
return ;
}
const char16_t * params [ ] = { aFormats . Data ( ) } ;
DD_DEBUG ( " DecoderDoctorDocumentWatcher[%p, doc=%p]::ReportAnalysis() ReportToConsole - aMsg='%s' params[0]='%s' " ,
this , mDocument , aReportStringId ,
NS_ConvertUTF16toUTF8 ( params [ 0 ] ) . get ( ) ) ;
nsContentUtils : : ReportToConsole ( nsIScriptError : : warningFlag ,
NS_LITERAL_CSTRING ( " Media " ) ,
mDocument ,
nsContentUtils : : eDOM_PROPERTIES ,
aReportStringId ,
params ,
ArrayLength ( params ) ) ;
2016-04-19 10:36:19 +03:00
2016-04-19 10:36:20 +03:00
// For now, disable all front-end notifications by default.
// TODO: Future bugs will use finer-grained filtering instead.
if ( Preferences : : GetBool ( " media.decoderdoctor.enable-notification-bar " , false ) ) {
DispatchNotification (
mDocument - > GetInnerWindow ( ) , aNotificationType , aFormats ) ;
}
2016-04-19 10:36:19 +03:00
}
void
DecoderDoctorDocumentWatcher : : SynthesizeAnalysis ( )
{
MOZ_ASSERT ( NS_IsMainThread ( ) ) ;
bool canPlay = false ;
2016-04-19 10:36:20 +03:00
# if defined(MOZ_FFMPEG)
2016-04-22 06:42:11 +03:00
bool FFMpegNeeded = false ;
2016-04-19 10:36:20 +03:00
# endif
2016-04-22 06:42:11 +03:00
nsAutoString unplayableFormats ;
2016-04-19 10:36:19 +03:00
for ( auto & diag : mDiagnosticsSequence ) {
2016-04-22 06:42:11 +03:00
if ( ! diag . mDecoderDoctorDiagnostics . Format ( ) . IsEmpty ( ) ) {
if ( diag . mDecoderDoctorDiagnostics . CanPlay ( ) ) {
canPlay = true ;
} else {
2016-04-19 10:36:20 +03:00
# if defined(MOZ_FFMPEG)
2016-04-22 06:42:11 +03:00
if ( diag . mDecoderDoctorDiagnostics . DidFFmpegFailToLoad ( ) ) {
FFMpegNeeded = true ;
}
2016-04-19 10:36:20 +03:00
# endif
2016-04-22 06:42:11 +03:00
if ( ! unplayableFormats . IsEmpty ( ) ) {
unplayableFormats + = NS_LITERAL_STRING ( " , " ) ;
}
unplayableFormats + = diag . mDecoderDoctorDiagnostics . Format ( ) ;
2016-04-19 10:36:19 +03:00
}
}
}
2016-04-22 06:42:11 +03:00
2016-04-19 10:36:19 +03:00
if ( ! canPlay ) {
2016-04-19 10:36:20 +03:00
# if defined(MOZ_FFMPEG)
2016-04-22 06:42:11 +03:00
if ( FFMpegNeeded ) {
DD_DEBUG ( " DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> Cannot play media because platform decoder was not found " ,
this , mDocument , NS_ConvertUTF16toUTF8 ( unplayableFormats ) . get ( ) ) ;
2016-04-19 10:36:20 +03:00
ReportAnalysis ( dom : : DecoderDoctorNotificationType : : Platform_decoder_not_found ,
2016-04-22 06:42:11 +03:00
" MediaPlatformDecoderNotFound " , unplayableFormats ) ;
2016-04-19 10:36:20 +03:00
} else
# endif
{
2016-04-22 06:42:11 +03:00
DD_WARN ( " DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Cannot play media, unplayable formats: %s " ,
this , mDocument , NS_ConvertUTF16toUTF8 ( unplayableFormats ) . get ( ) ) ;
2016-04-19 10:36:20 +03:00
ReportAnalysis ( dom : : DecoderDoctorNotificationType : : Cannot_play ,
2016-04-22 06:42:11 +03:00
" MediaCannotPlayNoDecoders " , unplayableFormats ) ;
2016-04-19 10:36:20 +03:00
}
2016-04-22 06:42:11 +03:00
} else if ( ! unplayableFormats . IsEmpty ( ) ) {
DD_INFO ( " DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Can play media, but no decoders for some requested formats: %s " ,
this , mDocument , NS_ConvertUTF16toUTF8 ( unplayableFormats ) . get ( ) ) ;
2016-04-19 10:36:19 +03:00
if ( Preferences : : GetBool ( " media.decoderdoctor.verbose " , false ) ) {
2016-04-19 10:36:19 +03:00
ReportAnalysis (
dom : : DecoderDoctorNotificationType : : Can_play_but_some_missing_decoders ,
2016-04-22 06:42:11 +03:00
" MediaNoDecoders " , unplayableFormats ) ;
2016-04-19 10:36:19 +03:00
}
} else {
2016-04-22 06:42:11 +03:00
DD_DEBUG ( " DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Can play media, decoders available for all requested formats " ,
2016-04-19 10:36:19 +03:00
this , mDocument ) ;
}
}
void
2016-04-22 06:42:11 +03:00
DecoderDoctorDocumentWatcher : : AddDiagnostics ( DecoderDoctorDiagnostics & & aDiagnostics ,
const char * aCallSite )
2016-04-19 10:36:19 +03:00
{
MOZ_ASSERT ( NS_IsMainThread ( ) ) ;
if ( ! mDocument ) {
return ;
}
2016-04-22 06:42:11 +03:00
DD_DEBUG ( " DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics(DecoderDoctorDiagnostics{%s}, call site '%s') " ,
this , mDocument , aDiagnostics . GetDescription ( ) . Data ( ) , aCallSite ) ;
2016-04-19 10:36:19 +03:00
mDiagnosticsSequence . AppendElement (
2016-04-22 06:42:11 +03:00
Diagnostics ( Move ( aDiagnostics ) , aCallSite ) ) ;
2016-04-19 10:36:19 +03:00
EnsureTimerIsStarted ( ) ;
}
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 ( ) ;
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 ;
}
2016-04-19 10:36:19 +03:00
void
2016-04-22 06:42:11 +03:00
DecoderDoctorDiagnostics : : StoreFormatDiagnostics ( nsIDocument * aDocument ,
const nsAString & aFormat ,
bool aCanPlay ,
const char * aCallSite )
2016-04-19 10:36:19 +03:00
{
2016-04-19 10:36:19 +03:00
MOZ_ASSERT ( NS_IsMainThread ( ) ) ;
2016-04-19 10:36:19 +03:00
if ( NS_WARN_IF ( ! aDocument ) ) {
2016-04-22 06:42:11 +03:00
DD_WARN ( " DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(nsIDocument* 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(nsIDocument* aDocument=%p, format=<empty>, can-play=%d, call site '%s') " ,
this , aDocument , aCanPlay , aCallSite ) ;
2016-04-19 10:36:19 +03:00
return ;
}
2016-04-19 10:36:19 +03:00
RefPtr < DecoderDoctorDocumentWatcher > watcher =
DecoderDoctorDocumentWatcher : : RetrieveOrCreate ( aDocument ) ;
if ( NS_WARN_IF ( ! watcher ) ) {
2016-04-22 06:42:11 +03:00
DD_WARN ( " DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(nsIDocument* aDocument=%p, format='%s', can-play=%d, call site '%s') - Could not create document watcher " ,
this , aDocument , NS_ConvertUTF16toUTF8 ( aFormat ) . get ( ) , aCanPlay , aCallSite ) ;
2016-04-19 10:36:19 +03:00
return ;
}
2016-04-22 06:42:11 +03:00
mFormat = aFormat ;
mCanPlay = aCanPlay ;
2016-04-19 10:36:19 +03:00
// StoreDiagnostics should only be called once, after all data is available,
// so it is safe to Move() from this object.
2016-04-22 06:42:11 +03:00
watcher - > AddDiagnostics ( Move ( * this ) , aCallSite ) ;
}
nsCString
DecoderDoctorDiagnostics : : GetDescription ( ) const
{
nsCString s ;
if ( ! mFormat . IsEmpty ( ) ) {
s = " format=' " ;
s + = NS_ConvertUTF16toUTF8 ( mFormat ) . get ( ) ;
s + = mCanPlay ? " ', can play " : " ', cannot play " ;
if ( mFFmpegFailedToLoad ) {
s + = " , Linux platform decoder failed to load " ;
}
} else {
s = " ? " ;
}
return s ;
2016-04-19 10:36:19 +03:00
}
} // namespace mozilla