Bug 1275856 - Capture MediaRecorder DOMExceptions early in order to capture JS traces. r=jib
authorBryce Van Dyk <bvandyk@mozilla.com>
Mon, 07 Aug 2017 09:49:24 +1200
changeset 647078 2d3c818bf51db0fd21bac31f09affc3272ce8ebf
parent 647077 3c105bee024cafb8110af764b887707781064279
child 647079 c735325c95be798ade798ee0b0a8804902f42847
push id74288
push userhikezoe@mozilla.com
push dateWed, 16 Aug 2017 00:19:57 +0000
reviewersjib
bugs1275856
milestone57.0a1
Bug 1275856 - Capture MediaRecorder DOMExceptions early in order to capture JS traces. r=jib In order to expose the JS stack on aync exceptions from the MediaRecorder, these exceptions must be created at the time of the operation which led to the exception. E.g. during the start() operation. This changeset creates the exceptions ahead of time in order to expose the JS stack traces. MozReview-Commit-ID: HgDJrpjgidD
dom/media/MediaRecorder.cpp
dom/media/MediaRecorder.h
--- a/dom/media/MediaRecorder.cpp
+++ b/dom/media/MediaRecorder.cpp
@@ -104,23 +104,27 @@ private:
 NS_IMPL_ISUPPORTS(MediaRecorderReporter, nsIMemoryReporter);
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(MediaRecorder)
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaRecorder,
                                                   DOMEventTargetHelper)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMStream)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioNode)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSecurityDomException)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mUnknownDomException)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaRecorder,
                                                 DOMEventTargetHelper)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDOMStream)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioNode)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mSecurityDomException)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mUnknownDomException)
   tmp->UnRegisterActivityObserver();
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaRecorder)
   NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity)
 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
 
@@ -1050,16 +1054,19 @@ MediaRecorder::GetMimeType(nsString &aMi
 {
   aMimeType = mMimeType;
 }
 
 void
 MediaRecorder::Start(const Optional<int32_t>& aTimeSlice, ErrorResult& aResult)
 {
   LOG(LogLevel::Debug, ("MediaRecorder.Start %p", this));
+
+  InitializeDomExceptions();
+
   if (mState != RecordingState::Inactive) {
     aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
   if (GetSourceMediaStream()->IsFinished() || GetSourceMediaStream()->IsDestroyed()) {
     aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
@@ -1394,22 +1401,37 @@ MediaRecorder::NotifyError(nsresult aRv)
   MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
   nsresult rv = CheckInnerWindowCorrectness();
   if (NS_FAILED(rv)) {
     return;
   }
   MediaRecorderErrorEventInit init;
   init.mBubbles = false;
   init.mCancelable = false;
+  // These DOMExceptions have been created earlier so they can contain stack
+  // traces. We attach the appropriate one here to be fired. We should have
+  // exceptions here, but defensively check.
   switch (aRv) {
     case NS_ERROR_DOM_SECURITY_ERR:
-      init.mError = DOMException::Create(aRv);
+      if (!mSecurityDomException) {
+        LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
+          "mSecurityDomException was not initialized"));
+        mSecurityDomException = DOMException::Create(NS_ERROR_DOM_SECURITY_ERR);
+      }
+      init.mError = mSecurityDomException.forget();
       break;
     default:
-      init.mError = DOMException::Create(NS_ERROR_DOM_UNKNOWN_ERR);
+      if (!mUnknownDomException) {
+        LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
+          "mUnknownDomException was not initialized"));
+        mUnknownDomException = DOMException::Create(NS_ERROR_DOM_UNKNOWN_ERR);
+      }
+      LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
+        "mUnknownDomException being fired for aRv: %X", uint32_t(aRv)));
+      init.mError = mUnknownDomException.forget();
   }
 
   RefPtr<MediaRecorderErrorEvent> event = MediaRecorderErrorEvent::Constructor(
     this, NS_LITERAL_STRING("error"), init);
   event->SetTrusted(true);
 
   bool dummy;
   rv = DispatchEvent(event, &dummy);
@@ -1448,16 +1470,23 @@ MediaRecorder::GetSourceMediaStream()
 {
   if (mDOMStream != nullptr) {
     return mDOMStream->GetPlaybackStream();
   }
   MOZ_ASSERT(mAudioNode != nullptr);
   return mPipeStream ? mPipeStream.get() : mAudioNode->GetStream();
 }
 
+void
+MediaRecorder::InitializeDomExceptions()
+{
+  mSecurityDomException = DOMException::Create(NS_ERROR_DOM_SECURITY_ERR);
+  mUnknownDomException = DOMException::Create(NS_ERROR_DOM_UNKNOWN_ERR);
+}
+
 size_t
 MediaRecorder::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
 {
   size_t amount = 42;
   for (size_t i = 0; i < mSessions.Length(); ++i) {
     amount += mSessions[i]->SizeOfExcludingThis(aMallocSizeOf);
   }
   return amount;
--- a/dom/media/MediaRecorder.h
+++ b/dom/media/MediaRecorder.h
@@ -23,16 +23,17 @@ class ErrorResult;
 class MediaInputPort;
 struct MediaRecorderOptions;
 class MediaStream;
 class GlobalObject;
 
 namespace dom {
 
 class AudioNode;
+class DOMException;
 
 /**
  * Implementation of https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html
  * The MediaRecorder accepts a mediaStream as input source passed from UA. When recorder starts,
  * a MediaEncoder will be created and accept the mediaStream as input source.
  * Encoder will get the raw data by track data changes, encode it by selected MIME Type, then store the encoded in EncodedBufferCache object.
  * The encoded data will be extracted on every timeslice passed from Start function call or by RequestData function.
  * Thread model:
@@ -128,16 +129,22 @@ protected:
   void SetMimeType(const nsString &aMimeType);
   void SetOptions(const MediaRecorderOptions& aInitDict);
 
   MediaRecorder(const MediaRecorder& x) = delete; // prevent bad usage
   // Remove session pointer.
   void RemoveSession(Session* aSession);
   // Functions for Session to query input source info.
   MediaStream* GetSourceMediaStream();
+  // Create DOMExceptions capturing the JS stack for async errors. These are
+  // created ahead of time rather than on demand when firing an error as the JS
+  // stack of the operation that started the async behavior will not be
+  // available at the time the error event is fired. Note, depending on when
+  // this is called there may not be a JS stack to capture.
+  void InitializeDomExceptions();
   // DOM wrapper for source media stream. Will be null when input is audio node.
   RefPtr<DOMMediaStream> mDOMStream;
   // Source audio node. Will be null when input is a media stream.
   RefPtr<AudioNode> mAudioNode;
   // Pipe stream connecting non-destination source node and session track union
   // stream of recorder. Will be null when input is media stream or destination
   // node.
   RefPtr<AudioNodeStream> mPipeStream;
@@ -154,16 +161,22 @@ protected:
 
   // It specifies the container format as well as the audio and video capture formats.
   nsString mMimeType;
 
   uint32_t mAudioBitsPerSecond;
   uint32_t mVideoBitsPerSecond;
   uint32_t mBitsPerSecond;
 
+  // DOMExceptions that are created early and possibly thrown in NotifyError.
+  // Creating them early allows us to capture the JS stack for which cannot be
+  // done at the time the error event is fired.
+  RefPtr<DOMException> mSecurityDomException;
+  RefPtr<DOMException> mUnknownDomException;
+
 private:
   // Register MediaRecorder into Document to listen the activity changes.
   void RegisterActivityObserver();
   void UnRegisterActivityObserver();
 
   bool CheckPermission(const nsString &aType);
 };